managment frontend initiated

This commit is contained in:
berkay 2025-04-27 14:12:49 +03:00
parent ba784c40e4
commit 090567ade8
65 changed files with 5469 additions and 177 deletions

View File

@ -6,7 +6,6 @@ from Endpoints.routes import get_safe_endpoint_urls
async def token_middleware(request: Request, call_next):
base_url = request.url.path
safe_endpoints = [_[0] for _ in get_safe_endpoint_urls()]
if base_url in safe_endpoints:

View File

@ -22,7 +22,7 @@ class Configs(BaseSettings):
EMAIL_HOST: str = ""
DATETIME_FORMAT: str = ""
FORGOT_LINK: str = ""
ALLOW_ORIGINS: list = ["http://localhost:3000"]
ALLOW_ORIGINS: list = ["http://localhost:3000", "http://localhost:3001"]
VERSION: str = "0.1.001"
DESCRIPTION: str = ""

View File

@ -22,7 +22,7 @@ class Configs(BaseSettings):
EMAIL_HOST: str = ""
DATETIME_FORMAT: str = ""
FORGOT_LINK: str = ""
ALLOW_ORIGINS: list = ["http://localhost:3000"]
ALLOW_ORIGINS: list = ["http://localhost:3000", "http://localhost:3001"]
VERSION: str = "0.1.001"
DESCRIPTION: str = ""

View File

@ -304,7 +304,7 @@ def authentication_token_check_post(
status_code=status.HTTP_406_NOT_ACCEPTABLE,
headers=headers,
)
if AuthHandlers.LoginHandler.authentication_check_token_valid(access_token=token):
if AuthHandlers.LoginHandler.authentication_check_token_valid(domain=domain,access_token=token):
return JSONResponse(
content={"message": "MSG_0001"},
status_code=status.HTTP_202_ACCEPTED,

View File

@ -171,8 +171,18 @@ class LoginHandler:
access_key=data.access_key, db_session=db_session
)
other_domains_list, main_domain = [], ""
with mongo_handler.collection(f"{str(found_user.related_company)}*Domain") as collection:
result = collection.find_one({"user_uu_id": str(found_user.uu_id)})
if not result:
raise ValueError("EYS_00087")
other_domains_list = result.get("other_domains_list", [])
main_domain = result.get("main_domain", None)
if domain not in other_domains_list or not main_domain:
raise ValueError("EYS_00088")
if not user_handler.check_password_valid(
domain=domain or "",
domain=main_domain,
id_=str(found_user.uu_id),
password=data.password,
password_hashed=found_user.hash_password,
@ -233,6 +243,7 @@ class LoginHandler:
person_id=found_user.person_id,
person_uu_id=str(person.uu_id),
request=dict(request.headers),
domain_list=other_domains_list,
companies_uu_id_list=companies_uu_id_list,
companies_id_list=companies_id_list,
duty_uu_id_list=duty_uu_id_list,
@ -286,13 +297,24 @@ class LoginHandler:
found_user = user_handler.check_user_exists(
access_key=data.access_key, db_session=db_session
)
other_domains_list, main_domain = [], ""
with mongo_handler.collection(f"{str(found_user.related_company)}*Domain") as collection:
result = collection.find_one({"user_uu_id": str(found_user.uu_id)})
if not result:
raise ValueError("EYS_00087")
other_domains_list = result.get("other_domains_list", [])
main_domain = result.get("main_domain", None)
if domain not in other_domains_list or not main_domain:
raise ValueError("EYS_00088")
if not user_handler.check_password_valid(
domain=domain,
domain=main_domain,
id_=str(found_user.uu_id),
password=data.password,
password_hashed=found_user.hash_password,
):
raise ValueError("EYS_0005")
occupants_selection_dict: Dict[str, Any] = {}
living_spaces: list[BuildLivingSpace] = BuildLivingSpace.filter_all(
@ -343,6 +365,7 @@ class LoginHandler:
user_id=found_user.id,
person_id=person.id,
person_uu_id=str(person.uu_id),
domain_list=other_domains_list,
request=dict(request.headers),
available_occupants=occupants_selection_dict,
).model_dump()
@ -606,10 +629,17 @@ class LoginHandler:
)
@classmethod
def authentication_check_token_valid(cls, access_token: str) -> bool:
def authentication_check_token_valid(cls, domain, access_token: str) -> bool:
redis_handler = RedisHandlers()
if redis_handler.get_object_from_redis(access_token=access_token):
return True
if auth_token := redis_handler.get_object_from_redis(access_token=access_token):
if auth_token.is_employee:
if domain not in auth_token.domain_list:
raise ValueError("EYS_00112")
return True
elif auth_token.is_occupant:
if domain not in auth_token.domain_list:
raise ValueError("EYS_00113")
return True
return False

View File

@ -5,6 +5,7 @@ from ..config import api_config
async def token_middleware(request: Request, call_next):
print("Token Middleware", dict(request.headers))
base_url = request.url.path
safe_endpoints = [_[0] for _ in get_safe_endpoint_urls()]
if base_url in safe_endpoints:

View File

@ -35,6 +35,7 @@ class ApplicationToken(BaseModel):
person_uu_id: str
request: Optional[dict] = None # Request Info of Client
domain_list: Optional[list[str]] = None
expires_at: Optional[float] = None # Expiry timestamp

View File

@ -0,0 +1,41 @@
#!/bin/bash
# Exit on error
set -e
echo "🚀 Setting up shadcn/ui components and dependencies..."
# Install required dependencies for shadcn
echo "📦 Installing required dependencies..."
npm install tailwindcss-animate class-variance-authority clsx tailwind-merge --legacy-peer-deps
# Initialize shadcn/ui (components.json already exists, so we'll skip this step)
echo "✅ components.json already exists"
# Set npm config to use legacy-peer-deps for all npm operations
echo "🔧 Setting npm to use legacy-peer-deps..."
npm config set legacy-peer-deps true
# Install base components
echo "🧩 Installing base shadcn/ui components..."
npx shadcn@latest add button --yes
npx shadcn@latest add form --yes
npx shadcn@latest add input --yes
npx shadcn@latest add label --yes
npx shadcn@latest add select --yes
npx shadcn@latest add checkbox --yes
npx shadcn@latest add card --yes
npx shadcn@latest add dialog --yes
npx shadcn@latest add popover --yes
npx shadcn@latest add sonner --yes
npx shadcn@latest add table --yes
npx shadcn@latest add pagination --yes
npx shadcn@latest add calendar --yes
npx shadcn@latest add date-picker --yes
# Update any dependencies with legacy peer deps
echo "🔄 Updating dependencies..."
npm install --legacy-peer-deps
echo "✨ Setup complete! You can now use shadcn/ui components in your project."
echo "📚 Documentation: https://ui.shadcn.com/docs"

View File

@ -1,23 +1,95 @@
"use server";
export default async function Home() {
const result = await fetch("http://auth_service:8001/authentication/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
language: "tr",
domain: "evyos.com.tr",
tz: "GMT+3",
},
body: JSON.stringify({
access_key: "karatay.berkay.sup@evyos.com.tr",
password: "string",
remember_me: true,
}),
// Server-side rendering
const currentDate = new Date().toLocaleString("tr-TR", {
timeZone: "Europe/Istanbul",
});
const data = await result.json();
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
{JSON.stringify({ data: data?.data })}
<div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-b from-blue-50 to-white p-8">
<div className="w-full max-w-4xl bg-white rounded-xl shadow-lg overflow-hidden">
{/* Welcome Banner */}
<div className="bg-blue-600 text-white p-8 text-center">
<h1 className="text-4xl font-bold mb-2">Welcome to EVYOS</h1>
<p className="text-xl">Enterprise Management System</p>
<p className="text-sm mt-4">Server Time: {currentDate}</p>
</div>
{/* Login Section */}
<div className="p-8">
<div className="bg-white rounded-lg p-6 border border-gray-200">
<h2 className="text-2xl font-semibold text-center mb-6 text-gray-800">
Login to Your Account
</h2>
<div className="space-y-4">
<div>
<label
className="block text-gray-700 text-sm font-medium mb-2"
htmlFor="email"
>
Email Address
</label>
<input
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
id="email"
type="email"
placeholder="Enter your email"
/>
</div>
<div>
<label
className="block text-gray-700 text-sm font-medium mb-2"
htmlFor="password"
>
Password
</label>
<input
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
id="password"
type="password"
placeholder="Enter your password"
/>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<input
id="remember_me"
type="checkbox"
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label
htmlFor="remember_me"
className="ml-2 block text-sm text-gray-700"
>
Remember me
</label>
</div>
<div className="text-sm">
<a
href="#"
className="font-medium text-blue-600 hover:text-blue-500"
>
Forgot your password?
</a>
</div>
</div>
<button className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors">
Sign In
</button>
</div>
</div>
<div className="mt-6 text-center text-sm text-gray-600">
<p>© {new Date().getFullYear()} EVYOS. All rights reserved.</p>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,111 @@
# REACT_TEMPLATE_BLUEPRINT
## Component Structure Reference
```
template/
├── schema.ts # Data validation, field definitions, mock API
├── hooks.ts # Custom hooks for data fetching and state management
├── language.ts # Internationalization support
├── app.tsx # Main orchestration component
├── FormComponent.tsx # Form handling with validation
├── SearchComponent.tsx # Field-specific search functionality
├── DataDisplayComponent.tsx # Card-based data display
├── ListInfoComponent.tsx # Pagination information and controls
├── SortingComponent.tsx # Column sorting functionality
├── PaginationToolsComponent.tsx # Page size and navigation controls
└── ActionButtonsComponent.tsx # Context-aware action buttons
```
## Core Features
- Zod schema validation
- Field-specific search
- Multi-field sorting (none/asc/desc)
- Pagination with configurable page sizes
- Form validation with error messages
- Create/Update/View modes
- Internationalization (en/tr)
- Responsive design with Tailwind CSS
## Implementation Details
### Schema Structure
```typescript
// Base schema with validation
export const DataSchema = z.object({
id: z.string().optional(),
title: z.string(),
description: z.string().optional(),
status: z.string(),
createdAt: z.string().or(z.date()).optional(),
updatedAt: z.string().or(z.date()).optional(),
});
// Field definitions with metadata
const baseFieldDefinitions = {
id: { type: "text", group: "identificationInfo", label: "ID" },
title: { type: "text", group: "basicInfo", label: "Title" },
// Additional fields...
};
```
### Hook Implementation
```typescript
export function usePaginatedData() {
// State management for data, pagination, loading, errors
// API fetching with debouncing
// Data validation with Zod
// Pagination state updates
}
```
### Component Integration
```typescript
function TemplateApp({ lang = "en" }) {
// Data fetching with custom hook
// Mode management (list/create/view/update)
// Component orchestration
}
```
## Build Instructions
1. Create directory structure
2. Implement schema with Zod validation
3. Create custom hooks for data fetching
4. Implement internationalization
5. Build form component with validation
6. Create search component with field selection
7. Implement data display with cards
8. Add pagination and sorting
9. Connect components in main app
10. Style with Tailwind CSS
## Date Display Format
Always use toLocaleString() instead of toLocaleDateString() for date formatting to show both date and time together.
## API Integration
Replace mock API functions with actual API calls:
- GET for list view with pagination/sorting/filtering
- POST for create operations
- PUT/PATCH for update operations
- GET with ID for view operations
## Field Types Support
- text: Standard text input
- textarea: Multi-line text input
- select: Dropdown selection
- date: Date picker
- checkbox: Boolean toggle
- number: Numeric input
## Customization Points
- schema.ts: Data structure and validation
- language.ts: Text translations
- FormComponent.tsx: Field rendering logic
- app.tsx: Component composition

View File

@ -0,0 +1,90 @@
# React Template Component
This directory contains a reusable React template for building data management interfaces with complete CRUD (Create, Read, Update, Delete) functionality.
## Overview
The template provides a ready-to-use solution for displaying, searching, sorting, and editing data with a modern UI. It's designed to be easily customizable for different data types while maintaining a consistent user experience.
## Key Features
- **Data Display**: Card-based UI for showing data items
- **Advanced Search**: Field-specific search capabilities
- **Sorting**: Multi-field sorting with ascending/descending options
- **Pagination**: Configurable page sizes and navigation
- **Form Handling**: Create/Update/View modes with validation
- **Internationalization**: Built-in support for multiple languages
- **Responsive Design**: Works on desktop and mobile devices
## Component Structure
The template is organized into modular components that work together:
### Core Files
- **`schema.ts`**: Defines the data structure using Zod validation
- **`hooks.ts`**: Custom hook for data fetching and pagination
- **`language.ts`**: Internationalization support
- **`app.tsx`**: Main component that orchestrates all other components
### UI Components
- **`FormComponent.tsx`**: Handles data entry and editing
- **`SearchComponent.tsx`**: Provides field-specific search functionality
- **`DataDisplayComponent.tsx`**: Displays data items in a card format
- **`ListInfoComponent.tsx`**: Shows pagination information and controls
- **`SortingComponent.tsx`**: Manages column sorting
- **`PaginationToolsComponent.tsx`**: Controls for page size and navigation
- **`ActionButtonsComponent.tsx`**: Context-aware action buttons
## How to Use
1. **Basic Usage**:
```jsx
import TemplateApp from './template/app';
function MyPage() {
return <TemplateApp lang="en" />;
}
```
2. **Customizing Data Schema**:
- Modify `schema.ts` to match your data structure
- Update field definitions with appropriate types, labels, and grouping
3. **API Integration**:
- Replace the mock `fetchData` function in `schema.ts` with your actual API call
- Implement the create/update API calls in `FormComponent.tsx`
4. **Styling**:
- The template uses Tailwind CSS classes for styling
- Customize the appearance by modifying the CSS classes
## Data Flow
1. User interacts with the UI (search, sort, paginate)
2. Component state updates trigger API calls
3. Data is fetched, validated, and stored in state
4. UI components render based on the current state
5. Form submissions trigger API calls with validated data
## Example Workflow
1. **List View**: Users see paginated data with search and sort options
2. **Create**: Click "Create New" to open a form for adding a new item
3. **View**: Click "View" on an item to see all its details
4. **Update**: Click "Update" on an item to edit its information
## Customization Tips
- **Adding Fields**: Update both the Zod schema and field definitions
- **New Field Types**: Extend the form rendering logic in `FormComponent.tsx`
- **Additional Languages**: Add new language entries to `language.ts`
- **Custom Styling**: Modify the Tailwind classes in each component
## Best Practices
- Keep the schema definition in sync with your backend API
- Use the field grouping feature to organize complex forms
- Leverage the built-in validation to ensure data quality
- Consider adding custom field types for specific data needs

View File

@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

File diff suppressed because it is too large Load Diff

View File

@ -3,22 +3,43 @@
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"dev": "next dev -p 3001",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",
"@radix-ui/react-checkbox": "^1.2.3",
"@radix-ui/react-dialog": "^1.1.11",
"@radix-ui/react-label": "^2.1.4",
"@radix-ui/react-popover": "^1.1.11",
"@radix-ui/react-select": "^2.2.2",
"@radix-ui/react-slot": "^1.2.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"flatpickr": "^4.6.13",
"lucide-react": "^0.503.0",
"next": "15.2.4",
"next-crypto": "^1.0.8",
"next-themes": "^0.4.6",
"react": "^19.0.0",
"react-day-picker": "^8.10.1",
"react-dom": "^19.0.0",
"next": "15.2.4"
"react-hook-form": "^7.56.1",
"sonner": "^2.0.3",
"tailwind-merge": "^3.2.0",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.24.3"
},
"devDependencies": {
"typescript": "^5",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4"
"tailwindcss": "^4",
"tw-animate-css": "^1.2.8",
"typescript": "^5"
}
}

