From 090567ade8e55dc56abf4212e7bbd4ba0fac191c Mon Sep 17 00:00:00 2001 From: berkay Date: Sun, 27 Apr 2025 14:12:49 +0300 Subject: [PATCH] managment frontend initiated --- .../middlewares/token_middleware.py | 1 - ApiDefaults/config.py | 2 +- ApiServices/AuthService/config.py | 2 +- .../AuthService/endpoints/auth/route.py | 2 +- ApiServices/AuthService/events/auth/auth.py | 40 +- .../middlewares/token_middleware.py | 1 + .../AuthService/validations/custom/token.py | 1 + WebServices/client-frontend/setup-shadcn.sh | 41 + WebServices/client-frontend/src/app/page.tsx | 104 +- .../template/REACT_TEMPLATE_BLUEPRINT.md | 111 ++ .../src/components/Pages/template/README.md | 90 ++ .../management-frontend/components.json | 21 + .../management-frontend/package-lock.json | 945 +++++++++++++++++- WebServices/management-frontend/package.json | 31 +- .../management-frontend/setup-shadcn.sh | 45 + .../src/apicalls/api-fetcher.tsx | 144 +++ .../src/apicalls/basics.ts | 75 ++ .../src/apicalls/cookies/token.tsx | 148 +++ .../src/apicalls/login/login.tsx | 183 ++++ .../src/apicalls/schemas/list.tsx | 14 + .../src/app/(AuthLayout)/auth/login/page.tsx | 16 + .../src/app/(AuthLayout)/auth/select/page.tsx | 42 + .../src/app/(AuthLayout)/layout.tsx | 29 + .../app/(DashboardLayout)/dashboard/page.tsx | 35 + .../src/app/(DashboardLayout)/layout.tsx | 24 + .../src/app/commons/pageDefaults.ts | 9 + .../management-frontend/src/app/globals.css | 123 ++- .../management-frontend/src/app/page.tsx | 130 +-- .../src/components/Pages/Readme.md | 0 .../components/Pages/appenderEvent/page.tsx | 8 + .../Pages/appendersService/page.tsx | 8 + .../src/components/Pages/application/page.tsx | 8 + .../src/components/Pages/dashboard/page.tsx | 8 + .../src/components/Pages/employee/page.tsx | 8 + .../src/components/Pages/index.ts | 15 + .../src/components/Pages/ocuppant/page.tsx | 8 + .../src/components/Pages/pageRetriever.tsx | 10 + .../src/components/Pages/unauthorizedpage.tsx | 43 + .../src/components/auth/LoginEmployee.tsx | 146 +++ .../src/components/auth/LoginOccupant.tsx | 111 ++ .../src/components/auth/login.tsx | 152 +++ .../src/components/auth/select.tsx | 87 ++ .../src/components/auth/types.ts | 36 + .../src/components/header/Header.tsx | 407 ++++++++ .../menu/EmployeeProfileSection.tsx | 223 +++++ .../src/components/menu/NavigationMenu.tsx | 55 + .../menu/OccupantProfileSection.tsx | 455 +++++++++ .../components/menu/ProfileLoadingState.tsx | 22 + .../src/components/menu/handler.tsx | 109 ++ .../src/components/menu/menu.tsx | 93 ++ .../src/components/menu/store.tsx | 255 +++++ .../src/components/ui/button.tsx | 59 ++ .../src/components/ui/card.tsx | 92 ++ .../src/components/ui/checkbox.tsx | 32 + .../src/components/ui/dialog.tsx | 135 +++ .../src/components/ui/form.tsx | 167 ++++ .../src/components/ui/input.tsx | 21 + .../src/components/ui/label.tsx | 24 + .../src/components/ui/popover.tsx | 48 + .../src/components/ui/select.tsx | 185 ++++ .../src/components/ui/sonner.tsx | 25 + .../validations/list/paginations.ts | 30 + .../src/components/validations/menu/menu.tsx | 11 + .../validations/translations/translation.tsx | 61 ++ docker-compose.yml | 80 +- 65 files changed, 5469 insertions(+), 177 deletions(-) create mode 100755 WebServices/client-frontend/setup-shadcn.sh create mode 100644 WebServices/client-frontend/src/components/Pages/template/REACT_TEMPLATE_BLUEPRINT.md create mode 100644 WebServices/client-frontend/src/components/Pages/template/README.md create mode 100644 WebServices/management-frontend/components.json create mode 100755 WebServices/management-frontend/setup-shadcn.sh create mode 100644 WebServices/management-frontend/src/apicalls/api-fetcher.tsx create mode 100644 WebServices/management-frontend/src/apicalls/basics.ts create mode 100644 WebServices/management-frontend/src/apicalls/cookies/token.tsx create mode 100644 WebServices/management-frontend/src/apicalls/login/login.tsx create mode 100644 WebServices/management-frontend/src/apicalls/schemas/list.tsx create mode 100644 WebServices/management-frontend/src/app/(AuthLayout)/auth/login/page.tsx create mode 100644 WebServices/management-frontend/src/app/(AuthLayout)/auth/select/page.tsx create mode 100644 WebServices/management-frontend/src/app/(AuthLayout)/layout.tsx create mode 100644 WebServices/management-frontend/src/app/(DashboardLayout)/dashboard/page.tsx create mode 100644 WebServices/management-frontend/src/app/(DashboardLayout)/layout.tsx create mode 100644 WebServices/management-frontend/src/app/commons/pageDefaults.ts create mode 100644 WebServices/management-frontend/src/components/Pages/Readme.md create mode 100644 WebServices/management-frontend/src/components/Pages/appenderEvent/page.tsx create mode 100644 WebServices/management-frontend/src/components/Pages/appendersService/page.tsx create mode 100644 WebServices/management-frontend/src/components/Pages/application/page.tsx create mode 100644 WebServices/management-frontend/src/components/Pages/dashboard/page.tsx create mode 100644 WebServices/management-frontend/src/components/Pages/employee/page.tsx create mode 100644 WebServices/management-frontend/src/components/Pages/index.ts create mode 100644 WebServices/management-frontend/src/components/Pages/ocuppant/page.tsx create mode 100644 WebServices/management-frontend/src/components/Pages/pageRetriever.tsx create mode 100644 WebServices/management-frontend/src/components/Pages/unauthorizedpage.tsx create mode 100644 WebServices/management-frontend/src/components/auth/LoginEmployee.tsx create mode 100644 WebServices/management-frontend/src/components/auth/LoginOccupant.tsx create mode 100644 WebServices/management-frontend/src/components/auth/login.tsx create mode 100644 WebServices/management-frontend/src/components/auth/select.tsx create mode 100644 WebServices/management-frontend/src/components/auth/types.ts create mode 100644 WebServices/management-frontend/src/components/header/Header.tsx create mode 100644 WebServices/management-frontend/src/components/menu/EmployeeProfileSection.tsx create mode 100644 WebServices/management-frontend/src/components/menu/NavigationMenu.tsx create mode 100644 WebServices/management-frontend/src/components/menu/OccupantProfileSection.tsx create mode 100644 WebServices/management-frontend/src/components/menu/ProfileLoadingState.tsx create mode 100644 WebServices/management-frontend/src/components/menu/handler.tsx create mode 100644 WebServices/management-frontend/src/components/menu/menu.tsx create mode 100644 WebServices/management-frontend/src/components/menu/store.tsx create mode 100644 WebServices/management-frontend/src/components/ui/button.tsx create mode 100644 WebServices/management-frontend/src/components/ui/card.tsx create mode 100644 WebServices/management-frontend/src/components/ui/checkbox.tsx create mode 100644 WebServices/management-frontend/src/components/ui/dialog.tsx create mode 100644 WebServices/management-frontend/src/components/ui/form.tsx create mode 100644 WebServices/management-frontend/src/components/ui/input.tsx create mode 100644 WebServices/management-frontend/src/components/ui/label.tsx create mode 100644 WebServices/management-frontend/src/components/ui/popover.tsx create mode 100644 WebServices/management-frontend/src/components/ui/select.tsx create mode 100644 WebServices/management-frontend/src/components/ui/sonner.tsx create mode 100644 WebServices/management-frontend/src/components/validations/list/paginations.ts create mode 100644 WebServices/management-frontend/src/components/validations/menu/menu.tsx create mode 100644 WebServices/management-frontend/src/components/validations/translations/translation.tsx diff --git a/ApiControllers/middlewares/token_middleware.py b/ApiControllers/middlewares/token_middleware.py index ed9cf66..df25fc2 100644 --- a/ApiControllers/middlewares/token_middleware.py +++ b/ApiControllers/middlewares/token_middleware.py @@ -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: diff --git a/ApiDefaults/config.py b/ApiDefaults/config.py index 3e3cc2b..a9c0ea3 100644 --- a/ApiDefaults/config.py +++ b/ApiDefaults/config.py @@ -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 = "" diff --git a/ApiServices/AuthService/config.py b/ApiServices/AuthService/config.py index 7b81f87..1ec17d4 100644 --- a/ApiServices/AuthService/config.py +++ b/ApiServices/AuthService/config.py @@ -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 = "" diff --git a/ApiServices/AuthService/endpoints/auth/route.py b/ApiServices/AuthService/endpoints/auth/route.py index e2f819d..5c2779e 100644 --- a/ApiServices/AuthService/endpoints/auth/route.py +++ b/ApiServices/AuthService/endpoints/auth/route.py @@ -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, diff --git a/ApiServices/AuthService/events/auth/auth.py b/ApiServices/AuthService/events/auth/auth.py index 5643f42..b1b460c 100644 --- a/ApiServices/AuthService/events/auth/auth.py +++ b/ApiServices/AuthService/events/auth/auth.py @@ -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 diff --git a/ApiServices/AuthService/middlewares/token_middleware.py b/ApiServices/AuthService/middlewares/token_middleware.py index fd9d93d..be084d0 100644 --- a/ApiServices/AuthService/middlewares/token_middleware.py +++ b/ApiServices/AuthService/middlewares/token_middleware.py @@ -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: diff --git a/ApiServices/AuthService/validations/custom/token.py b/ApiServices/AuthService/validations/custom/token.py index 92f0a4e..10142a2 100644 --- a/ApiServices/AuthService/validations/custom/token.py +++ b/ApiServices/AuthService/validations/custom/token.py @@ -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 diff --git a/WebServices/client-frontend/setup-shadcn.sh b/WebServices/client-frontend/setup-shadcn.sh new file mode 100755 index 0000000..b3c8d15 --- /dev/null +++ b/WebServices/client-frontend/setup-shadcn.sh @@ -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" diff --git a/WebServices/client-frontend/src/app/page.tsx b/WebServices/client-frontend/src/app/page.tsx index 01a89f7..89ed8a0 100644 --- a/WebServices/client-frontend/src/app/page.tsx +++ b/WebServices/client-frontend/src/app/page.tsx @@ -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 ( -
- {JSON.stringify({ data: data?.data })} +
+
+ {/* Welcome Banner */} +
+