View File

@ -0,0 +1,45 @@
#!/bin/bash
# Exit on error
set -e
echo "🚀 Setting up shadcn/ui components and dependencies..."
# Install required dependencies for shadcn
echo "📦 Installing required dependencies..."
npm install tailwindcss-animate class-variance-authority clsx tailwind-merge --legacy-peer-deps
# Initialize shadcn/ui (components.json already exists, so we'll skip this step)
echo "✅ components.json already exists"
# Set npm config to use legacy-peer-deps for all npm operations
echo "🔧 Setting npm to use legacy-peer-deps..."
npm config set legacy-peer-deps true
# Install base components
echo "🧩 Installing base shadcn/ui components..."
npx shadcn@latest add button --yes
npx shadcn@latest add form --yes
npx shadcn@latest add input --yes
npx shadcn@latest add label --yes
npx shadcn@latest add select --yes
npx shadcn@latest add checkbox --yes
npx shadcn@latest add card --yes
npx shadcn@latest add dialog --yes
npx shadcn@latest add popover --yes
npx shadcn@latest add sonner --yes
npx shadcn@latest add table --yes
npx shadcn@latest add pagination --yes
npx shadcn@latest add calendar --yes
npx shadcn@latest add date-picker --yes
# Update any dependencies with legacy peer deps
echo "🔄 Updating dependencies..."
npm install --legacy-peer-deps
npm install next-crypto@^1.0.8 --legacy-peer-deps
npm install flatpickr@^4.6.13 --legacy-peer-deps
npm install date-fns@^4.1.0 --legacy-peer-deps
npm install react-day-picker@^8.10.1 --legacy-peer-deps
echo "✨ Setup complete! You can now use shadcn/ui components in your project."
echo "📚 Documentation: https://ui.shadcn.com/docs"

View File

@ -0,0 +1,144 @@
"use server";
import { retrieveAccessToken } from "@/apicalls/cookies/token";
const defaultHeaders = {
accept: "application/json",
language: "tr",
domain: "management.com.tr",
tz: "GMT+3",
"Content-type": "application/json",
};
const DefaultResponse = {
error: "Hata tipi belirtilmedi",
status: "500",
data: {},
};
const cacheList = ["no-cache", "no-store", "force-cache", "only-if-cached"];
const prepareResponse = (response: any, statusCode: number) => {
try {
return {
status: statusCode,
data: response || {},
};
} catch (error) {
console.error("Error preparing response:", error);
return {
...DefaultResponse,
error: "Response parsing error",
};
}
};
const fetchData = async (
endpoint: string,
payload: any,
method: string = "POST",
cache: boolean = false
) => {
try {
const headers = {
...defaultHeaders,
};
const fetchOptions: RequestInit = {
method,
headers,
cache: cache ? "force-cache" : "no-cache",
};
if (method === "POST" && payload) {
fetchOptions.body = JSON.stringify(payload);
}
const response = await fetch(endpoint, fetchOptions);
const responseJson = await response.json();
console.log("Fetching:", endpoint, fetchOptions);
console.log("Response:", responseJson);
return prepareResponse(responseJson, response.status);
} catch (error) {
console.error("Fetch error:", error);
return {
...DefaultResponse,
error: error instanceof Error ? error.message : "Network error",
};
}
};
const updateDataWithToken = async (
endpoint: string,
uuid: string,
payload: any,
method: string = "POST",
cache: boolean = false
) => {
const accessToken = (await retrieveAccessToken()) || "";
const headers = {
...defaultHeaders,
"eys-acs-tkn": accessToken,
};
try {
const fetchOptions: RequestInit = {
method,
headers,
cache: cache ? "force-cache" : "no-cache",
};
if (method !== "GET" && payload) {
fetchOptions.body = JSON.stringify(payload.payload);
}
const response = await fetch(`${endpoint}/${uuid}`, fetchOptions);
const responseJson = await response.json();
console.log("Fetching:", `${endpoint}/${uuid}`, fetchOptions);
console.log("Response:", responseJson);
return prepareResponse(responseJson, response.status);
} catch (error) {
console.error("Update error:", error);
return {
...DefaultResponse,
error: error instanceof Error ? error.message : "Network error",
};
}
};
const fetchDataWithToken = async (
endpoint: string,
payload: any,
method: string = "POST",
cache: boolean = false
) => {
const accessToken = (await retrieveAccessToken()) || "";
const headers = {
...defaultHeaders,
"eys-acs-tkn": accessToken,
};
try {
const fetchOptions: RequestInit = {
method,
headers,
cache: cache ? "force-cache" : "no-cache",
};
if (method === "POST" && payload) {
fetchOptions.body = JSON.stringify(payload);
}
const response = await fetch(endpoint, fetchOptions);
const responseJson = await response.json();
console.log("Fetching:", endpoint, fetchOptions);
console.log("Response:", responseJson);
return prepareResponse(responseJson, response.status);
} catch (error) {
console.error("Fetch with token error:", error);
return {
...DefaultResponse,
error: error instanceof Error ? error.message : "Network error",
};
}
};
export { fetchData, fetchDataWithToken, updateDataWithToken };

View File

@ -0,0 +1,75 @@
const formatServiceUrl = (url: string) => {
if (!url) return "";
return url.startsWith("http") ? url : `http://${url}`;
};
export const baseUrlAuth = formatServiceUrl(
process.env.NEXT_PUBLIC_AUTH_SERVICE_URL || "auth_service:8001"
);
export const baseUrlPeople = formatServiceUrl(
process.env.NEXT_PUBLIC_VALIDATION_SERVICE_URL || "identity_service:8002"
);
// export const baseUrlEvent = formatServiceUrl(
// process.env.NEXT_PUBLIC_EVENT_SERVICE_URL || "eventservice:8888"
// );
export const tokenSecret = process.env.TOKENSECRET_90 || "";
export const cookieObject: any = {
httpOnly: true,
path: "/",
sameSite: "none",
secure: true,
maxAge: 3600,
priority: "high",
};
interface FilterListInterface {
page?: number | null | undefined;
size?: number | null | undefined;
orderField?: string | null | undefined;
orderType?: string | null | undefined;
includeJoins?: any[] | null | undefined;
query?: any | null | undefined;
}
class FilterList {
page: number;
size: number;
orderField: string;
orderType: string;
includeJoins: any[];
query: any;
constructor({
page = 1,
size = 5,
orderField = "id",
orderType = "asc",
includeJoins = [],
query = {},
}: FilterListInterface = {}) {
this.page = page ?? 1;
this.size = size ?? 5;
this.orderField = orderField ?? "uu_id";
this.orderType = orderType ?? "asc";
this.orderType = this.orderType.startsWith("a") ? "asc" : "desc";
this.includeJoins = includeJoins ?? [];
this.query = query ?? {};
}
filter() {
return {
page: this.page,
size: this.size,
orderField: this.orderField,
orderType: this.orderType,
includeJoins: this.includeJoins,
query: this.query,
};
}
}
const defaultFilterList = new FilterList({});
export { FilterList, defaultFilterList };
export type { FilterListInterface };

View File

@ -0,0 +1,148 @@
"use server";
import { fetchDataWithToken } from "../api-fetcher";
import { baseUrlAuth, tokenSecret } from "../basics";
import { cookies } from "next/headers";
import NextCrypto from "next-crypto";
const checkToken = `${baseUrlAuth}/authentication/token/check`;
const pageValid = `${baseUrlAuth}/authentication/page/valid`;
const siteUrls = `${baseUrlAuth}/authentication/sites/list`;
const nextCrypto = new NextCrypto(tokenSecret);
async function checkAccessTokenIsValid() {
const response = await fetchDataWithToken(checkToken, {}, "GET", false);
return response?.status === 200 || response?.status === 202 ? true : false;
}
async function retrievePageList() {
const response = await fetchDataWithToken(siteUrls, {}, "GET", false);
return response?.status === 200 || response?.status === 202
? response.data?.sites
: null;
}
async function retrievePagebyUrl(pageUrl: string) {
const response = await fetchDataWithToken(
pageValid,
{
page_url: pageUrl,
},
"POST",
false
);
return response?.status === 200 || response?.status === 202
? response.data?.application
: null;
}
async function retrieveAccessToken() {
const cookieStore = await cookies();
const encrpytAccessToken = cookieStore.get("accessToken")?.value || "";
return encrpytAccessToken
? await nextCrypto.decrypt(encrpytAccessToken)
: null;
}
async function retrieveUserType() {
const cookieStore = await cookies();
const encrpytaccessObject = cookieStore.get("accessObject")?.value || "{}";
const decrpytUserType = JSON.parse(
(await nextCrypto.decrypt(encrpytaccessObject)) || "{}"
);
return decrpytUserType ? decrpytUserType : null;
}
async function retrieveAccessObjects() {
const cookieStore = await cookies();
const encrpytAccessObject = cookieStore.get("accessObject")?.value || "";
const decrpytAccessObject = await nextCrypto.decrypt(encrpytAccessObject);
return decrpytAccessObject ? JSON.parse(decrpytAccessObject) : null;
}
async function retrieveUserSelection() {
const cookieStore = await cookies();
const encrpytUserSelection = cookieStore.get("userSelection")?.value || "";
let objectUserSelection = {};
let decrpytUserSelection: any = await nextCrypto.decrypt(
encrpytUserSelection
);
decrpytUserSelection = decrpytUserSelection
? JSON.parse(decrpytUserSelection)
: null;
console.log("decrpytUserSelection", decrpytUserSelection);
const userSelection = decrpytUserSelection?.selected;
const accessObjects = (await retrieveAccessObjects()) || {};
console.log("accessObjects", accessObjects);
if (decrpytUserSelection?.user_type === "employee") {
const companyList = accessObjects?.selectionList;
const selectedCompany = companyList.find(
(company: any) => company.uu_id === userSelection
);
if (selectedCompany) {
objectUserSelection = { userType: "employee", selected: selectedCompany };
}
} else if (decrpytUserSelection?.user_type === "occupant") {
const buildingsList = accessObjects?.selectionList;
// Iterate through all buildings
if (buildingsList) {
// Loop through each building
for (const buildKey in buildingsList) {
const building = buildingsList[buildKey];
// Check if the building has occupants
if (building.occupants && building.occupants.length > 0) {
// Find the occupant with the matching build_living_space_uu_id
const occupant = building.occupants.find(
(occ: any) => occ.build_living_space_uu_id === userSelection
);
if (occupant) {
objectUserSelection = {
userType: "occupant",
selected: {
...occupant,
buildName: building.build_name,
buildNo: building.build_no,
},
};
break;
}
}
}
}
}
return {
...objectUserSelection,
};
}
// const avatarInfo = await retrieveAvatarInfo();
// lang: avatarInfo?.data?.lang
// ? String(avatarInfo?.data?.lang).toLowerCase()
// : undefined,
// avatar: avatarInfo?.data?.avatar,
// fullName: avatarInfo?.data?.full_name,
// async function retrieveAvatarInfo() {
// const response = await fetchDataWithToken(
// `${baseUrlAuth}/authentication/avatar`,
// {},
// "POST"
// );
// return response;
// }
export {
checkAccessTokenIsValid,
retrieveAccessToken,
retrieveUserType,
retrieveAccessObjects,
retrieveUserSelection,
retrievePagebyUrl,
retrievePageList,
// retrieveavailablePages,
};

View File

@ -0,0 +1,183 @@
"use server";
import NextCrypto from "next-crypto";
import { fetchData, fetchDataWithToken } from "../api-fetcher";
import { baseUrlAuth, cookieObject, tokenSecret } from "../basics";
import { cookies } from "next/headers";
const loginEndpoint = `${baseUrlAuth}/authentication/login`;
const loginSelectEndpoint = `${baseUrlAuth}/authentication/select`;
const logoutEndpoint = `${baseUrlAuth}/authentication/logout`;
console.log("loginEndpoint", loginEndpoint);
console.log("loginSelectEndpoint", loginSelectEndpoint);
interface LoginViaAccessKeys {
accessKey: string;
password: string;
rememberMe: boolean;
}
interface LoginSelectEmployee {
company_uu_id: string;
}
interface LoginSelectOccupant {
build_living_space_uu_id: any;
}
async function logoutActiveSession() {
const cookieStore = await cookies();
const response = await fetchDataWithToken(logoutEndpoint, {}, "GET", false);
cookieStore.delete("accessToken");
cookieStore.delete("accessObject");
cookieStore.delete("userProfile");
cookieStore.delete("userSelection");
return response;
}
async function loginViaAccessKeys(payload: LoginViaAccessKeys) {
try {
const cookieStore = await cookies();
const nextCrypto = new NextCrypto(tokenSecret);
const response = await fetchData(
loginEndpoint,
{
access_key: payload.accessKey,
password: payload.password,
remember_me: payload.rememberMe,
},
"POST",
false
);
console.log("response", response);
if (response.status === 200 || response.status === 202) {
const loginRespone = response?.data;
const accessToken = await nextCrypto.encrypt(loginRespone.access_token);
const accessObject = await nextCrypto.encrypt(
JSON.stringify({
userType: loginRespone.user_type,
selectionList: loginRespone.selection_list,
})
);
const userProfile = await nextCrypto.encrypt(
JSON.stringify(loginRespone.user)
);
cookieStore.set({
name: "accessToken",
value: accessToken,
...cookieObject,
});
cookieStore.set({
name: "accessObject",
value: accessObject,
...cookieObject,
});
cookieStore.set({
name: "userProfile",
value: JSON.stringify(userProfile),
...cookieObject,
});
try {
return {
completed: true,
message: "Login successful",
status: 200,
data: loginRespone,
};
} catch (error) {
console.error("JSON parse error:", error);
return {
completed: false,
message: "Login NOT successful",
status: 401,
data: "{}",
};
}
}
return {
completed: false,
// error: response.error || "Login failed",
// message: response.message || "Authentication failed",
status: response.status || 500,
};
} catch (error) {
console.error("Login error:", error);
return {
completed: false,
// error: error instanceof Error ? error.message : "Login error",
// message: "An error occurred during login",
status: 500,
};
}
}
async function loginSelectEmployee(payload: LoginSelectEmployee) {
const cookieStore = await cookies();
const nextCrypto = new NextCrypto(tokenSecret);
const companyUUID = payload.company_uu_id;
const selectResponse: any = await fetchDataWithToken(
loginSelectEndpoint,
{
company_uu_id: companyUUID,
},
"POST",
false
);
cookieStore.delete("userSelection");
if (selectResponse.status === 200 || selectResponse.status === 202) {
const usersSelection = await nextCrypto.encrypt(
JSON.stringify({
selected: companyUUID,
user_type: "employee",
})
);
cookieStore.set({
name: "userSelection",
value: usersSelection,
...cookieObject,
});
}
return selectResponse;
}
async function loginSelectOccupant(payload: LoginSelectOccupant) {
const livingSpaceUUID = payload.build_living_space_uu_id;
const cookieStore = await cookies();
const nextCrypto = new NextCrypto(tokenSecret);
const selectResponse: any = await fetchDataWithToken(
loginSelectEndpoint,
{
build_living_space_uu_id: livingSpaceUUID,
},
"POST",
false
);
cookieStore.delete("userSelection");
if (selectResponse.status === 200 || selectResponse.status === 202) {
const usersSelection = await nextCrypto.encrypt(
JSON.stringify({
selected: livingSpaceUUID,
user_type: "occupant",
})
);
cookieStore.set({
name: "userSelection",
value: usersSelection,
...cookieObject,
});
}
return selectResponse;
}
export {
loginViaAccessKeys,
loginSelectEmployee,
loginSelectOccupant,
logoutActiveSession,
};

View File

@ -0,0 +1,14 @@
export interface PaginateOnly {
page?: number;
size?: number;
orderField?: string[];
orderType?: string[];
}
export interface PageListOptions {
page?: number;
size?: number;
orderField?: string[];
orderType?: string[];
query?: any;
}

View File

@ -0,0 +1,16 @@
import React from "react";
import Login from "@/components/auth/login";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "WAG Login",
description: "Login to WAG system",
};
export default function LoginPage() {
return (
<>
<Login />
</>
);
}

View File

@ -0,0 +1,42 @@
"use server";
import React from "react";
import {
checkAccessTokenIsValid,
retrieveUserType,
} from "@/apicalls/cookies/token";
import { redirect } from "next/navigation";
import LoginEmployee from "@/components/auth/LoginEmployee";
import LoginOccupant from "@/components/auth/LoginOccupant";
async function SelectPage() {
const token_is_valid = await checkAccessTokenIsValid();
const selection = await retrieveUserType();
console.log("selection", selection);
const isEmployee = selection?.userType == "employee";
const isOccupant = selection?.userType == "occupant";
const selectionList = selection?.selectionList;
if (!selectionList || !token_is_valid) {
redirect("/auth/login");
}
return (
<>
<div className="flex flex-col items-center justify-center h-screen">
<div className="flex flex-col items-center justify-center">
{isEmployee && Array.isArray(selectionList) && (
<LoginEmployee selectionList={selectionList} />
)}
{isOccupant && !Array.isArray(selectionList) && (
<LoginOccupant selectionList={selectionList} />
)}
</div>
</div>
</>
);
}
export default SelectPage;

View File

@ -0,0 +1,29 @@
import type { Metadata } from "next";
import { Suspense } from "react";
export const metadata: Metadata = {
title: "WAG Frontend",
description: "WAG Frontend Application",
};
export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen min-w-screen flex h-screen w-screen overflow-hidden">
<div className="w-1/4">
<div className="flex flex-col items-center justify-center h-screen bg-purple-600">
<div className="text-2xl font-bold">WAG Frontend</div>
<div className="text-sm text-gray-500 mt-4">
Welcome to the WAG Frontend Application
</div>
</div>
</div>
<div className="w-3/4 text-black">
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
</div>
</div>
);
}

View File

@ -0,0 +1,35 @@
import React from "react";
import Header from "@/components/header/Header";
import ClientMenu from "@/components/menu/menu";
import { retrievePageByUrl } from "@/components/Pages/pageRetriever";
async function PageDashboard({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | undefined }>;
}) {
const activePage = "/dashboard";
const searchParamsInstance = await searchParams;
const lang = (searchParamsInstance?.lang as "en" | "tr") || "en";
const PageComponent = retrievePageByUrl(activePage);
return (
<>
<div className="min-h-screen min-w-screen flex h-screen w-screen overflow-hidden">
{/* Sidebar */}
<aside className="w-1/4 border-r p-4 overflow-y-auto">
<ClientMenu lang={lang} activePage={activePage} />
</aside>
{/* Main Content Area */}
<div className="flex flex-col w-3/4">
{/* Header Component */}
<Header lang={lang} />
<PageComponent lang={lang} queryParams={searchParamsInstance} />
</div>
</div>
</>
);
}
export default PageDashboard;

View File

@ -0,0 +1,24 @@
import type { Metadata } from "next";
import { checkAccessTokenIsValid } from "@/apicalls/cookies/token";
import { redirect } from "next/navigation";
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default async function DashLayout({
children,
}: {
children: React.ReactNode;
}) {
const token_is_valid = await checkAccessTokenIsValid();
if (!token_is_valid) {
redirect("/auth/login");
}
return (
<div className="h-screen w-full">
<div className="h-full w-full overflow-y-auto">{children}</div>
</div>
);
}

View File

@ -0,0 +1,9 @@
export const searchPlaceholder = {
tr: "Ara...",
en: "Search...",
};
export const menuLanguage = {
tr: "Menü",
en: "Menu",
};

View File

@ -1,26 +1,121 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

View File