Welcome to EVYOS

+

Enterprise Management System

+

Server Time: {currentDate}

+
+ + {/* Login Section */} +
+
+

+ Login to Your Account +

+ +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+ + +
+ + +
+
+ +
+

© {new Date().getFullYear()} EVYOS. All rights reserved.

+
+
+
); } diff --git a/WebServices/client-frontend/src/components/Pages/template/REACT_TEMPLATE_BLUEPRINT.md b/WebServices/client-frontend/src/components/Pages/template/REACT_TEMPLATE_BLUEPRINT.md new file mode 100644 index 0000000..cabecca --- /dev/null +++ b/WebServices/client-frontend/src/components/Pages/template/REACT_TEMPLATE_BLUEPRINT.md @@ -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 diff --git a/WebServices/client-frontend/src/components/Pages/template/README.md b/WebServices/client-frontend/src/components/Pages/template/README.md new file mode 100644 index 0000000..6cdd600 --- /dev/null +++ b/WebServices/client-frontend/src/components/Pages/template/README.md @@ -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 ; + } + ``` + +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 diff --git a/WebServices/management-frontend/components.json b/WebServices/management-frontend/components.json new file mode 100644 index 0000000..ffe928f --- /dev/null +++ b/WebServices/management-frontend/components.json @@ -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" +} \ No newline at end of file diff --git a/WebServices/management-frontend/package-lock.json b/WebServices/management-frontend/package-lock.json index 3edf030..5745e4f 100644 --- a/WebServices/management-frontend/package-lock.json +++ b/WebServices/management-frontend/package-lock.json @@ -8,9 +8,29 @@ "name": "management-frontend", "version": "0.1.0", "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-dom": "^19.0.0" + "react-day-picker": "^8.10.1", + "react-dom": "^19.0.0", + "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": { "@tailwindcss/postcss": "^4", @@ -18,6 +38,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "tailwindcss": "^4", + "tw-animate-css": "^1.2.8", "typescript": "^5" } }, @@ -42,6 +63,51 @@ "tslib": "^2.4.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" + }, + "node_modules/@hookform/resolvers": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.0.1.tgz", + "integrity": "sha512-u/+Jp83luQNx9AdyW2fIPGY6Y7NG68eN2ZW8FOJYL+M0i4s49+refdJdOp/A9n9HFQtQs3HIDHQvX3ZET2o7YA==", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@img/sharp-darwin-arm64": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", @@ -509,6 +575,626 @@ "node": ">= 10" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.4.tgz", + "integrity": "sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.2.3.tgz", + "integrity": "sha512-pHVzDYsnaDmBlAuwim45y3soIN8H4R7KbkSVirGhXO+R/kO2OLCe0eucUEbddaTcdMHHdzcIGHtZSMSQlA+apw==", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.4.tgz", + "integrity": "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.11.tgz", + "integrity": "sha512-yI7S1ipkP5/+99qhSI6nthfo/tR6bL6Zgxi/+1UO6qPa6UeM6nlafWcQ65vB4rU2XjgjMfMhI3k9Y5MztA62VQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.7", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.4", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.6", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.7.tgz", + "integrity": "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw==", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.4.tgz", + "integrity": "sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.4.tgz", + "integrity": "sha512-wy3dqizZnZVV4ja0FNnUhIWNwWdoldXrneEyUcVtLYDAt8ovGS4ridtMAOGgXBBIfggL4BOveVWsjXDORdGEQg==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.11.tgz", + "integrity": "sha512-yFMfZkVA5G3GJnBgb2PxrrcLKm1ZLWXrbYVgdyTl//0TYEIHS9LJbnyz7WWcZ0qCq7hIlJZpRtxeSeIG5T5oJw==", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.7", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.4", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.4", + "@radix-ui/react-portal": "1.1.6", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.4.tgz", + "integrity": "sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.6.tgz", + "integrity": "sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", + "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.2.tgz", + "integrity": "sha512-HjkVHtBkuq+r3zUAZ/CvNWUGKPfuicGDbgtZgiQuFmNcV5F+Tgy24ep2nsAW2nFgvhGPJVqeBZa6KyVN0EyrBA==", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.7", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.4", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.4", + "@radix-ui/react-portal": "1.1.6", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.0.tgz", + "integrity": "sha512-rQj0aAWOpCdCMRbI6pLQm8r7S2BM3YhTa0SzOYD55k+hJA8oo9J+H+9wLM9oMlZWOX/wJWPTzfDfmZkf7LvCfg==", + "dependencies": { + "@radix-ui/react-primitive": "2.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==" + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -772,6 +1458,17 @@ "@types/react": "^19.0.0" } }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -802,11 +1499,30 @@ } ] }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -854,6 +1570,15 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "dev": true }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/detect-libc": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", @@ -863,6 +1588,11 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -876,6 +1606,19 @@ "node": ">=10.13.0" } }, + "node_modules/flatpickr": { + "version": "4.6.13", + "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz", + "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw==" + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1125,6 +1868,14 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lucide-react": { + "version": "0.503.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.503.0.tgz", + "integrity": "sha512-HGGkdlPWQ0vTF8jJ5TdIqhQXZi6uh3LnNgfZ8MHiuxFfX3RZeA79r2MW2tHAZKlAVfoNE8esm3p+O6VkIvpj6w==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1195,6 +1946,20 @@ } } }, + "node_modules/next-crypto": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/next-crypto/-/next-crypto-1.0.8.tgz", + "integrity": "sha512-6VcrH+xFuuCRGCdDMjFFibhJ97c4s+J/6SEV73RUYJhh38MDW4WXNZNTWIMZBq0B29LOIfAQ0XA37xGUZZCCjA==" + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -1263,6 +2028,19 @@ "node": ">=0.10.0" } }, + "node_modules/react-day-picker": { + "version": "8.10.1", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz", + "integrity": "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "date-fns": "^2.28.0 || ^3.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", @@ -1274,6 +2052,87 @@ "react": "^19.1.0" } }, + "node_modules/react-hook-form": { + "version": "7.56.1", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.1.tgz", + "integrity": "sha512-qWAVokhSpshhcEuQDSANHx3jiAEFzu2HAaaQIzi/r9FNPm1ioAvuJSD4EuZzWd7Al7nTRKcKPnBKO7sRn+zavQ==", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -1339,6 +2198,15 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/sonner": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.3.tgz", + "integrity": "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1377,12 +2245,29 @@ } } }, + "node_modules/tailwind-merge": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz", + "integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.3.tgz", "integrity": "sha512-2Q+rw9vy1WFXu5cIxlvsabCwhU2qUwodGq03ODhLJ0jW4ek5BUtoCsnLB0qG+m8AHgEsSJcJGDSDe06FXlP74g==", "dev": true }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -1397,6 +2282,15 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, + "node_modules/tw-animate-css": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.8.tgz", + "integrity": "sha512-AxSnYRvyFnAiZCUndS3zQZhNfV/B77ZhJ+O7d3K6wfg/jKJY+yv6ahuyXwnyaYA9UdLqnpCwhTRv9pPTBnPR2g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -1415,6 +2309,55 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/zod": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", + "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/WebServices/management-frontend/package.json b/WebServices/management-frontend/package.json index 8fbe569..2938bbf 100644 --- a/WebServices/management-frontend/package.json +++ b/WebServices/management-frontend/package.json @@ -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" } } diff --git a/WebServices/management-frontend/setup-shadcn.sh b/WebServices/management-frontend/setup-shadcn.sh new file mode 100755 index 0000000..5f485bd --- /dev/null +++ b/WebServices/management-frontend/setup-shadcn.sh @@ -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" diff --git a/WebServices/management-frontend/src/apicalls/api-fetcher.tsx b/WebServices/management-frontend/src/apicalls/api-fetcher.tsx new file mode 100644 index 0000000..4c6b490 --- /dev/null +++ b/WebServices/management-frontend/src/apicalls/api-fetcher.tsx @@ -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 }; diff --git a/WebServices/management-frontend/src/apicalls/basics.ts b/WebServices/management-frontend/src/apicalls/basics.ts new file mode 100644 index 0000000..46f4d58 --- /dev/null +++ b/WebServices/management-frontend/src/apicalls/basics.ts @@ -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 }; diff --git a/WebServices/management-frontend/src/apicalls/cookies/token.tsx b/WebServices/management-frontend/src/apicalls/cookies/token.tsx new file mode 100644 index 0000000..194472b --- /dev/null +++ b/WebServices/management-frontend/src/apicalls/cookies/token.tsx @@ -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, +}; diff --git a/WebServices/management-frontend/src/apicalls/login/login.tsx b/WebServices/management-frontend/src/apicalls/login/login.tsx new file mode 100644 index 0000000..42bc0f9 --- /dev/null +++ b/WebServices/management-frontend/src/apicalls/login/login.tsx @@ -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, +}; diff --git a/WebServices/management-frontend/src/apicalls/schemas/list.tsx b/WebServices/management-frontend/src/apicalls/schemas/list.tsx new file mode 100644 index 0000000..659fb37 --- /dev/null +++ b/WebServices/management-frontend/src/apicalls/schemas/list.tsx @@ -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; +} diff --git a/WebServices/management-frontend/src/app/(AuthLayout)/auth/login/page.tsx b/WebServices/management-frontend/src/app/(AuthLayout)/auth/login/page.tsx new file mode 100644 index 0000000..2d20e55 --- /dev/null +++ b/WebServices/management-frontend/src/app/(AuthLayout)/auth/login/page.tsx @@ -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 ( + <> + + + ); +} diff --git a/WebServices/management-frontend/src/app/(AuthLayout)/auth/select/page.tsx b/WebServices/management-frontend/src/app/(AuthLayout)/auth/select/page.tsx new file mode 100644 index 0000000..d079ba3 --- /dev/null +++ b/WebServices/management-frontend/src/app/(AuthLayout)/auth/select/page.tsx @@ -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 ( + <> +
+
+ {isEmployee && Array.isArray(selectionList) && ( + + )} + + {isOccupant && !Array.isArray(selectionList) && ( + + )} +
+
+ + ); +} + +export default SelectPage; diff --git a/WebServices/management-frontend/src/app/(AuthLayout)/layout.tsx b/WebServices/management-frontend/src/app/(AuthLayout)/layout.tsx new file mode 100644 index 0000000..2353a89 --- /dev/null +++ b/WebServices/management-frontend/src/app/(AuthLayout)/layout.tsx @@ -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 ( +
+
+
+
WAG Frontend
+
+ Welcome to the WAG Frontend Application +
+
+
+
+ Loading...
}>{children} +
+
+ ); +} diff --git a/WebServices/management-frontend/src/app/(DashboardLayout)/dashboard/page.tsx b/WebServices/management-frontend/src/app/(DashboardLayout)/dashboard/page.tsx new file mode 100644 index 0000000..d4bc63f --- /dev/null +++ b/WebServices/management-frontend/src/app/(DashboardLayout)/dashboard/page.tsx @@ -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 ( + <> +
+ {/* Sidebar */} + + + {/* Main Content Area */} +
+ {/* Header Component */} +
+ +
+
+ + ); +} + +export default PageDashboard; diff --git a/WebServices/management-frontend/src/app/(DashboardLayout)/layout.tsx b/WebServices/management-frontend/src/app/(DashboardLayout)/layout.tsx new file mode 100644 index 0000000..521102d --- /dev/null +++ b/WebServices/management-frontend/src/app/(DashboardLayout)/layout.tsx @@ -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 ( +
+
{children}
+
+ ); +} diff --git a/WebServices/management-frontend/src/app/commons/pageDefaults.ts b/WebServices/management-frontend/src/app/commons/pageDefaults.ts new file mode 100644 index 0000000..2d66b12 --- /dev/null +++ b/WebServices/management-frontend/src/app/commons/pageDefaults.ts @@ -0,0 +1,9 @@ +export const searchPlaceholder = { + tr: "Ara...", + en: "Search...", +}; + +export const menuLanguage = { + tr: "Menü", + en: "Menu", +}; \ No newline at end of file diff --git a/WebServices/management-frontend/src/app/globals.css b/WebServices/management-frontend/src/app/globals.css index a2dc41e..c139824 100644 --- a/WebServices/management-frontend/src/app/globals.css +++ b/WebServices/management-frontend/src/app/globals.css @@ -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; -} diff --git a/WebServices/management-frontend/src/app/page.tsx b/WebServices/management-frontend/src/app/page.tsx index e68abe6..0313277 100644 --- a/WebServices/management-frontend/src/app/page.tsx +++ b/WebServices/management-frontend/src/app/page.tsx @@ -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 ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
- -
- - Vercel logomark - Deploy now - - - Read our docs - +
+
+ {/* Welcome Banner */} +
+

Welcome to EVYOS

+

Enterprise Management System

+

Server Time: {currentDate}

-
- + + {/* Login Section */} +
+
+ + Go to Sign In + +
+ +
+

© {new Date().getFullYear()} EVYOS. All rights reserved.

+
+
+
); } diff --git a/WebServices/management-frontend/src/components/Pages/Readme.md b/WebServices/management-frontend/src/components/Pages/Readme.md new file mode 100644 index 0000000..e69de29 diff --git a/WebServices/management-frontend/src/components/Pages/appenderEvent/page.tsx b/WebServices/management-frontend/src/components/Pages/appenderEvent/page.tsx new file mode 100644 index 0000000..ea688ab --- /dev/null +++ b/WebServices/management-frontend/src/components/Pages/appenderEvent/page.tsx @@ -0,0 +1,8 @@ +import { PageProps } from "@/components/validations/translations/translation"; +import React from "react"; + +const EventAppendPage: React.FC = () => { + return
EventAppendPage
; +}; + +export default EventAppendPage; diff --git a/WebServices/management-frontend/src/components/Pages/appendersService/page.tsx b/WebServices/management-frontend/src/components/Pages/appendersService/page.tsx new file mode 100644 index 0000000..505d2aa --- /dev/null +++ b/WebServices/management-frontend/src/components/Pages/appendersService/page.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import { PageProps } from "@/components/validations/translations/translation"; + +const AppendersServicePage: React.FC = () => { + return
AppendersServicePage
; +}; + +export default AppendersServicePage; diff --git a/WebServices/management-frontend/src/components/Pages/application/page.tsx b/WebServices/management-frontend/src/components/Pages/application/page.tsx new file mode 100644 index 0000000..77127f5 --- /dev/null +++ b/WebServices/management-frontend/src/components/Pages/application/page.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import { PageProps } from "@/components/validations/translations/translation"; + +const ApplicationPage: React.FC = () => { + return
ApplicationPage
; +}; + +export default ApplicationPage; diff --git a/WebServices/management-frontend/src/components/Pages/dashboard/page.tsx b/WebServices/management-frontend/src/components/Pages/dashboard/page.tsx new file mode 100644 index 0000000..71a2e8b --- /dev/null +++ b/WebServices/management-frontend/src/components/Pages/dashboard/page.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import { PageProps } from "@/components/validations/translations/translation"; + +const DashboardPage: React.FC = () => { + return
DashboardPage
; +}; + +export default DashboardPage; diff --git a/WebServices/management-frontend/src/components/Pages/employee/page.tsx b/WebServices/management-frontend/src/components/Pages/employee/page.tsx new file mode 100644 index 0000000..7b600d8 --- /dev/null +++ b/WebServices/management-frontend/src/components/Pages/employee/page.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import { PageProps } from "@/components/validations/translations/translation"; + +const EmployeePage: React.FC = () => { + return
EmployeePage
; +}; + +export default EmployeePage; diff --git a/WebServices/management-frontend/src/components/Pages/index.ts b/WebServices/management-frontend/src/components/Pages/index.ts new file mode 100644 index 0000000..d025691 --- /dev/null +++ b/WebServices/management-frontend/src/components/Pages/index.ts @@ -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, +}; diff --git a/WebServices/management-frontend/src/components/Pages/ocuppant/page.tsx b/WebServices/management-frontend/src/components/Pages/ocuppant/page.tsx new file mode 100644 index 0000000..931742d --- /dev/null +++ b/WebServices/management-frontend/src/components/Pages/ocuppant/page.tsx @@ -0,0 +1,8 @@ +import React from "react"; +import { PageProps } from "@/components/validations/translations/translation"; + +const OcuppantPage: React.FC = () => { + return
OcuppantPage
; +}; + +export default OcuppantPage; diff --git a/WebServices/management-frontend/src/components/Pages/pageRetriever.tsx b/WebServices/management-frontend/src/components/Pages/pageRetriever.tsx new file mode 100644 index 0000000..63fd96c --- /dev/null +++ b/WebServices/management-frontend/src/components/Pages/pageRetriever.tsx @@ -0,0 +1,10 @@ +import { menuPages } from "."; +import { PageProps } from "../validations/translations/translation"; +import { UnAuthorizedPage } from "./unauthorizedpage"; + +export function retrievePageByUrl(url: string): React.FC { + if (url in menuPages) { + return menuPages[url as keyof typeof menuPages]; + } + return UnAuthorizedPage; +} diff --git a/WebServices/management-frontend/src/components/Pages/unauthorizedpage.tsx b/WebServices/management-frontend/src/components/Pages/unauthorizedpage.tsx new file mode 100644 index 0000000..9ee42de --- /dev/null +++ b/WebServices/management-frontend/src/components/Pages/unauthorizedpage.tsx @@ -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 = ({ + 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 ( + <> +
+
+

{t.title}

+
+
+

{t.message1}

+

{t.message2}

+
+
+

{t.footer}

+
+
+ + ); +}; diff --git a/WebServices/management-frontend/src/components/auth/LoginEmployee.tsx b/WebServices/management-frontend/src/components/auth/LoginEmployee.tsx new file mode 100644 index 0000000..3c925ff --- /dev/null +++ b/WebServices/management-frontend/src/components/auth/LoginEmployee.tsx @@ -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 ( + <> +
{t.companySelection}
+
{t.loggedInAs}
+ + {Array.isArray(selectionList) && selectionList.length === 0 && ( +
{t.noSelections}
+ )} + + {Array.isArray(selectionList) && selectionList.length === 1 && ( +
+
+
+ + {selectionList[0].public_name} + + {selectionList[0].company_type && ( + + {selectionList[0].company_type} + + )} +
+ {selectionList[0].duty && ( +
+ + {t.duty}: {selectionList[0].duty} + +
+ )} +
+ + + {t.id}: {selectionList[0].uu_id} + + +
+
+ +
+
+
+ )} + + {Array.isArray(selectionList) && + selectionList.length > 1 && + selectionList.map((item: Company, index: number) => ( +
handleSelect(item.uu_id)} + > +
+
+ {item.public_name} + {item.company_type && ( + + {item.company_type} + + )} +
+ {item.duty && ( +
+ + {t.duty}: {item.duty} + +
+ )} +
+ + + {t.id}: {item.uu_id} + + +
+
+
+ ))} + + ); +} + +export default LoginEmployee; diff --git a/WebServices/management-frontend/src/components/auth/LoginOccupant.tsx b/WebServices/management-frontend/src/components/auth/LoginOccupant.tsx new file mode 100644 index 0000000..493e27c --- /dev/null +++ b/WebServices/management-frontend/src/components/auth/LoginOccupant.tsx @@ -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 ( + <> +
{t.occupantSelection}
+
+ {t.loggedInAs} +
+ {selectionList && Object.keys(selectionList).length > 0 ? ( + Object.keys(selectionList).map((buildKey: string) => { + const building = selectionList[buildKey]; + return ( +
+
+

+ {t.buildingInfo}: + {building.build_name} - No: {building.build_no} +

+
+ +
+ {building.occupants.map((occupant: any, idx: number) => ( +
handleSelect(occupant.build_living_space_uu_id)} + > +
+
+ + {occupant.description} + + + {occupant.code} + +
+
+ + {occupant.part_name} + +
+
+ + {t.level}: {occupant.part_level} + +
+
+
+ ))} +
+
+ ); + }) + ) : ( +
{t.noSelections}
+ )} + + ); +} + +export default LoginOccupant; diff --git a/WebServices/management-frontend/src/components/auth/login.tsx b/WebServices/management-frontend/src/components/auth/login.tsx new file mode 100644 index 0000000..f468d19 --- /dev/null +++ b/WebServices/management-frontend/src/components/auth/login.tsx @@ -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(null); + const [jsonText, setJsonText] = useState(null); + + const Router = useRouter(); + + const { + register, + formState: { errors }, + handleSubmit, + } = useForm({ + 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 ( + <> +
+
+

+ Login +

+
+
+ + + {errors.email && ( +

+ {errors.email.message} +

+ )} +
+ +
+ + + {errors.password && ( +

+ {errors.password.message} +

+ )} +
+ + {error && ( +
+

{error}

+
+ )} + + +
+
+ + {jsonText && ( +
+

+ Response Data +

+
+ {Object.entries(JSON.parse(jsonText)).map(([key, value]) => ( +
+ {key}: + + {typeof value === "object" + ? JSON.stringify(value) + : value?.toString() || "N/A"} + +
+ ))} +
+
+ )} +
+ + ); +} + +export default Login; diff --git a/WebServices/management-frontend/src/components/auth/select.tsx b/WebServices/management-frontend/src/components/auth/select.tsx new file mode 100644 index 0000000..4077251 --- /dev/null +++ b/WebServices/management-frontend/src/components/auth/select.tsx @@ -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) && ( + + )} + + {isOccupant && !Array.isArray(selectionList) && ( + + )} + + ); +} + +export default SelectList; diff --git a/WebServices/management-frontend/src/components/auth/types.ts b/WebServices/management-frontend/src/components/auth/types.ts new file mode 100644 index 0000000..bc1f175 --- /dev/null +++ b/WebServices/management-frontend/src/components/auth/types.ts @@ -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"; +} diff --git a/WebServices/management-frontend/src/components/header/Header.tsx b/WebServices/management-frontend/src/components/header/Header.tsx new file mode 100644 index 0000000..4f1ab4b --- /dev/null +++ b/WebServices/management-frontend/src/components/header/Header.tsx @@ -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 = ({ 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(null); + const notificationsRef = useRef(null); + const messagesRef = useRef(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 ( +
+

{menuLanguage[lang]}

+
+ + + {/* Notifications dropdown */} +
+
{ + setIsNotificationsOpen(!isNotificationsOpen); + setIsMessagesOpen(false); + setIsDropdownOpen(false); + }} + > + + {notifications.some((n) => !n.read) && ( + + {notifications.filter((n) => !n.read).length} + + )} +
+ + {/* Notifications dropdown menu */} + {isNotificationsOpen && ( +
+
+

{t.notifications}

+ {notifications.some((n) => !n.read) && ( + + )} +
+ +
+ {notifications.length === 0 ? ( +
+ {t.noNotifications} +
+ ) : ( + notifications.map((notification) => ( +
+
+

+ {notification.title} +

+ {!notification.read && ( + + )} +
+

+ {notification.description} +

+

+ {formatDate(notification.time)} +

+
+ )) + )} +
+ + +
+ )} +
+ + {/* Messages dropdown */} +
+
{ + setIsMessagesOpen(!isMessagesOpen); + setIsNotificationsOpen(false); + setIsDropdownOpen(false); + }} + > + + {messages.some((m) => !m.read) && ( + + {messages.filter((m) => !m.read).length} + + )} +
+ + {/* Messages dropdown menu */} + {isMessagesOpen && ( +
+
+

{t.messages}

+ {messages.some((m) => !m.read) && ( + + )} +
+ +
+ {messages.length === 0 ? ( +
+ {t.noMessages} +
+ ) : ( + messages.map((message) => ( +
+
+
+ + {message.avatar} + +
+
+
+

+ {message.sender} +

+

+ {formatDate(message.time)} +

+
+

+ {message.message} +

+
+
+
+ )) + )} +
+ + +
+ )} +
+ + {/* Profile dropdown */} +
+
{ + setIsDropdownOpen(!isDropdownOpen); + setIsNotificationsOpen(false); + setIsMessagesOpen(false); + }} + > +
+ +
+ +
+ + {/* Dropdown menu */} + {isDropdownOpen && ( + + )} +
+
+
+ ); +}; + +export default Header; diff --git a/WebServices/management-frontend/src/components/menu/EmployeeProfileSection.tsx b/WebServices/management-frontend/src/components/menu/EmployeeProfileSection.tsx new file mode 100644 index 0000000..5493744 --- /dev/null +++ b/WebServices/management-frontend/src/components/menu/EmployeeProfileSection.tsx @@ -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 = ({ + userSelectionData, + lang = "en", +}) => { + const t = + profileLanguage[lang as keyof typeof profileLanguage] || profileLanguage.en; + + const [showSelectionList, setShowSelectionList] = useState(false); + const [loading, setLoading] = useState(true); + // Initialize state with data from props + const [userSelection, setUserSelection] = useState( + userSelectionData?.selected || null + ); + const [selectionList, setSelectionList] = useState( + null + ); + const [availableCompanies, setAvailableCompanies] = useState( + [] + ); + const [hasMultipleOptions, setHasMultipleOptions] = useState(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
{t.loading}
; + } + + return ( +
+ {/*
+
+ +
+
+

{t.userType}

+

{t.employee}

+
+
*/} + +
+ hasMultipleOptions && setShowSelectionList(!showSelectionList) + } + > +
+
+ +
+
+

+ {userSelection.public_name} {userSelection.company_type} +

+

{userSelection.duty}

+
+
+
+ + {/* Selection dropdown */} + {showSelectionList && hasMultipleOptions && ( +
+
+

{t.selectCompany}

+
+
+ {loading ? ( +
{t.loading}
+ ) : availableCompanies.length > 0 ? ( + availableCompanies.map((company, index) => ( +
handleSelectCompany(company)} + > +
{company.public_name}
+ {company.company_type && ( +
+ {company.company_type} +
+ )} + {company.duty && ( +
+ {t.duty}: {company.duty} +
+ )} +
+ )) + ) : ( +
+ {t.noCompanies} +
+ )} +
+
+ )} +
+ ); +}; + +export default EmployeeProfileSection; diff --git a/WebServices/management-frontend/src/components/menu/NavigationMenu.tsx b/WebServices/management-frontend/src/components/menu/NavigationMenu.tsx new file mode 100644 index 0000000..1f14b79 --- /dev/null +++ b/WebServices/management-frontend/src/components/menu/NavigationMenu.tsx @@ -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 ( + + ); +} + +export default NavigationMenu; diff --git a/WebServices/management-frontend/src/components/menu/OccupantProfileSection.tsx b/WebServices/management-frontend/src/components/menu/OccupantProfileSection.tsx new file mode 100644 index 0000000..5aaa973 --- /dev/null +++ b/WebServices/management-frontend/src/components/menu/OccupantProfileSection.tsx @@ -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 = ({ + userSelectionData, + lang = "en", +}) => { + const t = + profileLanguage[lang as keyof typeof profileLanguage] || profileLanguage.en; + const router = useRouter(); + + const [showSelectionList, setShowSelectionList] = useState(false); + const [loading, setLoading] = useState(true); + // Initialize state with data from props + const [userSelection, setUserSelection] = useState( + userSelectionData?.selected || null + ); + const [selectionList, setSelectionList] = + useState(null); + const [availableOccupants, setAvailableOccupants] = useState([]); + const [hasMultipleOptions, setHasMultipleOptions] = useState(false); + const [selectedBuildingKey, setSelectedBuildingKey] = useState( + 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
{t.loading}
; + } + + return ( +
+ {/*
+
+ +
+
+

{t.userType}

+

{t.occupant}

+
+
*/} + + {userSelection?.buildName && ( +
+
+ +
+
+

{t.building}

+

{userSelection.buildName}

+
+
+ )} + + {userSelection?.part_name && ( +
+ hasMultipleOptions && setShowSelectionList(!showSelectionList) + } + > +
+
+ +
+
+

{t.apartment}

+

{userSelection.part_name}

+ {userSelection.description && ( +

+ {userSelection.description} +

+ )} +
+
+ {hasMultipleOptions && ( +
+ {t.changeSelection} +
+ )} +
+ )} + + {/* Selection dropdown - First layer: Buildings */} + {showSelectionList && hasMultipleOptions && ( +
+
+

{t.selectOccupant}

+
+
+ {loading ? ( +
{t.loading}
+ ) : buildings && Object.keys(buildings).length > 0 ? ( + selectedBuildingKey ? ( + // Second layer: Occupants in the selected building +
+
+
+ {buildings[selectedBuildingKey].build_name} +
+ +
+ {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 ( +
handleSelectOccupant(occupant)} + > +
+ {occupant.description || "Apartment"} +
+
+ {occupant.part_name} +
+ {occupant.code && ( +
+ {t.apartment} {occupant.code} +
+ )} +
+ ); + } + ) + ) : ( +
+ {t.noOccupants} +
+ )} +
+ ) : ( + // 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 ( +
setSelectedBuildingKey(buildingKey)} + > +
{building.build_name}
+
+ No: {building.build_no} +
+
+ {building.occupants.length}{" "} + {building.occupants.length === 1 + ? t.apartment.toLowerCase() + : t.apartment.toLowerCase() + "s"} +
+
+ ); + }) + ) + ) : ( +
+ {t.noOccupants} +
+ )} +
+
+ )} +
+ ); +}; + +export default OccupantProfileSection; diff --git a/WebServices/management-frontend/src/components/menu/ProfileLoadingState.tsx b/WebServices/management-frontend/src/components/menu/ProfileLoadingState.tsx new file mode 100644 index 0000000..b5f2b63 --- /dev/null +++ b/WebServices/management-frontend/src/components/menu/ProfileLoadingState.tsx @@ -0,0 +1,22 @@ +"use client"; +import React from "react"; + +interface ProfileLoadingStateProps { + loadingText: string; +} + +const ProfileLoadingState: React.FC = ({ loadingText }) => { + return ( +
+
+
+
+
+
+
+
+
+ ); +}; + +export default ProfileLoadingState; diff --git a/WebServices/management-frontend/src/components/menu/handler.tsx b/WebServices/management-frontend/src/components/menu/handler.tsx new file mode 100644 index 0000000..d39db23 --- /dev/null +++ b/WebServices/management-frontend/src/components/menu/handler.tsx @@ -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; +} \ No newline at end of file diff --git a/WebServices/management-frontend/src/components/menu/menu.tsx b/WebServices/management-frontend/src/components/menu/menu.tsx new file mode 100644 index 0000000..aba8327 --- /dev/null +++ b/WebServices/management-frontend/src/components/menu/menu.tsx @@ -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 = ({ 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(true); + const [userType, setUserType] = useState(null); + const [userSelectionData, setUserSelectionData] = + useState(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 ( +
+
+

+ {t.dashboard} +

+
+ + {/* Profile Section with Suspense */} +
+ {t.loading}
} + > + {loading ? ( + + ) : userType === "employee" && userSelectionData ? ( + + ) : userType === "occupant" && userSelectionData ? ( + + ) : ( +
{t.loading}
+ )} + +
+ + {/* Navigation Menu + */} + + + ); +}; + +export default ClientMenu; diff --git a/WebServices/management-frontend/src/components/menu/store.tsx b/WebServices/management-frontend/src/components/menu/store.tsx new file mode 100644 index 0000000..c04419f --- /dev/null +++ b/WebServices/management-frontend/src/components/menu/store.tsx @@ -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; diff --git a/WebServices/management-frontend/src/components/ui/button.tsx b/WebServices/management-frontend/src/components/ui/button.tsx new file mode 100644 index 0000000..a2df8dc --- /dev/null +++ b/WebServices/management-frontend/src/components/ui/button.tsx @@ -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 & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/WebServices/management-frontend/src/components/ui/card.tsx b/WebServices/management-frontend/src/components/ui/card.tsx new file mode 100644 index 0000000..d05bbc6 --- /dev/null +++ b/WebServices/management-frontend/src/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/WebServices/management-frontend/src/components/ui/checkbox.tsx b/WebServices/management-frontend/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..fa0e4b5 --- /dev/null +++ b/WebServices/management-frontend/src/components/ui/checkbox.tsx @@ -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) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/WebServices/management-frontend/src/components/ui/dialog.tsx b/WebServices/management-frontend/src/components/ui/dialog.tsx new file mode 100644 index 0000000..7d7a9d3 --- /dev/null +++ b/WebServices/management-frontend/src/components/ui/dialog.tsx @@ -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) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + {children} + + + Close + + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/WebServices/management-frontend/src/components/ui/form.tsx b/WebServices/management-frontend/src/components/ui/form.tsx new file mode 100644 index 0000000..524b986 --- /dev/null +++ b/WebServices/management-frontend/src/components/ui/form.tsx @@ -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 = FieldPath, +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +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 ") + } + + 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( + {} as FormItemContextValue +) + +function FormItem({ className, ...props }: React.ComponentProps<"div">) { + const id = React.useId() + + return ( + +
+ + ) +} + +function FormLabel({ + className, + ...props +}: React.ComponentProps) { + const { error, formItemId } = useFormField() + + return ( +