@ -1,103 +1,39 @@
import Image from "next/image";
"use server";
import Link from "next/link";
export default async function Home() {
// Server-side rendering
const currentDate = new Date().toLocaleString("tr-TR", {
timeZone: "Europe/Istanbul",
});
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
<div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-b from-blue-50 to-white p-8">
<div className="w-full max-w-4xl bg-white rounded-xl shadow-lg overflow-hidden">
{/* Welcome Banner */}
<div className="bg-blue-600 text-white p-8 text-center">
<h1 className="text-4xl font-bold mb-2">Welcome to EVYOS</h1>
<p className="text-xl">Enterprise Management System</p>
<p className="text-sm mt-4">Server Time: {currentDate}</p>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
{/* Login Section */}
<div className="p-8">
<div className="bg-white rounded-lg p-6 border border-gray-200">
<Link
href="/auth/login"
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
>
Go to Sign In
</Link>
</div>
<div className="mt-6 text-center text-sm text-gray-600">
<p>© {new Date().getFullYear()} EVYOS. All rights reserved.</p>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,8 @@
import { PageProps } from "@/components/validations/translations/translation";
import React from "react";
const EventAppendPage: React.FC<PageProps> = () => {
return <div>EventAppendPage</div>;
};
export default EventAppendPage;

View File

@ -0,0 +1,8 @@
import React from "react";
import { PageProps } from "@/components/validations/translations/translation";
const AppendersServicePage: React.FC<PageProps> = () => {
return <div>AppendersServicePage</div>;
};
export default AppendersServicePage;

View File

@ -0,0 +1,8 @@
import React from "react";
import { PageProps } from "@/components/validations/translations/translation";
const ApplicationPage: React.FC<PageProps> = () => {
return <div>ApplicationPage</div>;
};
export default ApplicationPage;

View File

@ -0,0 +1,8 @@
import React from "react";
import { PageProps } from "@/components/validations/translations/translation";
const DashboardPage: React.FC<PageProps> = () => {
return <div>DashboardPage</div>;
};
export default DashboardPage;

View File

@ -0,0 +1,8 @@
import React from "react";
import { PageProps } from "@/components/validations/translations/translation";
const EmployeePage: React.FC<PageProps> = () => {
return <div>EmployeePage</div>;
};
export default EmployeePage;

View File

@ -0,0 +1,15 @@
import EventAppendPage from "@/components/Pages/appenderEvent/page";
import AppendersServicePage from "./appendersService/page";
import ApplicationPage from "./application/page";
import EmployeePage from "./employee/page";
import OcuppantPage from "./ocuppant/page";
import DashboardPage from "./dashboard/page";
export const menuPages = {
"/dashboard": DashboardPage,
"/append/event": EventAppendPage,
"/append/service": AppendersServicePage,
"/application": ApplicationPage,
"/employee": EmployeePage,
"/ocuppant": OcuppantPage,
};

View File

@ -0,0 +1,8 @@
import React from "react";
import { PageProps } from "@/components/validations/translations/translation";
const OcuppantPage: React.FC<PageProps> = () => {
return <div>OcuppantPage</div>;
};
export default OcuppantPage;

View File

@ -0,0 +1,10 @@
import { menuPages } from ".";
import { PageProps } from "../validations/translations/translation";
import { UnAuthorizedPage } from "./unauthorizedpage";
export function retrievePageByUrl(url: string): React.FC<PageProps> {
if (url in menuPages) {
return menuPages[url as keyof typeof menuPages];
}
return UnAuthorizedPage;
}

View File

@ -0,0 +1,43 @@
import { PageProps } from "../validations/translations/translation";
// Language dictionary for internationalization
const languageDictionary = {
en: {
title: "Unauthorized Access",
message1: "You do not have permission to access this page.",
message2: "Please contact the administrator.",
footer: `© ${new Date().getFullYear()} My Application`,
},
tr: {
title: "Yetkisiz Erişim",
message1: "Bu sayfaya erişim izniniz yok.",
message2: "Lütfen yönetici ile iletişime geçin.",
footer: `© ${new Date().getFullYear()} Uygulamam`,
},
};
export const UnAuthorizedPage: React.FC<PageProps> = ({
lang = "en",
queryParams,
}) => {
// Use the language dictionary based on the lang prop, defaulting to English
const t =
languageDictionary[lang as keyof typeof languageDictionary] ||
languageDictionary.en;
return (
<>
<div className="flex flex-col h-screen">
<header className="bg-gray-800 text-white p-4 text-center">
<h1 className="text-2xl font-bold">{t.title}</h1>
</header>
<main className="flex-grow p-4 bg-gray-100">
<p className="text-gray-700">{t.message1}</p>
<p className="text-gray-700">{t.message2}</p>
</main>
<footer className="bg-gray-800 text-white p-4 text-center">
<p>{t.footer}</p>
</footer>
</div>
</>
);
};

View File

@ -0,0 +1,146 @@
"use client";
import React from "react";
import { loginSelectEmployee } from "@/apicalls/login/login";
import { useRouter } from "next/navigation";
import { Company } from "./types";
interface LoginEmployeeProps {
selectionList: Company[];
lang?: "en" | "tr";
onSelect?: (uu_id: string) => void;
}
// Language dictionary for internationalization
const languageDictionary = {
tr: {
companySelection: "Şirket Seçimi",
loggedInAs: "Çalışan olarak giriş yaptınız",
duty: "Görev",
id: "Kimlik",
noSelections: "Seçenek bulunamadı",
},
en: {
companySelection: "Select your company",
loggedInAs: "You are logged in as an employee",
duty: "Duty",
id: "ID",
noSelections: "No selections available",
},
};
function LoginEmployee({
selectionList,
lang = "en",
onSelect,
}: LoginEmployeeProps) {
const t = languageDictionary[lang] || languageDictionary.en;
const router = useRouter();
const handleSelect = (uu_id: string) => {
console.log("Selected employee uu_id:", uu_id);
// If an external onSelect handler is provided, use it
if (onSelect) {
onSelect(uu_id);
return;
}
// Otherwise use the internal handler
loginSelectEmployee({ company_uu_id: uu_id })
.then((responseData: any) => {
if (responseData?.status === 200 || responseData?.status === 202) {
router.push("/dashboard");
}
})
.catch((error) => {
console.error(error);
});
};
return (
<>
<div className="text-2xl font-bold">{t.companySelection}</div>
<div className="text-sm text-gray-500 mt-4">{t.loggedInAs}</div>
{Array.isArray(selectionList) && selectionList.length === 0 && (
<div className="text-center p-4">{t.noSelections}</div>
)}
{Array.isArray(selectionList) && selectionList.length === 1 && (
<div className="w-full p-4 m-2 bg-emerald-300 rounded-lg">
<div className="flex flex-col items-center md:items-start">
<div>
<span className="text-2xl font-medium">
{selectionList[0].public_name}
</span>
{selectionList[0].company_type && (
<span className="ml-2 font-medium text-sky-500">
{selectionList[0].company_type}
</span>
)}
</div>
{selectionList[0].duty && (
<div className="mt-1">
<span className="text-md font-medium text-gray-700">
{t.duty}: {selectionList[0].duty}
</span>
</div>
)}
<div className="mt-2">
<span className="flex gap-2 font-medium text-gray-600 dark:text-gray-400 text-sm">
<span>
{t.id}: {selectionList[0].uu_id}
</span>
</span>
</div>
<div className="mt-3">
<button
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors"
onClick={() => handleSelect(selectionList[0].uu_id)}
>
Continue
</button>
</div>
</div>
</div>
)}
{Array.isArray(selectionList) &&
selectionList.length > 1 &&
selectionList.map((item: Company, index: number) => (
<div
key={index}
className="w-full p-4 m-2 bg-emerald-300 hover:bg-emerald-500 rounded-lg transition-colors duration-200 cursor-pointer"
onClick={() => handleSelect(item.uu_id)}
>
<div className="flex flex-col items-center md:items-start">
<div>
<span className="text-2xl font-medium">{item.public_name}</span>
{item.company_type && (
<span className="ml-2 font-medium text-sky-500">
{item.company_type}
</span>
)}
</div>
{item.duty && (
<div className="mt-1">
<span className="text-md font-medium text-gray-700">
{t.duty}: {item.duty}
</span>
</div>
)}
<div className="mt-2">
<span className="flex gap-2 font-medium text-gray-600 dark:text-gray-400 text-sm">
<span>
{t.id}: {item.uu_id}
</span>
</span>
</div>
</div>
</div>
))}
</>
);
}
export default LoginEmployee;

View File

@ -0,0 +1,111 @@
"use client";
import React from "react";
import { loginSelectOccupant } from "@/apicalls/login/login";
import { useRouter } from "next/navigation";
import { BuildingMap } from "./types";
interface LoginOccupantProps {
selectionList: BuildingMap;
lang?: "en" | "tr";
}
// Language dictionary for internationalization
const languageDictionary = {
tr: {
occupantSelection: "Daire Seçimi",
loggedInAs: "Kiracı olarak giriş yaptınız",
buildingInfo: "Bina Bilgisi",
level: "Kat",
noSelections: "Seçenek bulunamadı",
},
en: {
occupantSelection: "Select your occupant type",
loggedInAs: "You are logged in as an occupant",
buildingInfo: "Building Info",
level: "Level",
noSelections: "No selections available",
},
};
function LoginOccupant({
selectionList,
lang = "en"
}: LoginOccupantProps) {
const t = languageDictionary[lang] || languageDictionary.en;
const router = useRouter();
const handleSelect = (uu_id: string) => {
console.log("Selected occupant uu_id:", uu_id);
loginSelectOccupant({
build_living_space_uu_id: uu_id,
})
.then((responseData: any) => {
if (responseData?.status === 200 || responseData?.status === 202) {
router.push("/dashboard");
}
})
.catch((error) => {
console.error(error);
});
};
return (
<>
<div className="text-2xl font-bold">{t.occupantSelection}</div>
<div className="text-sm text-gray-500 mt-4">
{t.loggedInAs}
</div>
{selectionList && Object.keys(selectionList).length > 0 ? (
Object.keys(selectionList).map((buildKey: string) => {
const building = selectionList[buildKey];
return (
<div key={buildKey} className="mb-6">
<div className="w-full p-3 bg-blue-100 rounded-t-lg">
<h3 className="text-lg font-medium text-blue-800">
<span className="mr-1">{t.buildingInfo}:</span>
{building.build_name} - No: {building.build_no}
</h3>
</div>
<div className="space-y-2 mt-2">
{building.occupants.map((occupant: any, idx: number) => (
<div
key={`${buildKey}-${idx}`}
className="w-full p-4 bg-emerald-300 hover:bg-emerald-500 rounded-lg transition-colors duration-200 cursor-pointer"
onClick={() => handleSelect(occupant.build_living_space_uu_id)}
>
<div className="flex flex-col">
<div className="flex items-center justify-between">
<span className="text-xl font-medium">
{occupant.description}
</span>
<span className="text-sm font-medium bg-blue-500 text-white px-2 py-1 rounded">
{occupant.code}
</span>
</div>
<div className="mt-2">
<span className="text-md font-medium">
{occupant.part_name}
</span>
</div>
<div className="mt-1">
<span className="text-sm text-gray-700">
{t.level}: {occupant.part_level}
</span>
</div>
</div>
</div>
))}
</div>
</div>
);
})
) : (
<div className="text-center p-4">{t.noSelections}</div>
)}
</>
);
}
export default LoginOccupant;

View File

@ -0,0 +1,152 @@
"use client";
import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { loginViaAccessKeys } from "@/apicalls/login/login";
import { z } from "zod";
const loginSchema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(5, "Password must be at least 5 characters"),
remember_me: z.boolean().optional().default(false),
});
type LoginFormData = {
email: string;
password: string;
remember_me?: boolean;
};
function Login() {
// Open transition for form login
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const [jsonText, setJsonText] = useState<string | null>(null);
const Router = useRouter();
const {
register,
formState: { errors },
handleSubmit,
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
});
const onSubmit = async (data: LoginFormData) => {
try {
startTransition(() => {
try {
loginViaAccessKeys({
accessKey: data.email,
password: data.password,
rememberMe: false,
})
.then((result: any) => {
const dataResponse = result?.data;
if (dataResponse?.access_token) {
setJsonText(JSON.stringify(dataResponse));
setTimeout(() => {
Router.push("/auth/select");
}, 2000);
}
return dataResponse;
})
.catch(() => {});
} catch (error) {}
});
} catch (error) {
console.error("Login error:", error);
setError("An error occurred during login");
}
};
return (
<>
<div className="flex h-full min-h-[inherit] flex-col items-center justify-center gap-4">
<div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
<h2 className="mb-6 text-center text-2xl font-bold text-gray-900">
Login
</h2>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700"
>
Email
</label>
<input
{...register("email")}
type="email"
id="email"
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">
{errors.email.message}
</p>
)}
</div>
<div>
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700"
>
Password
</label>
<input
{...register("password")}
type="password"
id="password"
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-indigo-500"
/>
{errors.password && (
<p className="mt-1 text-sm text-red-600">
{errors.password.message}
</p>
)}
</div>
{error && (
<div className="rounded-md bg-red-50 p-4">
<p className="text-sm text-red-700">{error}</p>
</div>
)}
<button
type="submit"
disabled={isPending}
className="w-full rounded-md bg-indigo-600 px-4 py-2 text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50"
>
{isPending ? "Logging in..." : "Login"}
</button>
</form>
</div>
{jsonText && (
<div className="w-full max-w-md rounded-lg bg-white p-8 shadow-md">
<h2 className="mb-4 text-center text-xl font-bold text-gray-900">
Response Data
</h2>
<div className="space-y-2">
{Object.entries(JSON.parse(jsonText)).map(([key, value]) => (
<div key={key} className="flex items-start gap-2">
<strong className="text-gray-700">{key}:</strong>
<span className="text-gray-600">
{typeof value === "object"
? JSON.stringify(value)
: value?.toString() || "N/A"}
</span>
</div>
))}
</div>
</div>
)}
</div>
</>
);
}
export default Login;

View File

@ -0,0 +1,87 @@
"use client";
import React from "react";
import {
loginSelectEmployee,
loginSelectOccupant,
} from "@/apicalls/login/login";
import { useRouter } from "next/navigation";
import LoginEmployee from "./LoginEmployee";
import LoginOccupant from "./LoginOccupant";
import { SelectListProps, Company, BuildingMap } from "./types";
function SelectList({
selectionList,
isEmployee,
isOccupant,
lang = "en",
}: SelectListProps) {
const router = useRouter();
// Log the complete selectionList object and its structure
console.log("selectionList (complete):", selectionList);
console.log(
"selectionList (type):",
Array.isArray(selectionList) ? "Array" : "Object"
);
if (isEmployee && Array.isArray(selectionList)) {
console.log("Employee companies:", selectionList);
} else if (isOccupant && !Array.isArray(selectionList)) {
// Log each building and its occupants
Object.entries(selectionList).forEach(([buildingKey, building]) => {
console.log(`Building ${buildingKey}:`, building);
console.log(`Occupants for building ${buildingKey}:`, building.occupants);
});
}
const setSelectionHandler = (uu_id: string) => {
if (isEmployee) {
console.log("Selected isEmployee uu_id:", uu_id);
loginSelectEmployee({ company_uu_id: uu_id })
.then((responseData: any) => {
if (responseData?.status === 200 || responseData?.status === 202) {
router.push("/dashboard");
}
})
.catch((error) => {
console.error(error);
});
} else if (isOccupant) {
console.log("Selected isOccupant uu_id:", uu_id);
// For occupants, the uu_id is a composite of buildKey|partUuid
loginSelectOccupant({
build_living_space_uu_id: uu_id,
})
.then((responseData: any) => {
if (responseData?.status === 200 || responseData?.status === 202) {
router.push("/dashboard");
}
})
.catch((error) => {
console.error(error);
});
}
};
return (
<>
{isEmployee && Array.isArray(selectionList) && (
<LoginEmployee
selectionList={selectionList as Company[]}
lang={lang as "en" | "tr"}
onSelect={setSelectionHandler}
/>
)}
{isOccupant && !Array.isArray(selectionList) && (
<LoginOccupant
selectionList={selectionList as BuildingMap}
lang={lang as "en" | "tr"}
onSelect={setSelectionHandler}
/>
)}
</>
);
}
export default SelectList;

View File

@ -0,0 +1,36 @@
// TypeScript interfaces for proper type checking
export interface Company {
uu_id: string;
public_name: string;
company_type?: string;
company_address?: any;
duty?: string;
}
export interface Occupant {
build_living_space_uu_id: string;
part_uu_id: string;
part_name: string;
part_level: number;
occupant_uu_id: string;
description: string;
code: string;
}
export interface Building {
build_uu_id: string;
build_name: string;
build_no: string;
occupants: Occupant[];
}
export interface BuildingMap {
[key: string]: Building;
}
export interface SelectListProps {
selectionList: Company[] | BuildingMap;
isEmployee: boolean;
isOccupant: boolean;
lang?: "en" | "tr";
}

View File

@ -0,0 +1,407 @@
"use client";
import React, { useState, useRef, useEffect } from "react";
import {
ChevronDown,
LogOut,
Settings,
User,
Bell,
MessageSquare,
X,
} from "lucide-react";
import { searchPlaceholder, menuLanguage } from "@/app/commons/pageDefaults";
import { logoutActiveSession } from "@/apicalls/login/login";
import { useRouter } from "next/navigation";
interface HeaderProps {
lang: "en" | "tr";
}
// Language dictionary for the dropdown menu
const dropdownLanguage = {
en: {
profile: "Profile",
settings: "Settings",
logout: "Logout",
notifications: "Notifications",
messages: "Messages",
viewAll: "View all",
noNotifications: "No notifications",
noMessages: "No messages",
markAllAsRead: "Mark all as read",
},
tr: {
profile: "Profil",
settings: "Ayarlar",
logout: ıkış",
notifications: "Bildirimler",
messages: "Mesajlar",
viewAll: "Tümünü gör",
noNotifications: "Bildirim yok",
noMessages: "Mesaj yok",
markAllAsRead: "Tümünü okundu olarak işaretle",
},
};
// Mock data for notifications
const mockNotifications = [
{
id: 1,
title: "New update available",
description: "System update v2.4.1 is now available",
time: new Date(2025, 3, 19, 14, 30),
read: false,
},
{
id: 2,
title: "Meeting reminder",
description: "Team meeting in 30 minutes",
time: new Date(2025, 3, 19, 13, 45),
read: false,
},
{
id: 3,
title: "Task completed",
description: "Project X has been completed successfully",
time: new Date(2025, 3, 18, 16, 20),
read: true,
},
];
// Mock data for messages
const mockMessages = [
{
id: 1,
sender: "John Doe",
message: "Hi there! Can we discuss the project details?",
time: new Date(2025, 3, 19, 15, 10),
read: false,
avatar: "JD",
},
{
id: 2,
sender: "Jane Smith",
message: "Please review the latest documents I sent",
time: new Date(2025, 3, 19, 12, 5),
read: false,
avatar: "JS",
},
{
id: 3,
sender: "Mike Johnson",
message: "Thanks for your help yesterday!",
time: new Date(2025, 3, 18, 9, 45),
read: true,
avatar: "MJ",
},
];
const Header: React.FC<HeaderProps> = ({ lang }) => {
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isNotificationsOpen, setIsNotificationsOpen] = useState(false);
const [isMessagesOpen, setIsMessagesOpen] = useState(false);
const [notifications, setNotifications] = useState(mockNotifications);
const [messages, setMessages] = useState(mockMessages);
const dropdownRef = useRef<HTMLDivElement>(null);
const notificationsRef = useRef<HTMLDivElement>(null);
const messagesRef = useRef<HTMLDivElement>(null);
const t = dropdownLanguage[lang] || dropdownLanguage.en;
const router = useRouter();
// Close dropdowns when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsDropdownOpen(false);
}
if (
notificationsRef.current &&
!notificationsRef.current.contains(event.target as Node)
) {
setIsNotificationsOpen(false);
}
if (
messagesRef.current &&
!messagesRef.current.contains(event.target as Node)
) {
setIsMessagesOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
const handleLogout = () => {
// Implement logout functionality
console.log("Logging out...");
logoutActiveSession()
.then(() => {
console.log("Logout successful");
})
.catch((error) => {
console.error("Logout error:", error);
})
.finally(() => {
setIsDropdownOpen(false);
router.replace("/auth/login");
});
};
const markAllNotificationsAsRead = () => {
setNotifications(
notifications.map((notification) => ({
...notification,
read: true,
}))
);
};
const markAllMessagesAsRead = () => {
setMessages(
messages.map((message) => ({
...message,
read: true,
}))
);
};
// Format date to display in a user-friendly way
const formatDate = (date: Date) => {
return date.toLocaleString(lang === "tr" ? "tr-TR" : "en-US", {
hour: "2-digit",
minute: "2-digit",
day: "2-digit",
month: "2-digit",
year: "numeric",
});
};
return (
<header className="sticky top-0 bg-white shadow-md z-10 p-4 flex justify-between items-center">
<h1 className="text-2xl font-semibold">{menuLanguage[lang]}</h1>
<div className="flex items-center space-x-4">
<input
type="text"
placeholder={searchPlaceholder[lang]}
className="border px-3 py-2 rounded-lg"
/>
{/* Notifications dropdown */}
<div className="relative" ref={notificationsRef}>
<div
className="flex items-center cursor-pointer relative"
onClick={() => {
setIsNotificationsOpen(!isNotificationsOpen);
setIsMessagesOpen(false);
setIsDropdownOpen(false);
}}
>
<Bell size={20} className="text-gray-600" />
{notifications.some((n) => !n.read) && (
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
{notifications.filter((n) => !n.read).length}
</span>
)}
</div>
{/* Notifications dropdown menu */}
{isNotificationsOpen && (
<div className="absolute right-0 mt-2 w-80 bg-white rounded-md shadow-lg py-1 z-20 border">
<div className="px-4 py-2 border-b flex justify-between items-center">
<h3 className="font-semibold">{t.notifications}</h3>
{notifications.some((n) => !n.read) && (
<button
onClick={markAllNotificationsAsRead}
className="text-xs text-blue-600 hover:text-blue-800"
>
{t.markAllAsRead}
</button>
)}
</div>
<div className="max-h-80 overflow-y-auto">
{notifications.length === 0 ? (
<div className="px-4 py-2 text-sm text-gray-500">
{t.noNotifications}
</div>
) : (
notifications.map((notification) => (
<div
key={notification.id}
className={`px-4 py-2 border-b last:border-b-0 ${
!notification.read ? "bg-blue-50" : ""
}`}
>
<div className="flex justify-between">
<h4 className="text-sm font-semibold">
{notification.title}
</h4>
{!notification.read && (
<button className="text-gray-400 hover:text-gray-600">
<X size={14} />
</button>
)}
</div>
<p className="text-xs text-gray-600 mt-1">
{notification.description}
</p>
<p className="text-xs text-gray-400 mt-1">
{formatDate(notification.time)}
</p>
</div>
))
)}
</div>
<div className="px-4 py-2 border-t text-center">
<a
href="/notifications"
className="text-sm text-blue-600 hover:text-blue-800"
>
{t.viewAll}
</a>
</div>
</div>
)}
</div>
{/* Messages dropdown */}
<div className="relative" ref={messagesRef}>
<div
className="flex items-center cursor-pointer relative"
onClick={() => {
setIsMessagesOpen(!isMessagesOpen);
setIsNotificationsOpen(false);
setIsDropdownOpen(false);
}}
>
<MessageSquare size={20} className="text-gray-600" />
{messages.some((m) => !m.read) && (
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
{messages.filter((m) => !m.read).length}
</span>
)}
</div>
{/* Messages dropdown menu */}
{isMessagesOpen && (
<div className="absolute right-0 mt-2 w-80 bg-white rounded-md shadow-lg py-1 z-20 border">
<div className="px-4 py-2 border-b flex justify-between items-center">
<h3 className="font-semibold">{t.messages}</h3>
{messages.some((m) => !m.read) && (
<button
onClick={markAllMessagesAsRead}
className="text-xs text-blue-600 hover:text-blue-800"
>
{t.markAllAsRead}
</button>
)}
</div>
<div className="max-h-80 overflow-y-auto">
{messages.length === 0 ? (
<div className="px-4 py-2 text-sm text-gray-500">
{t.noMessages}
</div>
) : (
messages.map((message) => (
<div
key={message.id}
className={`px-4 py-2 border-b last:border-b-0 ${
!message.read ? "bg-blue-50" : ""
}`}
>
<div className="flex items-start">
<div className="w-8 h-8 bg-gray-300 rounded-full flex items-center justify-center mr-2 flex-shrink-0">
<span className="text-xs font-semibold">
{message.avatar}
</span>
</div>
<div className="flex-1">
<div className="flex justify-between">
<h4 className="text-sm font-semibold">
{message.sender}
</h4>
<p className="text-xs text-gray-400">
{formatDate(message.time)}
</p>
</div>
<p className="text-xs text-gray-600 mt-1">
{message.message}
</p>
</div>
</div>
</div>
))
)}
</div>
<div className="px-4 py-2 border-t text-center">
<a
href="/messages"
className="text-sm text-blue-600 hover:text-blue-800"
>
{t.viewAll}
</a>
</div>
</div>
)}
</div>
{/* Profile dropdown */}
<div className="relative" ref={dropdownRef}>
<div
className="flex items-center cursor-pointer"
onClick={() => {
setIsDropdownOpen(!isDropdownOpen);
setIsNotificationsOpen(false);
setIsMessagesOpen(false);
}}
>
<div className="w-10 h-10 bg-gray-300 rounded-full flex items-center justify-center">
<User size={20} className="text-gray-600" />
</div>
<ChevronDown size={16} className="ml-1 text-gray-600" />
</div>
{/* Dropdown menu */}
{isDropdownOpen && (
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-20 border">
<a
href="/profile"
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
<User size={16} className="mr-2" />
{t.profile}
</a>
<a
href="/settings"
className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
<Settings size={16} className="mr-2" />
{t.settings}
</a>
<button
onClick={handleLogout}
className="flex items-center w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
<LogOut size={16} className="mr-2" />
{t.logout}
</button>
</div>
)}
</div>
</div>
</header>
);
};
export default Header;

View File

@ -0,0 +1,223 @@
"use client";
import React, { useState, useEffect } from "react";
import { User, Briefcase, ChevronDown } from "lucide-react";
import {
retrieveAccessObjects,
retrieveUserSelection,
} from "@/apicalls/cookies/token";
import { loginSelectEmployee } from "@/apicalls/login/login";
// Language definitions for employee profile section
const profileLanguage = {
tr: {
userType: "Kullanıcı Tipi",
employee: "Çalışan",
loading: "Yükleniyor...",
changeSelection: "Seçimi Değiştir",
selectCompany: "Şirket Seçin",
noCompanies: "Kullanılabilir şirket bulunamadı",
duty: "Görev",
},
en: {
userType: "User Type",
employee: "Employee",
loading: "Loading...",
changeSelection: "Change Selection",
selectCompany: "Select Company",
noCompanies: "No companies available",
duty: "Duty",
},
};
interface CompanyInfo {
uu_id: string;
public_name: string;
company_type: string;
company_address: string | null;
duty: string;
}
interface UserSelection {
userType: string;
selected: CompanyInfo;
}
interface EmployeeProfileSectionProps {
userSelectionData: UserSelection;
lang?: "en" | "tr";
}
const EmployeeProfileSection: React.FC<EmployeeProfileSectionProps> = ({
userSelectionData,
lang = "en",
}) => {
const t =
profileLanguage[lang as keyof typeof profileLanguage] || profileLanguage.en;
const [showSelectionList, setShowSelectionList] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
// Initialize state with data from props
const [userSelection, setUserSelection] = useState<CompanyInfo | null>(
userSelectionData?.selected || null
);
const [selectionList, setSelectionList] = useState<CompanyInfo[] | null>(
null
);
const [availableCompanies, setAvailableCompanies] = useState<CompanyInfo[]>(
[]
);
const [hasMultipleOptions, setHasMultipleOptions] = useState<boolean>(false);
// Fetch access objects for selection list when needed
useEffect(() => {
if (showSelectionList && !selectionList) {
setLoading(true);
retrieveAccessObjects()
.then((accessObjectsData) => {
console.log("Access Objects:", accessObjectsData);
if (accessObjectsData && "selectionList" in accessObjectsData) {
const companies = (accessObjectsData as any)
.selectionList as CompanyInfo[];
setSelectionList(companies);
// Filter out the currently selected company
if (userSelection) {
const filteredCompanies = companies.filter(
(company) => company.uu_id !== userSelection.uu_id
);
setAvailableCompanies(filteredCompanies);
setHasMultipleOptions(filteredCompanies.length > 0);
} else {
setAvailableCompanies(companies);
setHasMultipleOptions(companies.length > 1);
}
}
})
.catch((err) => {
console.error("Error fetching access objects:", err);
})
.finally(() => setLoading(false));
}
}, [showSelectionList, selectionList, userSelection]);
// Update user selection when props change
useEffect(() => {
if (userSelectionData?.selected) {
setUserSelection(userSelectionData.selected as CompanyInfo);
// Check if we need to fetch selection list to determine if multiple options exist
if (!selectionList) {
retrieveAccessObjects()
.then((accessObjectsData) => {
if (accessObjectsData && "selectionList" in accessObjectsData) {
const companies = (accessObjectsData as any)
.selectionList as CompanyInfo[];
setSelectionList(companies);
// Filter out the currently selected company
const filteredCompanies = companies.filter(
(company) => company.uu_id !== userSelectionData.selected.uu_id
);
setHasMultipleOptions(filteredCompanies.length > 0);
}
})
.catch((err) => {
console.error("Error fetching access objects:", err);
});
}
}
}, [userSelectionData, selectionList]);
const handleSelectCompany = (company: any) => {
loginSelectEmployee({ company_uu_id: company.uu_id })
.then((responseData: any) => {
if (responseData?.status === 200 || responseData?.status === 202) {
// Refresh the page to update the selection
window.location.reload();
}
})
.catch((error) => {
console.error("Error selecting company:", error);
});
};
if (!userSelection) {
return <div className="text-center text-gray-500">{t.loading}</div>;
}
return (
<div className="flex flex-col space-y-3">
{/* <div className="flex items-center space-x-3">
<div className="bg-blue-100 p-2 rounded-full">
<User className="h-6 w-6 text-blue-600" />
</div>
<div>
<h3 className="font-medium text-gray-900">{t.userType}</h3>
<p className="text-sm text-gray-600">{t.employee}</p>
</div>
</div> */}
<div
className={`flex items-center justify-between p-2 ${
hasMultipleOptions ? "hover:bg-gray-100 cursor-pointer" : ""
} rounded-lg transition-colors`}
onClick={() =>
hasMultipleOptions && setShowSelectionList(!showSelectionList)
}
>
<div className="flex items-center space-x-3">
<div className="bg-green-100 p-2 rounded-full">
<Briefcase className="h-5 w-5 text-green-600" />
</div>
<div>
<h3 className="font-medium text-gray-900">
{userSelection.public_name} {userSelection.company_type}
</h3>
<p className="text-sm text-gray-500">{userSelection.duty}</p>
</div>
</div>
</div>
{/* Selection dropdown */}
{showSelectionList && hasMultipleOptions && (
<div className="mt-2 border rounded-lg overflow-hidden shadow-md">
<div className="bg-gray-50 p-2 border-b">
<h4 className="font-medium text-gray-700">{t.selectCompany}</h4>
</div>
<div className="max-h-60 overflow-y-auto">
{loading ? (
<div className="p-4 text-center text-gray-500">{t.loading}</div>
) : availableCompanies.length > 0 ? (
availableCompanies.map((company, index) => (
<div
key={index}
className="p-3 hover:bg-gray-50 cursor-pointer border-b last:border-b-0"
onClick={() => handleSelectCompany(company)}
>
<div className="font-medium">{company.public_name}</div>
{company.company_type && (
<div className="text-sm text-blue-600">
{company.company_type}
</div>
)}
{company.duty && (
<div className="text-xs text-gray-500 mt-1">
{t.duty}: {company.duty}
</div>
)}
</div>
))
) : (
<div className="p-4 text-center text-gray-500">
{t.noCompanies}
</div>
)}
</div>
</div>
)}
</div>
);
};
export default EmployeeProfileSection;

View File

@ -0,0 +1,55 @@
"use client";
import React from "react";
import Link from "next/link";
const NavigationLanguage = {
en: {
"/dashboard": "Dashboard",
"/append/event": "Event Append",
"/append/service": "Service Append",
"/application": "Application",
"/employee": "Employee",
"/ocuppant": "Ocuppant",
},
tr: {
"/dashboard": "Kontrol Paneli",
"/append/event": "Event Append",
"/append/service": "Service Append",
"/application": "Application",
"/employee": "Employee",
"/ocuppant": "Ocuppant",
},
};
function NavigationMenu({
lang,
activePage,
}: {
lang: string;
activePage: string;
}) {
// Get the navigation items based on the selected language
const navItems =
NavigationLanguage[lang as keyof typeof NavigationLanguage] ||
NavigationLanguage.en;
return (
<nav className="flex flex-col space-y-2 p-4">
{Object.entries(navItems).map(([url, title]) => (
<Link
key={url}
href={url}
className={`px-4 py-2 rounded-md transition-colors duration-200 ${
url === activePage
? "bg-emerald-500 text-white"
: "bg-white hover:bg-gray-100"
}`}
>
{title}
</Link>
))}
</nav>
);
}
export default NavigationMenu;

View File

@ -0,0 +1,455 @@
"use client";
import React, { useState, useEffect } from "react";
import { User, Building, Home, ChevronDown } from "lucide-react";
import { retrieveAccessObjects } from "@/apicalls/cookies/token";
import { loginSelectOccupant } from "@/apicalls/login/login";
import { useRouter } from "next/navigation";
// Language definitions for occupant profile section
const profileLanguage = {
tr: {
userType: "Kullanıcı Tipi",
occupant: "Sakin",
building: "Bina",
apartment: "Daire",
loading: "Yükleniyor...",
changeSelection: "Seçimi Değiştir",
selectOccupant: "Daire Seçin",
noOccupants: "Kullanılabilir daire bulunamadı",
},
en: {
userType: "User Type",
occupant: "Occupant",
building: "Building",
apartment: "Apartment",
loading: "Loading...",
changeSelection: "Change Selection",
selectOccupant: "Select Apartment",
noOccupants: "No apartments available",
},
};
// {
// "userType": "occupant",
// "selectionList": {
// "3fe72194-dad6-4ddc-8679-70acdbe7f619": {
// "build_uu_id": "3fe72194-dad6-4ddc-8679-70acdbe7f619",
// "build_name": "Build Example",
// "build_no": "B001",
// "occupants": [
// {
// "build_living_space_uu_id": "b67e5a37-ac04-45ab-8bca-5a3427358015",
// "part_uu_id": "441ef61b-1cc5-465b-90b2-4835d0e16540",
// "part_name": "APARTMAN DAIRESI : 1",
// "part_level": 1,
// "occupant_uu_id": "6bde6bf9-0d13-4b6f-a612-28878cd7324f",
// "description": "Daire Kiracısı",
// "code": "FL-TEN"
// }
// ]
// }
// }
// }
// Define interfaces for occupant data structures based on the access object structure
interface OccupantDetails {
build_living_space_uu_id: string;
part_uu_id: string;
part_name: string;
part_level: number;
occupant_uu_id: string;
description: string;
code: string;
[key: string]: any; // Allow other properties
}
interface BuildingInfo {
build_uu_id: string;
build_name: string;
build_no: string;
occupants: OccupantDetails[];
[key: string]: any; // Allow other properties
}
interface OccupantSelectionList {
userType: string;
selectionList: {
[key: string]: BuildingInfo;
};
}
// Interface for the selected occupant data
interface OccupantInfo {
buildName?: string;
buildNo?: string;
occupantName?: string;
description?: string;
code?: string;
part_name?: string;
build_living_space_uu_id?: string;
[key: string]: any; // Allow other properties
}
interface UserSelection {
userType: string;
selected: OccupantInfo;
}
interface OccupantProfileSectionProps {
userSelectionData: UserSelection;
lang?: "en" | "tr";
}
const OccupantProfileSection: React.FC<OccupantProfileSectionProps> = ({
userSelectionData,
lang = "en",
}) => {
const t =
profileLanguage[lang as keyof typeof profileLanguage] || profileLanguage.en;
const router = useRouter();
const [showSelectionList, setShowSelectionList] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
// Initialize state with data from props
const [userSelection, setUserSelection] = useState<OccupantInfo | null>(
userSelectionData?.selected || null
);
const [selectionList, setSelectionList] =
useState<OccupantSelectionList | null>(null);
const [availableOccupants, setAvailableOccupants] = useState<any[]>([]);
const [hasMultipleOptions, setHasMultipleOptions] = useState<boolean>(false);
const [selectedBuildingKey, setSelectedBuildingKey] = useState<string | null>(
null
);
const [buildings, setBuildings] = useState<{ [key: string]: BuildingInfo }>(
{}
);
// Fetch access objects for selection list when needed
useEffect(() => {
if (showSelectionList && !selectionList) {
setLoading(true);
retrieveAccessObjects()
.then((accessObjectsData) => {
console.log("Access Objects:", accessObjectsData);
if (accessObjectsData && accessObjectsData.selectionList) {
const data = accessObjectsData as OccupantSelectionList;
setSelectionList(data);
setBuildings(data.selectionList);
// Check if there are multiple buildings or multiple occupants across all buildings
const buildingKeys = Object.keys(data.selectionList);
let totalOccupants = 0;
let currentBuildingKey = null;
let currentOccupantId = null;
// Count total occupants and find current building/occupant
buildingKeys.forEach((key) => {
const building = data.selectionList[key];
if (building.occupants && building.occupants.length > 0) {
totalOccupants += building.occupants.length;
// Try to find the current user's building and occupant
if (userSelection) {
building.occupants.forEach((occupant) => {
if (
occupant.build_living_space_uu_id ===
userSelection.build_living_space_uu_id
) {
currentBuildingKey = key;
currentOccupantId = occupant.build_living_space_uu_id;
}
});
}
}
});
// Set whether there are multiple options
setHasMultipleOptions(totalOccupants > 1);
// If we found the current building, set it as selected
if (currentBuildingKey) {
setSelectedBuildingKey(currentBuildingKey);
}
}
})
.catch((err) => {
console.error("Error fetching access objects:", err);
})
.finally(() => setLoading(false));
}
}, [showSelectionList, userSelection]);
// Update user selection when props change
useEffect(() => {
if (userSelectionData?.selected) {
setUserSelection(userSelectionData.selected as OccupantInfo);
// Check if we need to fetch selection list to determine if multiple options exist
if (!selectionList) {
retrieveAccessObjects()
.then((accessObjectsData) => {
if (accessObjectsData && accessObjectsData.selectionList) {
const data = accessObjectsData as OccupantSelectionList;
setSelectionList(data);
setBuildings(data.selectionList);
// Count total occupants across all buildings
let totalOccupants = 0;
let currentBuildingKey = null;
Object.keys(data.selectionList).forEach((key) => {
const building = data.selectionList[key];
if (building.occupants && building.occupants.length > 0) {
totalOccupants += building.occupants.length;
// Try to find the current user's building
building.occupants.forEach((occupant) => {
if (
userSelectionData.selected.build_living_space_uu_id ===
occupant.build_living_space_uu_id
) {
currentBuildingKey = key;
}
});
}
});
setHasMultipleOptions(totalOccupants > 1);
if (currentBuildingKey) {
setSelectedBuildingKey(currentBuildingKey);
}
}
})
.catch((err) => {
console.error("Error fetching access objects:", err);
});
}
}
}, [userSelectionData, selectionList]);
// Helper function to process occupant data
const processOccupantData = (data: OccupantSelectionList) => {
if (!data.selectionList) return;
const occupantList: any[] = [];
// Process the building/occupant structure
Object.keys(data.selectionList).forEach((buildKey) => {
const building = data.selectionList[buildKey];
if (building.occupants && building.occupants.length > 0) {
building.occupants.forEach((occupant: OccupantDetails) => {
occupantList.push({
buildKey,
buildName: building.build_name,
buildNo: building.build_no,
...occupant,
});
});
}
});
setAvailableOccupants(occupantList);
};
// Process occupant data when selection menu is opened or when selectionList changes
useEffect(() => {
if (showSelectionList && selectionList && selectionList.selectionList) {
setLoading(true);
processOccupantData(selectionList);
setLoading(false);
}
}, [showSelectionList, selectionList]);
const handleSelectOccupant = (occupant: any) => {
loginSelectOccupant({
build_living_space_uu_id: occupant.build_living_space_uu_id,
})
.then((responseData: any) => {
if (responseData?.status === 200 || responseData?.status === 202) {
// Refresh the page to update the selection
window.location.reload();
}
})
.catch((error) => {
console.error("Error selecting occupant:", error);
});
};
if (!userSelection) {
return <div className="text-center text-gray-500">{t.loading}</div>;
}
return (
<div className="flex flex-col space-y-3">
{/* <div className="flex items-center space-x-3">
<div className="bg-blue-100 p-2 rounded-full">
<User className="h-6 w-6 text-blue-600" />
</div>
<div>
<h3 className="font-medium text-gray-900">{t.userType}</h3>
<p className="text-sm text-gray-600">{t.occupant}</p>
</div>
</div> */}
{userSelection?.buildName && (
<div className="flex items-center space-x-3">
<div className="bg-amber-100 p-2 rounded-full">
<Building className="h-5 w-5 text-amber-600" />
</div>
<div>
<h3 className="font-medium text-gray-900">{t.building}</h3>
<p className="text-sm text-gray-600">{userSelection.buildName}</p>
</div>
</div>
)}
{userSelection?.part_name && (
<div
className={`flex items-center justify-between space-x-3 p-2 ${
hasMultipleOptions ? "hover:bg-gray-100 cursor-pointer" : ""
} rounded-lg transition-colors`}
onClick={() =>
hasMultipleOptions && setShowSelectionList(!showSelectionList)
}
>
<div className="flex items-center space-x-3">
<div className="bg-purple-100 p-2 rounded-full">
<Home className="h-5 w-5 text-purple-600" />
</div>
<div>
<h3 className="font-medium text-gray-900">{t.apartment}</h3>
<p className="text-sm text-gray-600">{userSelection.part_name}</p>
{userSelection.description && (
<p className="text-xs text-gray-500">
{userSelection.description}
</p>
)}
</div>
</div>
{hasMultipleOptions && (
<div className="text-xs text-blue-600 flex items-center">
{t.changeSelection} <ChevronDown className="h-3 w-3 ml-1" />
</div>
)}
</div>
)}
{/* Selection dropdown - First layer: Buildings */}
{showSelectionList && hasMultipleOptions && (
<div className="mt-2 border rounded-lg overflow-hidden shadow-md">
<div className="bg-gray-50 p-2 border-b">
<h4 className="font-medium text-gray-700">{t.selectOccupant}</h4>
</div>
<div className="max-h-60 overflow-y-auto">
{loading ? (
<div className="p-4 text-center text-gray-500">{t.loading}</div>
) : buildings && Object.keys(buildings).length > 0 ? (
selectedBuildingKey ? (
// Second layer: Occupants in the selected building
<div>
<div className="bg-gray-100 p-2 border-b flex justify-between items-center">
<h5 className="font-medium text-gray-700">
{buildings[selectedBuildingKey].build_name}
</h5>
<button
className="text-xs text-blue-600"
onClick={() => setSelectedBuildingKey(null)}
>
Back to buildings
</button>
</div>
{buildings[selectedBuildingKey].occupants.length > 0 ? (
buildings[selectedBuildingKey].occupants.map(
(occupant, index) => {
// Skip the currently selected occupant
if (
userSelection &&
occupant.build_living_space_uu_id ===
userSelection.build_living_space_uu_id
) {
return null;
}
return (
<div
key={index}
className="p-3 hover:bg-gray-50 cursor-pointer border-b last:border-b-0"
onClick={() => handleSelectOccupant(occupant)}
>
<div className="font-medium">
{occupant.description || "Apartment"}
</div>
<div className="text-sm text-gray-600">
{occupant.part_name}
</div>
{occupant.code && (
<div className="text-xs text-gray-500 mt-1">
{t.apartment} {occupant.code}
</div>
)}
</div>
);
}
)
) : (
<div className="p-4 text-center text-gray-500">
{t.noOccupants}
</div>
)}
</div>
) : (
// First layer: Buildings list
Object.keys(buildings).map((buildingKey, index) => {
const building = buildings[buildingKey];
// Skip buildings with no occupants or only the current occupant
if (!building.occupants || building.occupants.length === 0) {
return null;
}
// Check if this building has any occupants other than the current one
if (userSelection) {
const hasOtherOccupants = building.occupants.some(
(occupant) =>
occupant.build_living_space_uu_id !==
userSelection.build_living_space_uu_id
);
if (!hasOtherOccupants) {
return null;
}
}
return (
<div
key={index}
className="p-3 hover:bg-gray-50 cursor-pointer border-b last:border-b-0"
onClick={() => setSelectedBuildingKey(buildingKey)}
>
<div className="font-medium">{building.build_name}</div>
<div className="text-sm text-gray-600">
No: {building.build_no}
</div>
<div className="text-xs text-gray-500 mt-1">
{building.occupants.length}{" "}
{building.occupants.length === 1
? t.apartment.toLowerCase()
: t.apartment.toLowerCase() + "s"}
</div>
</div>
);
})
)
) : (
<div className="p-4 text-center text-gray-500">
{t.noOccupants}
</div>
)}
</div>
</div>
)}
</div>
);
};
export default OccupantProfileSection;

View File

@ -0,0 +1,22 @@
"use client";
import React from "react";
interface ProfileLoadingStateProps {
loadingText: string;
}
const ProfileLoadingState: React.FC<ProfileLoadingStateProps> = ({ loadingText }) => {
return (
<div className="flex items-center justify-center py-2">
<div className="animate-pulse flex space-x-4 w-full">
<div className="rounded-full bg-gray-300 h-12 w-12"></div>
<div className="flex-1 space-y-2 py-1">
<div className="h-4 bg-gray-300 rounded w-3/4"></div>
<div className="h-4 bg-gray-300 rounded w-1/2"></div>
</div>
</div>
</div>
);
};
export default ProfileLoadingState;

View File

@ -0,0 +1,109 @@
"use client";
import Menu from "./store";
// Define TypeScript interfaces for menu structure
export interface LanguageTranslation {
tr: string;
en: string;
}
export interface MenuThirdLevel {
name: string;
lg: LanguageTranslation;
siteUrl: string;
}
export interface MenuSecondLevel {
name: string;
lg: LanguageTranslation;
subList: MenuThirdLevel[];
}
export interface MenuFirstLevel {
name: string;
lg: LanguageTranslation;
subList: MenuSecondLevel[];
}
// Define interfaces for the filtered menu structure
export interface FilteredMenuThirdLevel {
name: string;
lg: LanguageTranslation;
siteUrl: string;
}
export interface FilteredMenuSecondLevel {
name: string;
lg: LanguageTranslation;
subList: FilteredMenuThirdLevel[];
}
export interface FilteredMenuFirstLevel {
name: string;
lg: LanguageTranslation;
subList: FilteredMenuSecondLevel[];
}
/**
* Filters the menu structure based on intersections with provided URLs
* @param {string[]} siteUrls - Array of site URLs to check for intersection
* @returns {Array} - Filtered menu structure with only matching items
*/
export function transformMenu(siteUrls: string[]) {
// Process the menu structure
const filteredMenu: FilteredMenuFirstLevel[] = Menu.reduce(
(acc: FilteredMenuFirstLevel[], firstLevel: MenuFirstLevel) => {
// Create a new first level item with empty subList
const newFirstLevel: FilteredMenuFirstLevel = {
name: firstLevel.name,
lg: { ...firstLevel.lg },
subList: [],
};
// Process second level items
firstLevel.subList.forEach((secondLevel: MenuSecondLevel) => {
// Create a new second level item with empty subList
const newSecondLevel: FilteredMenuSecondLevel = {
name: secondLevel.name,
lg: { ...secondLevel.lg },
subList: [],
};
// Process third level items
secondLevel.subList.forEach((thirdLevel: MenuThirdLevel) => {
// Check if the third level's siteUrl matches exactly
if (
thirdLevel.siteUrl &&
siteUrls.some((url) => url === thirdLevel.siteUrl)
) {
// Create a modified third level item
const newThirdLevel: FilteredMenuThirdLevel = {
name: thirdLevel.name,
lg: { ...thirdLevel.lg },
siteUrl: thirdLevel.siteUrl,
};
// Add the modified third level to the second level's subList
newSecondLevel.subList.push(newThirdLevel);
}
});
// Only add the second level to the first level if it has any matching third level items
if (newSecondLevel.subList.length > 0) {
newFirstLevel.subList.push(newSecondLevel);
}
});
// Only add the first level to the result if it has any matching second level items
if (newFirstLevel.subList.length > 0) {
acc.push(newFirstLevel);
}
return acc;
},
[]
);
return filteredMenu;
}

View File

@ -0,0 +1,93 @@
"use client";
import React, { useEffect, useState, Suspense } from "react";
import { retrieveUserSelection } from "@/apicalls/cookies/token";
import EmployeeProfileSection from "./EmployeeProfileSection";
import OccupantProfileSection from "./OccupantProfileSection";
import ProfileLoadingState from "./ProfileLoadingState";
import {
ClientMenuProps,
UserSelection,
} from "@/components/validations/menu/menu";
import NavigationMenu from "./NavigationMenu";
// Language definitions for dashboard title
const dashboardLanguage = {
tr: {
dashboard: "Kontrol Paneli",
loading: "Yükleniyor...",
},
en: {
dashboard: "Control Panel",
loading: "Loading...",
},
};
const ClientMenu: React.FC<ClientMenuProps> = ({ lang = "en", activePage }) => {
const t =
dashboardLanguage[lang as keyof typeof dashboardLanguage] ||
dashboardLanguage.en;
// State for loading indicator, user type, and user selection data
const [loading, setLoading] = useState<boolean>(true);
const [userType, setUserType] = useState<string | null>(null);
const [userSelectionData, setUserSelectionData] =
useState<UserSelection | null>(null);
// Fetch user selection data
useEffect(() => {
setLoading(true);
retrieveUserSelection()
.then((data) => {
console.log("User Selection:", data);
if (data && "userType" in data) {
setUserType((data as UserSelection).userType);
setUserSelectionData(data as UserSelection);
}
})
.catch((err) => {
console.error("Error fetching user selection data:", err);
})
.finally(() => setLoading(false));
}, []);
return (
<div className="w-full bg-white shadow-sm rounded-lg overflow-hidden">
<div className="p-4 border-b border-gray-200">
<h2 className="text-xl font-bold text-center text-gray-800">
{t.dashboard}
</h2>
</div>
{/* Profile Section with Suspense */}
<div className="p-4 border-b border-gray-200 bg-gray-50">
<Suspense
fallback={<div className="text-center py-4">{t.loading}</div>}
>
{loading ? (
<ProfileLoadingState loadingText={t.loading} />
) : userType === "employee" && userSelectionData ? (
<EmployeeProfileSection
userSelectionData={userSelectionData}
lang={lang as "en" | "tr"}
/>
) : userType === "occupant" && userSelectionData ? (
<OccupantProfileSection
userSelectionData={userSelectionData}
lang={lang as "en" | "tr"}
/>
) : (
<div className="text-center text-gray-500">{t.loading}</div>
)}
</Suspense>
</div>
{/* Navigation Menu
<NavigationMenu transformedMenu={transformedMenu} lang={lang} /> */}
<NavigationMenu activePage={activePage} lang={lang} />
</div>
);
};
export default ClientMenu;

View File

@ -0,0 +1,255 @@
const Individual = {
name: "Individual",
lg: {
tr: "Birey",
en: "Individual",
},
siteUrl: "/individual",
};
const User = {
name: "User",
lg: {
tr: "Kullanıcı",
en: "User",
},
siteUrl: "/user",
};
const Build = {
name: "Build",
lg: {
tr: "Apartman",
en: "Build",
},
siteUrl: "/build",
};
const Dashboard = {
name: "Dashboard",
lg: {
tr: "Pano",
en: "Dashboard",
},
siteUrl: "/dashboard",
};
const BuildParts = {
name: "BuildParts",
lg: {
tr: "Daireler",
en: "Build Parts",
},
siteUrl: "/build/parts",
};
const BuildArea = {
name: "BuildArea",
lg: {
tr: "Daire Alanları",
en: "Build Area",
},
siteUrl: "/build/area",
};
const ManagementAccounting = {
name: "ManagementAccounting",
lg: {
tr: "Yönetim Cari Hareketler",
en: "ManagementAccounting",
},
siteUrl: "/management/accounting",
};
const ManagementBudget = {
name: "ManagementBudget",
lg: {
tr: "Yönetim Bütçe İşlemleri",
en: "Management Budget",
},
siteUrl: "/management/budget",
};
const BuildPartsAccounting = {
name: "BuildPartsAccounting",
lg: {
tr: "Daire Cari Hareketler",
en: "Build Parts Accounting",
},
siteUrl: "/build/parts/accounting",
};
const AnnualMeeting = {
name: "AnnualMeeting",
lg: {
tr: "Yıllık Olağan Toplantı Tanımlama ve Davet",
en: "Annual Meetings and Invitations",
},
siteUrl: "/annual/meeting",
};
const AnnualMeetingClose = {
name: "AnnualMeetingClose",
lg: {
tr: "Yıllık Olağan Toplantı kapatma ve Cari Yaratma",
en: "Annual Meeting Close and Accountings",
},
siteUrl: "/annual/meeting/close",
};
const EmergencyMeeting = {
name: "EmergencyMeeting",
lg: {
tr: "Acil Toplantı Tanımlama ve Davet",
en: "Emergency Meeting and Invitations",
},
siteUrl: "/emergency/meeting",
};
const EmergencyMeetingClose = {
name: "EmergencyMeetingClose",
lg: {
tr: "Acil Olağan Toplantı kapatma ve Cari Yaratma",
en: "Emergency Meeting Close and Accountings",
},
siteUrl: "/emergency/meeting/close",
};
const MeetingParticipations = {
name: "MeetingParticipations",
lg: {
tr: "Toplantı Katılım İşlemleri",
en: "Meeting Participations",
},
siteUrl: "/meeting/participation",
};
const TenantSendMessageToBuildManager = {
name: "TenantSendMessageToBuildManager",
lg: {
tr: "Bina Yöneticisine Mesaj Gönder",
en: "Send Message to Build Manager",
},
siteUrl: "/tenant/messageToBM",
};
const TenantSendMessageToOwner = {
name: "TenantSendMessageToOwner",
lg: {
tr: "Sahibine Mesaj Gönder",
en: "Send Message to Owner",
},
siteUrl: "/tenant/messageToOwner",
};
const TenantAccountView = {
name: "TenantAccountView",
lg: {
tr: "Kiracı Cari Hareketleri",
en: "Tenant Accountings",
},
siteUrl: "/tenant/accounting",
};
const Menu = [
{
name: "Dashboard",
lg: {
tr: "Pano",
en: "Dashboard",
},
subList: [
{
name: "Dashboard",
lg: {
tr: "Pano",
en: "Dashboard",
},
subList: [Dashboard],
},
],
},
{
name: "Definitions",
lg: {
tr: "Tanımlar",
en: "Definitions",
},
subList: [
{
name: "People",
lg: {
tr: "Kişiler",
en: "People",
},
subList: [Individual, User],
},
{
name: "Building",
lg: {
tr: "Binalar",
en: "Building",
},
subList: [Build, BuildParts, BuildArea],
},
],
},
{
name: "Building Management",
lg: {
tr: "Bina Yönetimi",
en: "Building Management",
},
subList: [
{
name: "Management Accounting",
lg: {
tr: "Cari işlemler",
en: "Management Accounting",
},
subList: [ManagementAccounting, ManagementBudget, BuildPartsAccounting],
},
{
name: "Meetings",
lg: {
tr: "Toplantılar",
en: "Meetings",
},
subList: [
AnnualMeeting,
AnnualMeetingClose,
EmergencyMeeting,
EmergencyMeetingClose,
MeetingParticipations,
],
},
],
},
{
name: "Tenants",
lg: {
tr: "Kiracı İşlemleri",
en: "Tenant Actions",
},
subList: [
{
name: "Accountings",
lg: {
tr: "Kiracı Cari Hareketler",
en: "Tenant Accountings",
},
subList: [TenantAccountView],
},
{
name: "Messages",
lg: {
tr: "Mesaj Gönder",
en: "Send Messages",
},
subList: [TenantSendMessageToBuildManager, TenantSendMessageToOwner],
},
],
},
];
export default Menu;

View File

@ -0,0 +1,59 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

View File

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View File

@ -0,0 +1,135 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@ -0,0 +1,167 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name })
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
)
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField()
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : props.children
if (!body) {
return null
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
{...props}
>
{body}
</p>
)
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }

View File

@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View File

@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
function Popover({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />
}
function PopoverTrigger({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}
function PopoverContent({
className,
align = "center",
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
)
}
function PopoverAnchor({
...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

View File

@ -0,0 +1,185 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "popper",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span className="absolute right-2 flex size-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@ -0,0 +1,25 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }

View File

@ -0,0 +1,30 @@
// Define pagination interface
export interface PagePagination {
page: number;
size: number;
totalCount: number;
allCount: number;
totalPages: number;
orderFields: string[];
orderTypes: string[];
pageCount: number;
query: Record<string, string>;
}
// Define request parameters interface
export interface RequestParams {
page: number;
size: number;
orderFields: string[];
orderTypes: string[];
query: Record<string, string>;
}
// Define response metadata interface
export interface ResponseMetadata {
totalCount: number;
allCount: number;
totalPages: number;
pageCount: number;
}

View File

@ -0,0 +1,11 @@
interface ClientMenuProps {
lang?: string;
activePage: string;
}
interface UserSelection {
userType: string;
selected: any;
}
export type { ClientMenuProps, UserSelection };

View File

@ -0,0 +1,61 @@
// Define TypeScript interfaces for menu structure
interface LanguageTranslation {
tr: string;
en: string;
}
interface MenuThirdLevel {
name: string;
lg: LanguageTranslation;
siteUrl: string;
}
interface MenuSecondLevel {
name: string;
lg: LanguageTranslation;
subList: MenuThirdLevel[];
}
interface MenuFirstLevel {
name: string;
lg: LanguageTranslation;
subList: MenuSecondLevel[];
}
// Define interfaces for the filtered menu structure
interface FilteredMenuThirdLevel {
name: string;
lg: LanguageTranslation;
siteUrl: string;
}
interface FilteredMenuSecondLevel {
name: string;
lg: LanguageTranslation;
subList: FilteredMenuThirdLevel[];
}
interface FilteredMenuFirstLevel {
name: string;
lg: LanguageTranslation;
subList: FilteredMenuSecondLevel[];
}
interface PageProps {
lang: keyof LanguageTranslation;
queryParams: { [key: string]: string | undefined };
}
type PageComponent = React.ComponentType<PageProps>;
export type {
PageComponent,
PageProps,
MenuFirstLevel,
MenuSecondLevel,
MenuThirdLevel,
FilteredMenuFirstLevel,
FilteredMenuSecondLevel,
FilteredMenuThirdLevel,
LanguageTranslation,
};

View File

@ -10,35 +10,24 @@ services:
- "3000:3000"
environment:
- NODE_ENV=development
cpus: 1
mem_limit: 2048m
# volumes:
# - client-frontend:/WebServices/client-frontend
identity_service:
container_name: identity_service
management_frontend:
container_name: management_frontend
build:
context: .
dockerfile: ApiServices/IdentityService/Dockerfile
dockerfile: WebServices/management-frontend/Dockerfile
networks:
- wag-services
depends_on:
- initializer_service
env_file:
- api_env.env
environment:
- API_PATH=app:app
- API_HOST=0.0.0.0
- API_PORT=8002
- API_LOG_LEVEL=info
- API_RELOAD=1
- API_APP_NAME=evyos-identity-api-gateway
- API_TITLE=WAG API Identity Api Gateway
- API_FORGOT_LINK=https://identity_service/forgot-password
- API_DESCRIPTION=This api is serves as web identity api gateway only to evyos web services.
- API_APP_URL=https://identity_service
ports:
- "8002:8002"
mem_limit: 4096m
cpus: 3
- "3001:3001"
environment:
- NODE_ENV=development
cpus: 1
mem_limit: 2048m
auth_service:
container_name: auth_service
@ -64,6 +53,35 @@ services:
- API_APP_URL=https://auth_service
ports:
- "8001:8001"
mem_limit: 512m
cpus: 0.5
identity_service:
container_name: identity_service
build:
context: .
dockerfile: ApiServices/IdentityService/Dockerfile
networks:
- wag-services
depends_on:
- initializer_service
env_file:
- api_env.env
environment:
- API_PATH=app:app
- API_HOST=0.0.0.0
- API_PORT=8002
- API_LOG_LEVEL=info
- API_RELOAD=1
- API_APP_NAME=evyos-identity-api-gateway
- API_TITLE=WAG API Identity Api Gateway
- API_FORGOT_LINK=https://identity_service/forgot-password
- API_DESCRIPTION=This api is serves as web identity api gateway only to evyos web services.
- API_APP_URL=https://identity_service
ports:
- "8002:8002"
mem_limit: 512m
cpus: 0.5
building_service:
container_name: building_service
@ -89,6 +107,8 @@ services:
- API_APP_URL=https://building_service
ports:
- "8003:8003"
mem_limit: 512m
cpus: 0.5
initializer_service:
container_name: initializer_service
@ -101,6 +121,8 @@ services:
- wag-services
env_file:
- api_env.env
mem_limit: 512m
cpus: 0.5
dealer_service:
container_name: dealer_service
@ -111,26 +133,14 @@ services:
- wag-services
env_file:
- api_env.env
mem_limit: 512m
cpus: 0.5
networks:
wag-services:
# client-frontend:
# management-frontend:
# management_frontend:
# container_name: management_frontend
# build:
# context: .
# dockerfile: WebServices/management-frontend/Dockerfile
# networks:
# - wag-services
# ports:
# - "3001:3000" # Using different host port to avoid conflicts
# # volumes:
# # - management-frontend:/WebServices/management-frontend
# environment:
# - NODE_ENV=development
# template_service:
# container_name: template_service
# build: