added application page
This commit is contained in:
parent
346b132f4c
commit
ac344773c5
|
|
@ -0,0 +1,29 @@
|
||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
# Install system dependencies and Poetry
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends gcc && rm -rf /var/lib/apt/lists/* && pip install --no-cache-dir poetry
|
||||||
|
|
||||||
|
# Copy Poetry configuration
|
||||||
|
COPY /pyproject.toml ./pyproject.toml
|
||||||
|
|
||||||
|
# Configure Poetry and install dependencies with optimizations
|
||||||
|
RUN poetry config virtualenvs.create false && poetry install --no-interaction --no-ansi --no-root --only main \
|
||||||
|
&& pip cache purge && rm -rf ~/.cache/pypoetry
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY /ApiControllers /ApiControllers
|
||||||
|
COPY /ApiDefaults /ApiDefaults
|
||||||
|
COPY /Controllers /Controllers
|
||||||
|
COPY /Schemas /Schemas
|
||||||
|
|
||||||
|
COPY /ApiServices/ManagementService/Endpoints /ApiDefaults/Endpoints
|
||||||
|
COPY /ApiServices/ManagementService/Events /ApiDefaults/Events
|
||||||
|
COPY /ApiServices/ManagementService/Validations /ApiDefaults/Validations
|
||||||
|
|
||||||
|
# Set Python path to include app directory
|
||||||
|
ENV PYTHONPATH=/ PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
||||||
|
# Run the application using the configured uvicorn server
|
||||||
|
CMD ["poetry", "run", "python", "ApiDefaults/app.py"]
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from ApiControllers.abstracts.default_validations import CommonHeaders
|
||||||
|
from ApiControllers.providers.token_provider import TokenProvider
|
||||||
|
|
||||||
|
from Controllers.Postgres.pagination import PaginateOnly, Pagination, PaginationResult
|
||||||
|
from Controllers.Postgres.response import EndpointResponse
|
||||||
|
from Schemas import (
|
||||||
|
Applications
|
||||||
|
)
|
||||||
|
from Validations.application.validations import (
|
||||||
|
RequestApplication,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create API router
|
||||||
|
application_route = APIRouter(prefix="/application", tags=["Application Management"])
|
||||||
|
|
||||||
|
|
||||||
|
@application_route.post(
|
||||||
|
path="/list",
|
||||||
|
description="List applications endpoint",
|
||||||
|
operation_id="application-list",
|
||||||
|
)
|
||||||
|
def application_list_route(
|
||||||
|
list_options: PaginateOnly,
|
||||||
|
headers: CommonHeaders = Depends(CommonHeaders.as_dependency),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
List applications with pagination and filtering options
|
||||||
|
"""
|
||||||
|
token_object = TokenProvider.get_dict_from_redis(token=headers.token)
|
||||||
|
list_options = PaginateOnly(**list_options.model_dump())
|
||||||
|
with Applications.new_session() as db_session:
|
||||||
|
if list_options.query:
|
||||||
|
applications_list = Applications.filter_all(
|
||||||
|
*Applications.convert(list_options.query), db=db_session
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
applications_list = Applications.filter_all(db=db_session)
|
||||||
|
pagination = Pagination(data=applications_list)
|
||||||
|
pagination.change(**list_options.model_dump())
|
||||||
|
pagination_result = PaginationResult(
|
||||||
|
data=applications_list,
|
||||||
|
pagination=pagination,
|
||||||
|
)
|
||||||
|
return EndpointResponse(
|
||||||
|
message="MSG0003-LIST",
|
||||||
|
pagination_result=pagination_result,
|
||||||
|
).response
|
||||||
|
|
||||||
|
|
||||||
|
@application_route.post(
|
||||||
|
path="/create",
|
||||||
|
description="Create application endpoint",
|
||||||
|
operation_id="application-create",
|
||||||
|
)
|
||||||
|
def application_create_route(
|
||||||
|
data: RequestApplication,
|
||||||
|
headers: CommonHeaders = Depends(CommonHeaders.as_dependency),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create a new application
|
||||||
|
"""
|
||||||
|
token_object = TokenProvider.get_dict_from_redis(token=headers.token)
|
||||||
|
with Applications.new_session() as db_session:
|
||||||
|
created_application_dict = data.model_dump()
|
||||||
|
created_application = Applications.find_or_create(
|
||||||
|
db=db_session,
|
||||||
|
include_args=[
|
||||||
|
Applications.application_for == data.application_for,
|
||||||
|
Applications.application_code == data.application_code,
|
||||||
|
Applications.site_url == data.site_url,
|
||||||
|
]
|
||||||
|
**created_application_dict
|
||||||
|
)
|
||||||
|
if created_application.meta_data.created:
|
||||||
|
return EndpointResponse(
|
||||||
|
message="MSG0001-INSERT",
|
||||||
|
data=created_application,
|
||||||
|
).response
|
||||||
|
return EndpointResponse(
|
||||||
|
message="MSG0002-FOUND",
|
||||||
|
data=created_application,
|
||||||
|
).response
|
||||||
|
|
||||||
|
|
||||||
|
@application_route.post(
|
||||||
|
path="/update/{application_uuid}",
|
||||||
|
description="Update application endpoint",
|
||||||
|
operation_id="application-update",
|
||||||
|
)
|
||||||
|
def application_update_route(
|
||||||
|
data: RequestApplication,
|
||||||
|
application_uuid: str,
|
||||||
|
headers: CommonHeaders = Depends(CommonHeaders.as_dependency),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Update an existing application
|
||||||
|
"""
|
||||||
|
token_object = TokenProvider.get_dict_from_redis(token=headers.token)
|
||||||
|
with Applications.new_session() as db_session:
|
||||||
|
updated_application_dict = data.model_dump()
|
||||||
|
found_application = Applications.filter_one(
|
||||||
|
Applications.uu_id == application_uuid, db=db_session
|
||||||
|
).data
|
||||||
|
if not found_application:
|
||||||
|
return EndpointResponse(
|
||||||
|
message="MSG0002-FOUND",
|
||||||
|
data=found_application,
|
||||||
|
).response
|
||||||
|
updated_application = found_application.update(**updated_application_dict)
|
||||||
|
updated_application.save(db_session)
|
||||||
|
if updated_application.meta_data.updated:
|
||||||
|
return EndpointResponse(
|
||||||
|
message="MSG0003-UPDATE",
|
||||||
|
data=updated_application,
|
||||||
|
).response
|
||||||
|
return EndpointResponse(
|
||||||
|
message="MSG0003-UPDATE",
|
||||||
|
data=updated_application,
|
||||||
|
).response
|
||||||
|
|
||||||
|
|
||||||
|
@application_route.delete(
|
||||||
|
path="/{application_uuid}",
|
||||||
|
description="Delete application endpoint",
|
||||||
|
operation_id="application-delete",
|
||||||
|
)
|
||||||
|
def application_delete_route(
|
||||||
|
application_uuid: str,
|
||||||
|
headers: CommonHeaders = Depends(CommonHeaders.as_dependency),
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Delete application by ID
|
||||||
|
"""
|
||||||
|
token_object = TokenProvider.get_dict_from_redis(token=headers.token)
|
||||||
|
with Applications.new_session() as db_session:
|
||||||
|
found_application = Applications.filter_one(
|
||||||
|
Applications.uu_id == application_uuid, db=db_session
|
||||||
|
).data
|
||||||
|
if not found_application:
|
||||||
|
return EndpointResponse(
|
||||||
|
message="MSG0002-FOUND",
|
||||||
|
data=found_application,
|
||||||
|
).response
|
||||||
|
found_application.destroy(db_session)
|
||||||
|
Applications.save(db_session)
|
||||||
|
if found_application.meta_data.deleted:
|
||||||
|
return EndpointResponse(
|
||||||
|
message="MSG0004-DELETE",
|
||||||
|
data=found_application,
|
||||||
|
).response
|
||||||
|
return EndpointResponse(
|
||||||
|
message="MSG0004-DELETE",
|
||||||
|
data=found_application,
|
||||||
|
).response
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
|
||||||
|
def get_routes() -> list[APIRouter]:
|
||||||
|
from .application.route import application_route
|
||||||
|
|
||||||
|
return [application_route]
|
||||||
|
|
||||||
|
|
||||||
|
def get_safe_endpoint_urls() -> list[tuple[str, str]]:
|
||||||
|
return [
|
||||||
|
("/", "GET"),
|
||||||
|
("/docs", "GET"),
|
||||||
|
("/redoc", "GET"),
|
||||||
|
("/openapi.json", "GET"),
|
||||||
|
("/metrics", "GET"),
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = []
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
class RequestApplication(BaseModel):
|
||||||
|
"""Base model for application data"""
|
||||||
|
name: str = Field(..., description="Application name")
|
||||||
|
application_code: str = Field(..., description="Unique application code")
|
||||||
|
site_url: str = Field(..., description="Application site URL")
|
||||||
|
application_type: str = Field(..., description="Application type (info, Dash, Admin)")
|
||||||
|
application_for: str = Field(..., description="Application for (EMP, OCC)")
|
||||||
|
description: Optional[str] = Field(None, description="Application description")
|
||||||
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
# Import all routes
|
||||||
|
from ApiServices.ManagementService.Endpoints.application.route import application_route
|
||||||
|
|
||||||
|
# Create main router for ManagementService
|
||||||
|
management_service_router = APIRouter(prefix="/management", tags=["Management Service"])
|
||||||
|
|
||||||
|
# Include all routes
|
||||||
|
management_service_router.include_router(application_route)
|
||||||
|
|
||||||
|
# Export the router
|
||||||
|
__all__ = ["management_service_router"]
|
||||||
|
|
@ -4,8 +4,8 @@ Advanced filtering functionality for SQLAlchemy models.
|
||||||
This module provides a comprehensive set of filtering capabilities for SQLAlchemy models,
|
This module provides a comprehensive set of filtering capabilities for SQLAlchemy models,
|
||||||
including pagination, ordering, and complex query building.
|
including pagination, ordering, and complex query building.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
|
|
||||||
from typing import Any, TypeVar, Type, Union, Optional
|
from typing import Any, TypeVar, Type, Union, Optional
|
||||||
|
|
@ -23,7 +23,7 @@ T = TypeVar("T", bound="QueryModel")
|
||||||
class QueryModel:
|
class QueryModel:
|
||||||
|
|
||||||
__abstract__ = True
|
__abstract__ = True
|
||||||
pre_query = None
|
pre_query: Optional[Query] = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _query(cls: Type[T], db: Session) -> Query:
|
def _query(cls: Type[T], db: Session) -> Query:
|
||||||
|
|
@ -149,11 +149,29 @@ class QueryModel:
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of SQLAlchemy filter expressions or None if validation fails
|
Tuple of SQLAlchemy filter expressions or None if validation fails
|
||||||
"""
|
"""
|
||||||
if validate_model is not None:
|
try:
|
||||||
# Add validation logic here if needed
|
# Let SQLAlchemy handle the validation by attempting to create the filter expressions
|
||||||
pass
|
return tuple(cls.filter_expr(**smart_options))
|
||||||
|
except Exception as e:
|
||||||
return tuple(cls.filter_expr(**smart_options))
|
# If there's an error, provide a helpful message with valid columns and relationships
|
||||||
|
valid_columns = set()
|
||||||
|
relationship_names = set()
|
||||||
|
|
||||||
|
# Get column names if available
|
||||||
|
if hasattr(cls, '__table__') and hasattr(cls.__table__, 'columns'):
|
||||||
|
valid_columns = set(column.key for column in cls.__table__.columns)
|
||||||
|
|
||||||
|
# Get relationship names if available
|
||||||
|
if hasattr(cls, '__mapper__') and hasattr(cls.__mapper__, 'relationships'):
|
||||||
|
relationship_names = set(rel.key for rel in cls.__mapper__.relationships)
|
||||||
|
|
||||||
|
# Create a helpful error message
|
||||||
|
error_msg = f"Error in filter expression: {str(e)}\n"
|
||||||
|
error_msg += f"Attempted to filter with: {smart_options}\n"
|
||||||
|
error_msg += f"Valid columns are: {', '.join(valid_columns)}\n"
|
||||||
|
error_msg += f"Valid relationships are: {', '.join(relationship_names)}"
|
||||||
|
|
||||||
|
raise ValueError(error_msg) from e
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def filter_by_one(
|
def filter_by_one(
|
||||||
|
|
|
||||||
|
|
@ -122,8 +122,6 @@ class EndpointResponse(BaseModel):
|
||||||
def response(self):
|
def response(self):
|
||||||
"""Convert response to dictionary format."""
|
"""Convert response to dictionary format."""
|
||||||
resutl_data = getattr(self.pagination_result, "data", None)
|
resutl_data = getattr(self.pagination_result, "data", None)
|
||||||
if not resutl_data:
|
|
||||||
raise ValueError("Invalid pagination result data.")
|
|
||||||
result_pagination = getattr(self.pagination_result, "pagination", None)
|
result_pagination = getattr(self.pagination_result, "pagination", None)
|
||||||
if not result_pagination:
|
if not result_pagination:
|
||||||
raise ValueError("Invalid pagination result pagination.")
|
raise ValueError("Invalid pagination result pagination.")
|
||||||
|
|
|
||||||
|
|
@ -343,7 +343,7 @@ class BuildParts(CrudCollection):
|
||||||
)
|
)
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
Index("build_parts_ndx_01", build_id, part_no, unique=True),
|
Index("build_parts_ndx_1", build_id, part_no, unique=True),
|
||||||
{"comment": "Part objects that are belong to building objects"},
|
{"comment": "Part objects that are belong to building objects"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import { fetchData, fetchDataWithToken } from "../api-fetcher";
|
||||||
|
import { baseUrlApplication } from "../basics";
|
||||||
|
import { PageListOptions, PaginateOnly } from "../schemas/list";
|
||||||
|
|
||||||
|
const applicationListEndpoint = `${baseUrlApplication}/application/list`;
|
||||||
|
const applicationUpdateEndpoint = `${baseUrlApplication}/application/update`;
|
||||||
|
const applicationCreateEndpoint = `${baseUrlApplication}/application/create`;
|
||||||
|
const applicationDeleteEndpoint = `${baseUrlApplication}/application/delete`;
|
||||||
|
|
||||||
|
interface RequestApplication {
|
||||||
|
name: string;
|
||||||
|
application_code: string;
|
||||||
|
site_url: string;
|
||||||
|
application_type: string;
|
||||||
|
application_for: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listApplications(payload: PageListOptions) {
|
||||||
|
try {
|
||||||
|
const response = await fetchDataWithToken(
|
||||||
|
applicationListEndpoint,
|
||||||
|
{
|
||||||
|
page: payload.page,
|
||||||
|
size: payload.size,
|
||||||
|
order_field: payload.orderField,
|
||||||
|
order_type: payload.orderType,
|
||||||
|
query: payload.query,
|
||||||
|
},
|
||||||
|
"POST",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
return response?.status === 200 || response?.status === 202
|
||||||
|
? response.data
|
||||||
|
: null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching application list:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createApplication(payload: any) {
|
||||||
|
try {
|
||||||
|
const response = await fetchDataWithToken(
|
||||||
|
applicationCreateEndpoint,
|
||||||
|
payload,
|
||||||
|
"POST",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
return response?.status === 200 || response?.status === 202
|
||||||
|
? response.data
|
||||||
|
: null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating application:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateApplication(payload: any, uuId: string) {
|
||||||
|
try {
|
||||||
|
const response = await fetchDataWithToken(
|
||||||
|
`${applicationUpdateEndpoint}/${uuId}`,
|
||||||
|
payload,
|
||||||
|
"POST",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
return response?.status === 200 || response?.status === 202
|
||||||
|
? response.data
|
||||||
|
: null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating application:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteApplication(uuId: string) {
|
||||||
|
try {
|
||||||
|
const response = await fetchDataWithToken(
|
||||||
|
`${applicationDeleteEndpoint}/${uuId}`,
|
||||||
|
{},
|
||||||
|
"DELETE",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
return response?.status === 200 || response?.status === 202
|
||||||
|
? response.data
|
||||||
|
: null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting application:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
listApplications,
|
||||||
|
createApplication,
|
||||||
|
updateApplication,
|
||||||
|
deleteApplication,
|
||||||
|
};
|
||||||
|
|
@ -9,6 +9,10 @@ export const baseUrlAuth = formatServiceUrl(
|
||||||
export const baseUrlPeople = formatServiceUrl(
|
export const baseUrlPeople = formatServiceUrl(
|
||||||
process.env.NEXT_PUBLIC_VALIDATION_SERVICE_URL || "identity_service:8002"
|
process.env.NEXT_PUBLIC_VALIDATION_SERVICE_URL || "identity_service:8002"
|
||||||
);
|
);
|
||||||
|
export const baseUrlApplication = formatServiceUrl(
|
||||||
|
process.env.NEXT_PUBLIC_MANAGEMENT_SERVICE_URL || "management_service:8004"
|
||||||
|
);
|
||||||
|
|
||||||
// export const baseUrlEvent = formatServiceUrl(
|
// export const baseUrlEvent = formatServiceUrl(
|
||||||
// process.env.NEXT_PUBLIC_EVENT_SERVICE_URL || "eventservice:8888"
|
// process.env.NEXT_PUBLIC_EVENT_SERVICE_URL || "eventservice:8888"
|
||||||
// );
|
// );
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
import { LanguageTranslation } from "@/components/validations/translations/translation";
|
||||||
|
|
||||||
|
interface ActionButtonsComponentProps {
|
||||||
|
onCreateClick: () => void;
|
||||||
|
translations: Record<string, LanguageTranslation>;
|
||||||
|
lang: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ActionButtonsComponent: React.FC<ActionButtonsComponentProps> = ({
|
||||||
|
onCreateClick,
|
||||||
|
translations,
|
||||||
|
lang,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-end my-4">
|
||||||
|
<Button onClick={onCreateClick}>
|
||||||
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
|
Create New Application
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { LanguageTranslation } from "@/components/validations/translations/translation";
|
||||||
|
import { ApplicationData } from "./types";
|
||||||
|
|
||||||
|
interface DataDisplayComponentProps {
|
||||||
|
data: ApplicationData[];
|
||||||
|
loading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
onUpdateClick: (item: ApplicationData) => void;
|
||||||
|
translations: Record<string, LanguageTranslation>;
|
||||||
|
lang: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DataDisplayComponent: React.FC<DataDisplayComponentProps> = ({
|
||||||
|
data,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
onUpdateClick,
|
||||||
|
translations,
|
||||||
|
lang,
|
||||||
|
}) => {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="w-full text-center py-8">Loading applications...</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="w-full text-center py-8 text-red-500">
|
||||||
|
Error loading applications: {error.message}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
return <div className="w-full text-center py-8">No applications found</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap -mx-2">
|
||||||
|
{data.map((app, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="w-full sm:w-1/5 p-1"
|
||||||
|
onClick={() => onUpdateClick(app)}
|
||||||
|
>
|
||||||
|
<Card className="h-full hover:bg-accent/50 cursor-pointer transition-colors">
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<div className="font-medium text-sm mb-1">{app.name}</div>
|
||||||
|
<div className="space-y-0.5 text-xs">
|
||||||
|
<div className="flex">
|
||||||
|
<span className="text-muted-foreground w-10">
|
||||||
|
{translations.code &&
|
||||||
|
translations.code[lang as keyof LanguageTranslation]}
|
||||||
|
:
|
||||||
|
</span>
|
||||||
|
<span className="truncate">{app.application_code}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="text-muted-foreground w-10">
|
||||||
|
{translations.url &&
|
||||||
|
translations.url[lang as keyof LanguageTranslation]}
|
||||||
|
:
|
||||||
|
</span>
|
||||||
|
<span className="truncate">{app.site_url}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<span className="text-muted-foreground w-10">
|
||||||
|
{translations.type &&
|
||||||
|
translations.type[lang as keyof LanguageTranslation]}
|
||||||
|
:
|
||||||
|
</span>
|
||||||
|
<span className="truncate">{app.application_type}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,359 @@
|
||||||
|
"use client";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Save, ArrowLeft } from "lucide-react";
|
||||||
|
import { ApplicationData } from "./types";
|
||||||
|
import {
|
||||||
|
ApplicationFormData,
|
||||||
|
ApplicationSchema,
|
||||||
|
CreateApplicationSchema,
|
||||||
|
UpdateApplicationSchema,
|
||||||
|
fieldDefinitions,
|
||||||
|
fieldsByMode,
|
||||||
|
FieldDefinition,
|
||||||
|
} from "./schema";
|
||||||
|
import { getTranslation, LanguageKey } from "./language";
|
||||||
|
|
||||||
|
interface FormComponentProps {
|
||||||
|
initialData?: ApplicationData;
|
||||||
|
mode: "create" | "update";
|
||||||
|
refetch: () => void;
|
||||||
|
setMode: (mode: "list" | "create" | "update") => void;
|
||||||
|
setSelectedItem: (item: ApplicationData | null) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
lang: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ValidationErrors {
|
||||||
|
[key: string]: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FormComponent: React.FC<FormComponentProps> = ({
|
||||||
|
initialData,
|
||||||
|
mode,
|
||||||
|
refetch,
|
||||||
|
setMode,
|
||||||
|
setSelectedItem,
|
||||||
|
onCancel,
|
||||||
|
lang,
|
||||||
|
}) => {
|
||||||
|
const t = getTranslation(lang as LanguageKey);
|
||||||
|
|
||||||
|
// Convert initialData to ApplicationFormData with proper defaults
|
||||||
|
const getInitialFormData = (): ApplicationFormData => {
|
||||||
|
if (initialData) {
|
||||||
|
return {
|
||||||
|
...initialData,
|
||||||
|
// Ensure required fields have defaults
|
||||||
|
active: initialData.active ?? true,
|
||||||
|
deleted: initialData.deleted ?? false,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
name: "",
|
||||||
|
application_code: "",
|
||||||
|
site_url: "",
|
||||||
|
application_type: "",
|
||||||
|
application_for: "EMP",
|
||||||
|
description: "",
|
||||||
|
active: true,
|
||||||
|
deleted: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState<ApplicationFormData>(
|
||||||
|
getInitialFormData()
|
||||||
|
);
|
||||||
|
|
||||||
|
const [validationErrors, setValidationErrors] = useState<ValidationErrors>(
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Get field definitions for current mode
|
||||||
|
const currentFields = fieldsByMode[mode];
|
||||||
|
const fieldDefs = fieldDefinitions.getDefinitionsByMode(mode);
|
||||||
|
|
||||||
|
// Handle form input changes
|
||||||
|
const handleInputChange = (name: string, value: any) => {
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
[name]: value,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear validation error for this field when it changes
|
||||||
|
if (validationErrors[name]) {
|
||||||
|
const newErrors = { ...validationErrors };
|
||||||
|
delete newErrors[name];
|
||||||
|
setValidationErrors(newErrors);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate form data based on mode
|
||||||
|
const validateForm = (): boolean => {
|
||||||
|
try {
|
||||||
|
if (mode === "create") {
|
||||||
|
CreateApplicationSchema.parse(formData);
|
||||||
|
} else if (mode === "update") {
|
||||||
|
UpdateApplicationSchema.parse(formData);
|
||||||
|
} else {
|
||||||
|
ApplicationSchema.parse(formData);
|
||||||
|
}
|
||||||
|
setValidationErrors({});
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.errors) {
|
||||||
|
const errors: ValidationErrors = {};
|
||||||
|
error.errors.forEach((err: any) => {
|
||||||
|
const field = err.path[0];
|
||||||
|
if (!errors[field]) {
|
||||||
|
errors[field] = [];
|
||||||
|
}
|
||||||
|
errors[field].push(err.message);
|
||||||
|
});
|
||||||
|
setValidationErrors(errors);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (isSubmitting) return;
|
||||||
|
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
// Validate form data
|
||||||
|
if (!validateForm()) {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// TODO: Implement API call to save the data
|
||||||
|
// For now, just go back to list mode after a delay to simulate API call
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
refetch();
|
||||||
|
setMode("list");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error saving application:", error);
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle cancel button click
|
||||||
|
const handleCancel = () => {
|
||||||
|
setSelectedItem(null);
|
||||||
|
setMode("list");
|
||||||
|
onCancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
const title =
|
||||||
|
mode === "create"
|
||||||
|
? "Create Application"
|
||||||
|
: mode === "update"
|
||||||
|
? "Update Application"
|
||||||
|
: "View Application";
|
||||||
|
|
||||||
|
// Render a field based on its definition
|
||||||
|
const renderField = (fieldName: string) => {
|
||||||
|
const fieldDef = fieldDefs[
|
||||||
|
fieldName as keyof typeof fieldDefs
|
||||||
|
] as FieldDefinition;
|
||||||
|
if (!fieldDef) return null;
|
||||||
|
|
||||||
|
const hasError = !!validationErrors[fieldName];
|
||||||
|
const errorMessages = validationErrors[fieldName] || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={fieldName} className="mb-4">
|
||||||
|
<Label
|
||||||
|
className={`block text-sm font-medium mb-1 ${
|
||||||
|
hasError ? "text-red-500" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{fieldDef.label}{" "}
|
||||||
|
{fieldDef.required && <span className="text-red-500">*</span>}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
{fieldDef.type === "text" && !fieldDef.readOnly && (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
name={fieldName}
|
||||||
|
value={
|
||||||
|
(formData[fieldName as keyof ApplicationFormData] as string) ||
|
||||||
|
""
|
||||||
|
}
|
||||||
|
onChange={(e) => handleInputChange(fieldName, e.target.value)}
|
||||||
|
className={hasError ? "border-red-500" : ""}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{fieldDef.type === "text" && fieldDef.readOnly && (
|
||||||
|
<h1>
|
||||||
|
{(formData[fieldName as keyof ApplicationFormData] as string) || ""}
|
||||||
|
</h1>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fieldDef.type === "textarea" && !fieldDef.readOnly && (
|
||||||
|
<textarea
|
||||||
|
name={fieldName}
|
||||||
|
className={`w-full min-h-[100px] rounded-md border ${
|
||||||
|
hasError ? "border-red-500" : "border-input"
|
||||||
|
} bg-transparent px-3 py-2 text-sm shadow-sm`}
|
||||||
|
value={
|
||||||
|
(formData[fieldName as keyof ApplicationFormData] as string) || ""
|
||||||
|
}
|
||||||
|
onChange={(e) => handleInputChange(fieldName, e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{fieldDef.type === "textarea" && fieldDef.readOnly && (
|
||||||
|
<h1>
|
||||||
|
{(formData[fieldName as keyof ApplicationFormData] as string) || ""}
|
||||||
|
</h1>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fieldDef.type === "select" && (
|
||||||
|
<Select
|
||||||
|
value={
|
||||||
|
(formData[fieldName as keyof ApplicationFormData] as string) || ""
|
||||||
|
}
|
||||||
|
onValueChange={(value) => handleInputChange(fieldName, value)}
|
||||||
|
disabled={fieldDef.readOnly}
|
||||||
|
>
|
||||||
|
<SelectTrigger className={hasError ? "border-red-500" : ""}>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={`Select ${fieldDef.label.toLowerCase()}`}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{(fieldDef.options || []).map((option: string) => (
|
||||||
|
<SelectItem key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fieldDef.type === "checkbox" && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id={fieldName}
|
||||||
|
checked={!!formData[fieldName as keyof ApplicationFormData]}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
handleInputChange(fieldName, !!checked)
|
||||||
|
}
|
||||||
|
disabled={fieldDef.readOnly}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={fieldName}
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
{fieldDef.label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fieldDef.type === "date" && !fieldDef.readOnly && (
|
||||||
|
<Input
|
||||||
|
type="datetime-local"
|
||||||
|
name={fieldName}
|
||||||
|
value={
|
||||||
|
(formData[fieldName as keyof ApplicationFormData] as string) || ""
|
||||||
|
}
|
||||||
|
onChange={(e) => handleInputChange(fieldName, e.target.value)}
|
||||||
|
className={hasError ? "border-red-500" : ""}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{fieldDef.type === "date" && fieldDef.readOnly && (
|
||||||
|
<h1>
|
||||||
|
{(formData[fieldName as keyof ApplicationFormData] as string) || ""}
|
||||||
|
</h1>
|
||||||
|
)}
|
||||||
|
{hasError &&
|
||||||
|
errorMessages.map((error, index) => (
|
||||||
|
<p key={index} className="text-xs text-red-500 mt-1">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Group fields by their group property
|
||||||
|
const groupedFields: Record<string, string[]> = {};
|
||||||
|
currentFields.forEach((field) => {
|
||||||
|
const fieldDef = fieldDefs[field as keyof typeof fieldDefs];
|
||||||
|
if (fieldDef) {
|
||||||
|
const group = fieldDef.group || "other";
|
||||||
|
if (!groupedFields[group]) {
|
||||||
|
groupedFields[group] = [];
|
||||||
|
}
|
||||||
|
groupedFields[group].push(field);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<CardTitle>{title}</CardTitle>
|
||||||
|
<Button variant="outline" size="sm" onClick={handleCancel}>
|
||||||
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||||
|
{t.previous}
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{/* Render fields by group */}
|
||||||
|
{Object.entries(groupedFields).map(([group, fields]) => (
|
||||||
|
<div key={group} className="mb-6">
|
||||||
|
<h3 className="text-lg font-medium mb-4 capitalize">
|
||||||
|
{group
|
||||||
|
.replace(/([A-Z])/g, " $1")
|
||||||
|
.replace(/Info$/, " Information")}
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{fields.map((field) => renderField(field))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{mode === "create" ||
|
||||||
|
(mode === "update" && (
|
||||||
|
<div className="flex justify-end mt-6">
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className="min-w-[120px]"
|
||||||
|
>
|
||||||
|
<Save className="mr-2 h-4 w-4" />
|
||||||
|
{isSubmitting ? "Saving..." : "Save"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{
|
||||||
|
<div className="mt-6">
|
||||||
|
<h3 className="text-lg font-medium mb-4">Raw Data</h3>
|
||||||
|
<pre className="bg-muted p-4 rounded-md overflow-auto">
|
||||||
|
{JSON.stringify(formData, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { PagePagination } from "./hooks";
|
||||||
|
import { getTranslation, LanguageKey } from "./language";
|
||||||
|
|
||||||
|
interface PaginationToolsComponentProps {
|
||||||
|
pagination: PagePagination;
|
||||||
|
updatePagination: (updates: Partial<PagePagination>) => void;
|
||||||
|
loading: boolean;
|
||||||
|
lang: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PaginationToolsComponent: React.FC<PaginationToolsComponentProps> = ({
|
||||||
|
pagination,
|
||||||
|
updatePagination,
|
||||||
|
loading,
|
||||||
|
lang,
|
||||||
|
}) => {
|
||||||
|
const t = getTranslation(lang as LanguageKey);
|
||||||
|
|
||||||
|
const handlePageChange = (newPage: number) => {
|
||||||
|
if (newPage >= 1 && newPage <= pagination.totalPages) {
|
||||||
|
updatePagination({ page: newPage });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap justify-between items-center mt-6 gap-4">
|
||||||
|
{/* Pagination stats - left side */}
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
<div>
|
||||||
|
{t.showing}{" "}
|
||||||
|
{/* Show the range based on filtered count when available */}
|
||||||
|
{(pagination.totalCount || pagination.allCount || 0) > 0
|
||||||
|
? (pagination.page - 1) * pagination.size + 1
|
||||||
|
: 0}{" "}
|
||||||
|
- {Math.min(
|
||||||
|
pagination.page * pagination.size,
|
||||||
|
pagination.totalCount || pagination.allCount || 0
|
||||||
|
)} {t.of} {pagination.totalCount || pagination.allCount || 0}{" "}
|
||||||
|
{t.items}
|
||||||
|
</div>
|
||||||
|
{pagination.totalCount && pagination.totalCount !== (pagination.allCount || 0) && (
|
||||||
|
<div>
|
||||||
|
{t.total}: {pagination.allCount || 0} {t.items} ({t.filtered}: {pagination.totalCount} {t.items})
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation buttons - center */}
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(pagination.page - 1)}
|
||||||
|
disabled={pagination.page <= 1 || loading}
|
||||||
|
>
|
||||||
|
{t.previous}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Page number buttons */}
|
||||||
|
<div className="flex items-center space-x-1">
|
||||||
|
{Array.from({ length: Math.min(5, Math.max(1, Math.ceil(
|
||||||
|
(pagination.totalCount && pagination.totalCount !== pagination.allCount
|
||||||
|
? pagination.totalCount
|
||||||
|
: (pagination.allCount || 0)) / pagination.size
|
||||||
|
))) }, (_, i) => {
|
||||||
|
// Show pages around current page
|
||||||
|
let pageNum;
|
||||||
|
const calculatedTotalPages = Math.max(1, Math.ceil(
|
||||||
|
(pagination.totalCount && pagination.totalCount !== pagination.allCount
|
||||||
|
? pagination.totalCount
|
||||||
|
: (pagination.allCount || 0)) / pagination.size
|
||||||
|
));
|
||||||
|
if (calculatedTotalPages <= 5) {
|
||||||
|
pageNum = i + 1;
|
||||||
|
} else if (pagination.page <= 3) {
|
||||||
|
pageNum = i + 1;
|
||||||
|
} else if (pagination.page >= calculatedTotalPages - 2) {
|
||||||
|
pageNum = calculatedTotalPages - 4 + i;
|
||||||
|
} else {
|
||||||
|
pageNum = pagination.page - 2 + i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={pageNum}
|
||||||
|
variant={pagination.page === pageNum ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
className="w-9 h-9 p-0"
|
||||||
|
onClick={() => handlePageChange(pageNum)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{pageNum}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(pagination.page + 1)}
|
||||||
|
disabled={pagination.page >= Math.max(1, Math.ceil(
|
||||||
|
(pagination.totalCount && pagination.totalCount !== pagination.allCount
|
||||||
|
? pagination.totalCount
|
||||||
|
: (pagination.allCount || 0)) / pagination.size
|
||||||
|
)) || loading}
|
||||||
|
>
|
||||||
|
{t.next}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Page text display */}
|
||||||
|
<span className="px-4 py-1 text-sm text-muted-foreground">
|
||||||
|
{t.page} {pagination.page} {t.of} {Math.max(1, Math.ceil(
|
||||||
|
(pagination.totalCount && pagination.totalCount !== pagination.allCount
|
||||||
|
? pagination.totalCount
|
||||||
|
: (pagination.allCount || 0)) / pagination.size
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Items per page selector - right side */}
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-sm text-muted-foreground">{t.itemsPerPage}</span>
|
||||||
|
<Select
|
||||||
|
value={pagination.size.toString()}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
updatePagination({
|
||||||
|
size: Number(value),
|
||||||
|
page: 1 // Reset to first page when changing page size
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-16">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="5">5</SelectItem>
|
||||||
|
<SelectItem value="10">10</SelectItem>
|
||||||
|
<SelectItem value="20">20</SelectItem>
|
||||||
|
<SelectItem value="50">50</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
"use client";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { User, Building, Filter, Search, Link } from "lucide-react";
|
||||||
|
import { LanguageTranslation } from "@/components/validations/translations/translation";
|
||||||
|
|
||||||
|
interface SearchComponentProps {
|
||||||
|
onSearch: (query: Record<string, string>) => void;
|
||||||
|
translations: Record<string, LanguageTranslation>;
|
||||||
|
lang: string;
|
||||||
|
urlOptions: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchComponent: React.FC<SearchComponentProps> = ({
|
||||||
|
onSearch,
|
||||||
|
translations,
|
||||||
|
lang,
|
||||||
|
urlOptions,
|
||||||
|
}) => {
|
||||||
|
const [selectedType, setSelectedType] = useState<"employee" | "occupant">("employee");
|
||||||
|
const [selectedUrl, setSelectedUrl] = useState<string>("");
|
||||||
|
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||||
|
|
||||||
|
// Handle selection button click
|
||||||
|
const handleTypeSelect = (type: "employee" | "occupant") => {
|
||||||
|
setSelectedType(type);
|
||||||
|
|
||||||
|
// Include type in search query
|
||||||
|
handleSearch(searchQuery, selectedUrl, type);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle search with all parameters
|
||||||
|
const handleSearch = (query: string, url: string, type: "employee" | "occupant") => {
|
||||||
|
const searchParams: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (query && query.length > 3) {
|
||||||
|
searchParams.name = query;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
searchParams.site_url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchParams.application_for = type === "employee" ? "EMP" : "OCC";
|
||||||
|
|
||||||
|
// Call onSearch with the search parameters
|
||||||
|
// The parent component will handle resetting pagination
|
||||||
|
onSearch(searchParams);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="flex flex-row">
|
||||||
|
{/* User type selection - vertical on the left (w-1/2) */}
|
||||||
|
<div className="w-1/2 flex flex-col space-y-4 pr-4">
|
||||||
|
<div className="font-medium text-sm mb-2 flex items-center">
|
||||||
|
<User className="mr-2 h-4 w-4" />
|
||||||
|
{translations.typeSelection && translations.typeSelection[lang as keyof LanguageTranslation]}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant={selectedType === "employee" ? "default" : "outline"}
|
||||||
|
size="lg"
|
||||||
|
onClick={() => handleTypeSelect("employee")}
|
||||||
|
className="w-full h-14 mb-2"
|
||||||
|
>
|
||||||
|
<User className="mr-2 h-4 w-4" />
|
||||||
|
{translations.employee && translations.employee[lang as keyof LanguageTranslation]}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={selectedType === "occupant" ? "default" : "outline"}
|
||||||
|
size="lg"
|
||||||
|
onClick={() => handleTypeSelect("occupant")}
|
||||||
|
className="w-full h-14"
|
||||||
|
>
|
||||||
|
<Building className="mr-2 h-4 w-4" />
|
||||||
|
{translations.occupant && translations.occupant[lang as keyof LanguageTranslation]}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters on the right (w-1/2) */}
|
||||||
|
<div className="w-1/2 flex flex-col space-y-4 pl-4">
|
||||||
|
<div className="font-medium text-sm mb-2 flex items-center">
|
||||||
|
<Filter className="mr-2 h-4 w-4" />
|
||||||
|
{translations.filterSelection && translations.filterSelection[lang as keyof LanguageTranslation]}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search input */}
|
||||||
|
<div className="w-full">
|
||||||
|
<label className="block text-xs font-medium mb-1">
|
||||||
|
{translations.search && translations.search[lang as keyof LanguageTranslation]}
|
||||||
|
</label>
|
||||||
|
<div className="relative w-full flex">
|
||||||
|
<Input
|
||||||
|
placeholder={`${translations.search && translations.search[lang as keyof LanguageTranslation]}...`}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
onKeyUp={(e) => {
|
||||||
|
if (e.key === 'Enter' && searchQuery.length >= 3) {
|
||||||
|
handleSearch(searchQuery, selectedUrl, selectedType);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="pl-8 w-full h-10"
|
||||||
|
/>
|
||||||
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
className="ml-2"
|
||||||
|
onClick={() => {
|
||||||
|
if (searchQuery.length >= 3) {
|
||||||
|
handleSearch(searchQuery, selectedUrl, selectedType);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Search className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Site URL dropdown */}
|
||||||
|
<div className="w-full">
|
||||||
|
<label className="block text-xs font-medium mb-1">
|
||||||
|
{translations.siteUrl && translations.siteUrl[lang as keyof LanguageTranslation]}
|
||||||
|
</label>
|
||||||
|
<div className="w-full">
|
||||||
|
<Select
|
||||||
|
value={selectedUrl}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setSelectedUrl(value);
|
||||||
|
handleSearch(searchQuery, value, selectedType);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full h-10">
|
||||||
|
<SelectValue
|
||||||
|
placeholder={`${translations.siteUrl && translations.siteUrl[lang as keyof LanguageTranslation]}...`}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{urlOptions.map((url) => (
|
||||||
|
<SelectItem key={url} value={url}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Link className="mr-2 h-3 w-3" />
|
||||||
|
{url}
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ArrowDown, ArrowUp } from "lucide-react";
|
||||||
|
import { LanguageTranslation } from "@/components/validations/translations/translation";
|
||||||
|
|
||||||
|
interface SortingComponentProps {
|
||||||
|
sortField: string | null;
|
||||||
|
sortDirection: "asc" | "desc" | null;
|
||||||
|
onSort: (field: string) => void;
|
||||||
|
translations: Record<string, LanguageTranslation>;
|
||||||
|
lang: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SortingComponent: React.FC<SortingComponentProps> = ({
|
||||||
|
sortField,
|
||||||
|
sortDirection,
|
||||||
|
onSort,
|
||||||
|
translations,
|
||||||
|
lang,
|
||||||
|
}) => {
|
||||||
|
// Available sort fields
|
||||||
|
const sortFields = [
|
||||||
|
{ key: "name", label: "Name" },
|
||||||
|
{ key: "application_code", label: "Code" },
|
||||||
|
{ key: "application_type", label: "Type" },
|
||||||
|
{ key: "created_at", label: "Created" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-wrap gap-2 my-4">
|
||||||
|
<div className="text-sm font-medium mr-2 flex items-center">
|
||||||
|
Sort by:
|
||||||
|
</div>
|
||||||
|
{sortFields.map((field) => (
|
||||||
|
<Button
|
||||||
|
key={field.key}
|
||||||
|
variant={sortField === field.key ? "default" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onSort(field.key)}
|
||||||
|
className="flex items-center"
|
||||||
|
>
|
||||||
|
{field.label}
|
||||||
|
{sortField === field.key && (
|
||||||
|
<>
|
||||||
|
{sortDirection === "asc" ? (
|
||||||
|
<ArrowUp className="ml-1 h-3 w-3" />
|
||||||
|
) : (
|
||||||
|
<ArrowDown className="ml-1 h-3 w-3" />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import { listApplications } from "@/apicalls/application/application";
|
||||||
|
import { ApplicationData } from "./types";
|
||||||
|
|
||||||
|
export interface RequestParams {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
orderField: string[];
|
||||||
|
orderType: string[];
|
||||||
|
query: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResponseMetadata {
|
||||||
|
totalCount: number;
|
||||||
|
totalItems: number;
|
||||||
|
totalPages: number;
|
||||||
|
pageCount: number;
|
||||||
|
allCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PagePagination extends RequestParams, ResponseMetadata {}
|
||||||
|
|
||||||
|
// Custom hook for pagination and data fetching
|
||||||
|
export function useApplicationData() {
|
||||||
|
const [data, setData] = useState<ApplicationData[]>([]);
|
||||||
|
|
||||||
|
// Request parameters - these are controlled by the user
|
||||||
|
const [requestParams, setRequestParams] = useState<RequestParams>({
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
orderField: ["name"],
|
||||||
|
orderType: ["asc"],
|
||||||
|
query: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Response metadata - these come from the API
|
||||||
|
const [responseMetadata, setResponseMetadata] = useState<ResponseMetadata>({
|
||||||
|
totalCount: 0,
|
||||||
|
totalItems: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
pageCount: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|
||||||
|
const fetchApplicationsFromApi = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await listApplications({
|
||||||
|
page: requestParams.page,
|
||||||
|
size: requestParams.size,
|
||||||
|
orderField: requestParams.orderField,
|
||||||
|
orderType: requestParams.orderType,
|
||||||
|
query: requestParams.query,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result && result.data) {
|
||||||
|
setData(result.data);
|
||||||
|
|
||||||
|
// Update response metadata from API response
|
||||||
|
if (result.pagination) {
|
||||||
|
setResponseMetadata({
|
||||||
|
totalCount: result.pagination.totalCount || 0,
|
||||||
|
totalItems: result.pagination.totalCount || 0,
|
||||||
|
totalPages: result.pagination.totalPages || 1,
|
||||||
|
pageCount: result.pagination.pageCount || 0,
|
||||||
|
allCount: result.pagination.allCount || 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err : new Error("Unknown error"));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
requestParams.page,
|
||||||
|
requestParams.size,
|
||||||
|
requestParams.orderField,
|
||||||
|
requestParams.orderType,
|
||||||
|
requestParams.query,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
fetchApplicationsFromApi();
|
||||||
|
}, 300); // Debounce
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [fetchApplicationsFromApi]);
|
||||||
|
|
||||||
|
const updatePagination = useCallback((updates: Partial<RequestParams>) => {
|
||||||
|
// Transform query parameters to use __ilike with %value% format
|
||||||
|
if (updates.query) {
|
||||||
|
const transformedQuery: Record<string, any> = {};
|
||||||
|
|
||||||
|
Object.entries(updates.query).forEach(([key, value]) => {
|
||||||
|
// Only transform string values that aren't already using a special operator
|
||||||
|
if (typeof value === 'string' && !key.includes('__') && value.trim() !== '') {
|
||||||
|
transformedQuery[`${key}__ilike`] = `%${value}%`;
|
||||||
|
} else {
|
||||||
|
transformedQuery[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updates.query = transformedQuery;
|
||||||
|
|
||||||
|
// Always reset to page 1 when search query changes
|
||||||
|
if (!updates.hasOwnProperty('page')) {
|
||||||
|
updates.page = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset response metadata when search changes to avoid stale pagination data
|
||||||
|
setResponseMetadata({
|
||||||
|
totalCount: 0,
|
||||||
|
totalItems: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
pageCount: 0,
|
||||||
|
allCount: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setRequestParams((prev) => ({
|
||||||
|
...prev,
|
||||||
|
...updates,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Create a combined refetch function
|
||||||
|
const refetch = useCallback(() => {
|
||||||
|
// Reset pagination to page 1 when manually refetching
|
||||||
|
setRequestParams(prev => ({
|
||||||
|
...prev,
|
||||||
|
page: 1
|
||||||
|
}));
|
||||||
|
fetchApplicationsFromApi();
|
||||||
|
}, [fetchApplicationsFromApi]);
|
||||||
|
|
||||||
|
// Combine request params and response metadata
|
||||||
|
const pagination: PagePagination = {
|
||||||
|
...requestParams,
|
||||||
|
...responseMetadata,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
pagination,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
updatePagination,
|
||||||
|
refetch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
export type LanguageKey = "en" | "tr";
|
||||||
|
|
||||||
|
export interface TranslationSet {
|
||||||
|
showing: string;
|
||||||
|
of: string;
|
||||||
|
items: string;
|
||||||
|
total: string;
|
||||||
|
filtered: string;
|
||||||
|
page: string;
|
||||||
|
previous: string;
|
||||||
|
next: string;
|
||||||
|
itemsPerPage: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const translations: Record<LanguageKey, TranslationSet> = {
|
||||||
|
en: {
|
||||||
|
showing: "Showing",
|
||||||
|
of: "of",
|
||||||
|
items: "items",
|
||||||
|
total: "Total",
|
||||||
|
filtered: "Filtered",
|
||||||
|
page: "Page",
|
||||||
|
previous: "Previous",
|
||||||
|
next: "Next",
|
||||||
|
itemsPerPage: "Items per page",
|
||||||
|
},
|
||||||
|
tr: {
|
||||||
|
showing: "Gösteriliyor",
|
||||||
|
of: "/",
|
||||||
|
items: "öğe",
|
||||||
|
total: "Toplam",
|
||||||
|
filtered: "Filtrelenmiş",
|
||||||
|
page: "Sayfa",
|
||||||
|
previous: "Önceki",
|
||||||
|
next: "Sonraki",
|
||||||
|
itemsPerPage: "Sayfa başına öğe",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getTranslation(lang: LanguageKey): TranslationSet {
|
||||||
|
return translations[lang];
|
||||||
|
}
|
||||||
|
|
@ -1,90 +1,31 @@
|
||||||
"use client";
|
"use client";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { PageProps } from "@/components/validations/translations/translation";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { useApplicationData } from "./hooks";
|
||||||
import { Input } from "@/components/ui/input";
|
import { ApplicationData, translations } from "./types";
|
||||||
import {
|
import { SearchComponent } from "./SearchComponent";
|
||||||
Select,
|
import { ActionButtonsComponent } from "./ActionButtonsComponent";
|
||||||
SelectContent,
|
import { SortingComponent } from "./SortingComponent";
|
||||||
SelectItem,
|
import { PaginationToolsComponent } from "./PaginationToolsComponent";
|
||||||
SelectTrigger,
|
import { DataDisplayComponent } from "./DataDisplayComponent";
|
||||||
SelectValue,
|
import { FormComponent } from "./FormComponent";
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { User, Building, Save, Filter, Search, Link } from "lucide-react";
|
|
||||||
import {
|
|
||||||
PageProps,
|
|
||||||
LanguageTranslation,
|
|
||||||
} from "@/components/validations/translations/translation";
|
|
||||||
|
|
||||||
interface ApplicationData {
|
|
||||||
name: string;
|
|
||||||
application_code: string;
|
|
||||||
site_url: string;
|
|
||||||
application_type: string;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const translations: Record<string, LanguageTranslation> = {
|
|
||||||
typeSelection: {
|
|
||||||
en: "Type Selection",
|
|
||||||
tr: "Tür Seçimi",
|
|
||||||
},
|
|
||||||
filterSelection: {
|
|
||||||
en: "Filter Selection",
|
|
||||||
tr: "Filtre Seçimi",
|
|
||||||
},
|
|
||||||
employee: {
|
|
||||||
en: "Employee",
|
|
||||||
tr: "Çalışan",
|
|
||||||
},
|
|
||||||
occupant: {
|
|
||||||
en: "Occupant",
|
|
||||||
tr: "Sakin",
|
|
||||||
},
|
|
||||||
search: {
|
|
||||||
en: "Search",
|
|
||||||
tr: "Ara",
|
|
||||||
},
|
|
||||||
siteUrl: {
|
|
||||||
en: "Site URL",
|
|
||||||
tr: "Site URL",
|
|
||||||
},
|
|
||||||
applicationType: {
|
|
||||||
en: "Application Type",
|
|
||||||
tr: "Uygulama Türü",
|
|
||||||
},
|
|
||||||
availableApplications: {
|
|
||||||
en: "Available Applications",
|
|
||||||
tr: "Mevcut Uygulamalar",
|
|
||||||
},
|
|
||||||
code: {
|
|
||||||
en: "Code",
|
|
||||||
tr: "Kod",
|
|
||||||
},
|
|
||||||
url: {
|
|
||||||
en: "URL",
|
|
||||||
tr: "URL",
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
en: "Type",
|
|
||||||
tr: "Tür",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const ApplicationPage: React.FC<PageProps> = ({ lang = "en" }) => {
|
const ApplicationPage: React.FC<PageProps> = ({ lang = "en" }) => {
|
||||||
const [selectedType, setSelectedType] = useState<"employee" | "occupant">(
|
// Use the custom hook for paginated data
|
||||||
"employee"
|
const { data, pagination, loading, error, updatePagination, refetch } =
|
||||||
|
useApplicationData();
|
||||||
|
|
||||||
|
// State for managing view/edit modes
|
||||||
|
const [mode, setMode] = useState<"list" | "create" | "update">("list");
|
||||||
|
const [selectedItem, setSelectedItem] = useState<ApplicationData | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
// State for sorting
|
||||||
|
const [sortField, setSortField] = useState<string | null>(null);
|
||||||
|
const [sortDirection, setSortDirection] = useState<"asc" | "desc" | null>(
|
||||||
|
null
|
||||||
);
|
);
|
||||||
const [selectedUrl, setSelectedUrl] = useState<string>("");
|
|
||||||
const [selectedAppType, setSelectedAppType] = useState<string>("");
|
|
||||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
|
||||||
const [applicationData, setApplicationData] = useState<ApplicationData>({
|
|
||||||
name: "",
|
|
||||||
application_code: "",
|
|
||||||
site_url: "",
|
|
||||||
application_type: "",
|
|
||||||
description: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Available options for dropdowns
|
// Available options for dropdowns
|
||||||
const urlOptions = [
|
const urlOptions = [
|
||||||
|
|
@ -94,275 +35,115 @@ const ApplicationPage: React.FC<PageProps> = ({ lang = "en" }) => {
|
||||||
"/settings",
|
"/settings",
|
||||||
"/reports",
|
"/reports",
|
||||||
];
|
];
|
||||||
const typeOptions = ["info", "Dash", "Admin"];
|
|
||||||
|
|
||||||
// Handle selection button click
|
const handleUpdateClick = (item: ApplicationData) => {
|
||||||
const handleTypeSelect = (type: "employee" | "occupant") => {
|
setSelectedItem(item);
|
||||||
setSelectedType(type);
|
setMode("update");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle application data input changes
|
// Function to handle the create button click
|
||||||
const handleInputChange = (name: string, value: string) => {
|
const handleCreateClick = () => {
|
||||||
setApplicationData({
|
setSelectedItem(null);
|
||||||
...applicationData,
|
setMode("create");
|
||||||
[name]: value,
|
};
|
||||||
|
|
||||||
|
// Handle search from the SearchComponent
|
||||||
|
const handleSearch = (query: Record<string, string>) => {
|
||||||
|
updatePagination({
|
||||||
|
page: 1, // Reset to first page on new search
|
||||||
|
query: query,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sample application data for the grid
|
// Handle sorting
|
||||||
const sampleApplications = [
|
const handleSort = (field: string) => {
|
||||||
{
|
let direction: "asc" | "desc" | null = "asc";
|
||||||
name: "Dashboard",
|
|
||||||
application_code: "app000001",
|
|
||||||
site_url: "/dashboard",
|
|
||||||
application_type: "info",
|
|
||||||
description: "Dashboard Page",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Individual",
|
|
||||||
application_code: "app000003",
|
|
||||||
site_url: "/individual",
|
|
||||||
application_type: "Dash",
|
|
||||||
description: "Individual Page for people",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "User",
|
|
||||||
application_code: "app000004",
|
|
||||||
site_url: "/user",
|
|
||||||
application_type: "Dash",
|
|
||||||
description: "Individual Page for user",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Settings",
|
|
||||||
application_code: "app000005",
|
|
||||||
site_url: "/settings",
|
|
||||||
application_type: "Admin",
|
|
||||||
description: "Settings Page",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Reports",
|
|
||||||
application_code: "app000006",
|
|
||||||
site_url: "/reports",
|
|
||||||
application_type: "info",
|
|
||||||
description: "Reports Page",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Generate grid of application cards
|
if (sortField === field) {
|
||||||
const renderApplicationGrid = () => {
|
// Toggle direction if same field is clicked
|
||||||
return sampleApplications.map((app, index) => (
|
if (sortDirection === "asc") {
|
||||||
<div key={index} className="w-full sm:w-1/5 p-1">
|
direction = "desc";
|
||||||
<Card className="h-full hover:bg-accent/50 cursor-pointer transition-colors">
|
} else if (sortDirection === "desc") {
|
||||||
<CardContent className="p-3">
|
// Clear sorting if already desc
|
||||||
<div className="font-medium text-sm mb-1">{app.name}</div>
|
field = "";
|
||||||
<div className="space-y-0.5 text-xs">
|
direction = null;
|
||||||
<div className="flex">
|
}
|
||||||
<span className="text-muted-foreground w-10">
|
}
|
||||||
{translations.code[lang]}:
|
|
||||||
</span>
|
setSortField(field || null);
|
||||||
<span className="truncate">{app.application_code}</span>
|
setSortDirection(direction);
|
||||||
</div>
|
|
||||||
<div className="flex">
|
updatePagination({
|
||||||
<span className="text-muted-foreground w-10">
|
orderField: field ? [field] : [],
|
||||||
{translations.url[lang]}:
|
orderType: direction ? [direction] : [],
|
||||||
</span>
|
});
|
||||||
<span className="truncate">{app.site_url}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="text-muted-foreground w-10">
|
|
||||||
{translations.type[lang]}:
|
|
||||||
</span>
|
|
||||||
<span className="truncate">{app.application_type}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-4 space-y-6">
|
<div className="container mx-auto px-4 py-6">
|
||||||
{/* Selection Buttons */}
|
{/* List Mode - Show search, actions, and data display */}
|
||||||
<Card>
|
{mode === "list" && (
|
||||||
<CardContent className="pt-6">
|
<>
|
||||||
<div className="flex flex-row">
|
{/* Search Component */}
|
||||||
{/* User type selection - vertical on the left (w-1/2) */}
|
<SearchComponent
|
||||||
<div className="w-1/2 flex flex-col space-y-4 pr-4">
|
onSearch={handleSearch}
|
||||||
<div className="font-medium text-sm mb-2 flex items-center">
|
translations={translations}
|
||||||
<User className="mr-2 h-4 w-4" />
|
lang={lang}
|
||||||
{translations.typeSelection[lang]}
|
urlOptions={urlOptions}
|
||||||
</div>
|
/>
|
||||||
<Button
|
|
||||||
variant={selectedType === "employee" ? "default" : "outline"}
|
|
||||||
size="lg"
|
|
||||||
onClick={() => handleTypeSelect("employee")}
|
|
||||||
className="w-full h-14 mb-2"
|
|
||||||
>
|
|
||||||
<User className="mr-2 h-4 w-4" />
|
|
||||||
{translations.employee[lang]}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={selectedType === "occupant" ? "default" : "outline"}
|
|
||||||
size="lg"
|
|
||||||
onClick={() => handleTypeSelect("occupant")}
|
|
||||||
className="w-full h-14"
|
|
||||||
>
|
|
||||||
<Building className="mr-2 h-4 w-4" />
|
|
||||||
{translations.occupant[lang]}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters on the right (w-1/2) */}
|
{/* Action Buttons Component */}
|
||||||
<div className="w-1/2 flex flex-col space-y-4 pl-4">
|
<ActionButtonsComponent
|
||||||
<div className="font-medium text-sm mb-2 flex items-center">
|
onCreateClick={handleCreateClick}
|
||||||
<Filter className="mr-2 h-4 w-4" />
|
translations={translations}
|
||||||
{translations.filterSelection[lang]}
|
lang={lang}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
{/* Search input */}
|
{/* Sorting Component */}
|
||||||
<div className="w-full">
|
<SortingComponent
|
||||||
<label className="block text-xs font-medium mb-1">
|
sortField={sortField}
|
||||||
{translations.search[lang]}
|
sortDirection={sortDirection}
|
||||||
</label>
|
onSort={handleSort}
|
||||||
<div className="relative w-full">
|
translations={translations}
|
||||||
<Input
|
lang={lang}
|
||||||
placeholder={`${translations.search[lang]}...`}
|
/>
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
className="pl-8 w-full h-10"
|
|
||||||
/>
|
|
||||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Site URL dropdown */}
|
{/* Data Display Component */}
|
||||||
<div className="w-full">
|
<div className="mt-6">
|
||||||
<label className="block text-xs font-medium mb-1">
|
<DataDisplayComponent
|
||||||
{translations.siteUrl[lang]}
|
data={data}
|
||||||
</label>
|
loading={loading}
|
||||||
<div className="w-full">
|
error={error}
|
||||||
<Select value={selectedUrl} onValueChange={setSelectedUrl}>
|
onUpdateClick={handleUpdateClick}
|
||||||
<SelectTrigger className="w-full h-10">
|
translations={translations}
|
||||||
<SelectValue
|
lang={lang}
|
||||||
placeholder={`${translations.siteUrl[lang]}...`}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{urlOptions.map((url) => (
|
|
||||||
<SelectItem key={url} value={url}>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Link className="mr-2 h-3 w-3" />
|
|
||||||
{url}
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Grid of Application Cards */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{translations.availableApplications[lang]}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex flex-wrap -mx-2">{renderApplicationGrid()}</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Application Data Form */}
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Application Details</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-1">Name</label>
|
|
||||||
<Input
|
|
||||||
name="name"
|
|
||||||
value={applicationData.name}
|
|
||||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-1">
|
|
||||||
Application Code
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
name="application_code"
|
|
||||||
value={applicationData.application_code}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange("application_code", e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-1">Site URL</label>
|
|
||||||
<Input
|
|
||||||
name="site_url"
|
|
||||||
value={applicationData.site_url}
|
|
||||||
onChange={(e) => handleInputChange("site_url", e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium mb-1">
|
|
||||||
Application Type
|
|
||||||
</label>
|
|
||||||
<Select
|
|
||||||
value={applicationData.application_type}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
handleInputChange("application_type", value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select application type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="info">Info</SelectItem>
|
|
||||||
<SelectItem value="Dash">Dash</SelectItem>
|
|
||||||
<SelectItem value="Admin">Admin</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<label className="block text-sm font-medium mb-1">
|
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
name="description"
|
|
||||||
className="w-full min-h-[100px] rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm"
|
|
||||||
value={applicationData.description}
|
|
||||||
onChange={(e) => handleInputChange("description", e.target.value)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button>
|
|
||||||
<Save className="mr-2 h-4 w-4" />
|
|
||||||
Save Application
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Display current application data */}
|
{/* Pagination Tools Component */}
|
||||||
<Card>
|
<div className="mt-4">
|
||||||
<CardHeader>
|
<PaginationToolsComponent
|
||||||
<CardTitle>Current Application Data</CardTitle>
|
pagination={pagination}
|
||||||
</CardHeader>
|
updatePagination={updatePagination}
|
||||||
<CardContent>
|
loading={loading}
|
||||||
<pre className="bg-muted p-4 rounded-md overflow-auto">
|
lang={lang}
|
||||||
{JSON.stringify(applicationData, null, 2)}
|
/>
|
||||||
</pre>
|
</div>
|
||||||
</CardContent>
|
</>
|
||||||
</Card>
|
)}
|
||||||
|
|
||||||
|
{/* Form Mode - Show create/update/view form */}
|
||||||
|
{mode !== "list" && (
|
||||||
|
<FormComponent
|
||||||
|
initialData={selectedItem || undefined}
|
||||||
|
mode={mode}
|
||||||
|
refetch={refetch}
|
||||||
|
setMode={setMode}
|
||||||
|
setSelectedItem={setSelectedItem}
|
||||||
|
onCancel={() => setMode("list")}
|
||||||
|
lang={lang}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
import { listApplications } from "@/apicalls/application/application";
|
||||||
|
import { PagePagination } from "./hooks";
|
||||||
|
|
||||||
|
// Base schema with all possible fields
|
||||||
|
const ApplicationBaseSchema = z.object({
|
||||||
|
// Identification fields
|
||||||
|
uu_id: z.number().optional(),
|
||||||
|
name: z.string().min(1, "Name is required"),
|
||||||
|
application_code: z.string().min(1, "Application code is required"),
|
||||||
|
|
||||||
|
// Application details
|
||||||
|
site_url: z.string().min(1, "Site URL is required"),
|
||||||
|
application_type: z.string().min(1, "Application type is required"),
|
||||||
|
application_for: z.string().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
|
||||||
|
// Status fields
|
||||||
|
active: z.boolean().default(true),
|
||||||
|
deleted: z.boolean().default(false),
|
||||||
|
|
||||||
|
// System fields
|
||||||
|
created_at: z.string().optional(),
|
||||||
|
updated_at: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schema for creating a new application
|
||||||
|
export const CreateApplicationSchema = ApplicationBaseSchema.omit({
|
||||||
|
uu_id: true,
|
||||||
|
created_at: true,
|
||||||
|
updated_at: true,
|
||||||
|
deleted: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schema for updating an existing application
|
||||||
|
export const UpdateApplicationSchema = ApplicationBaseSchema.omit({
|
||||||
|
created_at: true,
|
||||||
|
updated_at: true,
|
||||||
|
deleted: true
|
||||||
|
}).required({
|
||||||
|
uu_id: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schema for viewing an application (all fields)
|
||||||
|
export const ViewApplicationSchema = ApplicationBaseSchema;
|
||||||
|
|
||||||
|
// Default schema (used for validation)
|
||||||
|
export const ApplicationSchema = ApplicationBaseSchema;
|
||||||
|
|
||||||
|
export type ApplicationFormData = z.infer<typeof ApplicationSchema>;
|
||||||
|
export type CreateApplicationFormData = z.infer<typeof CreateApplicationSchema>;
|
||||||
|
export type UpdateApplicationFormData = z.infer<typeof UpdateApplicationSchema>;
|
||||||
|
export type ViewApplicationFormData = z.infer<typeof ViewApplicationSchema>;
|
||||||
|
|
||||||
|
// Define field definition type
|
||||||
|
export interface FieldDefinition {
|
||||||
|
type: string;
|
||||||
|
group: string;
|
||||||
|
label: string;
|
||||||
|
options?: string[];
|
||||||
|
readOnly?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
defaultValue?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base field definitions with common properties
|
||||||
|
const baseFieldDefinitions: Record<string, FieldDefinition> = {
|
||||||
|
// Identification fields
|
||||||
|
uu_id: { type: "text", group: "identificationInfo", label: "UUID", readOnly: true, required: false },
|
||||||
|
name: { type: "text", group: "identificationInfo", label: "Name", readOnly: false, required: true },
|
||||||
|
application_code: { type: "text", group: "identificationInfo", label: "Application Code", readOnly: false, required: true },
|
||||||
|
|
||||||
|
// Application details
|
||||||
|
site_url: { type: "text", group: "applicationDetails", label: "Site URL", readOnly: false, required: true },
|
||||||
|
application_type: { type: "select", group: "applicationDetails", label: "Application Type", options: ["info", "Dash", "Admin"], readOnly: false, required: true },
|
||||||
|
application_for: { type: "select", group: "applicationDetails", label: "Application For", options: ["EMP", "OCC"], readOnly: false, required: false },
|
||||||
|
description: { type: "textarea", group: "applicationDetails", label: "Description", readOnly: false, required: false },
|
||||||
|
|
||||||
|
// Status fields
|
||||||
|
active: { type: "checkbox", group: "statusInfo", label: "Active", readOnly: false, required: false, defaultValue: true },
|
||||||
|
deleted: { type: "checkbox", group: "statusInfo", label: "Deleted", readOnly: true, required: false, defaultValue: false },
|
||||||
|
|
||||||
|
// System fields
|
||||||
|
created_at: { type: "date", group: "systemInfo", label: "Created At", readOnly: true, required: false },
|
||||||
|
updated_at: { type: "date", group: "systemInfo", label: "Updated At", readOnly: true, required: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Field definitions for create mode
|
||||||
|
export const createFieldDefinitions = {
|
||||||
|
name: { ...baseFieldDefinitions.name, readOnly: false, required: true, defaultValue: "" },
|
||||||
|
application_code: { ...baseFieldDefinitions.application_code, readOnly: false, required: true, defaultValue: "" },
|
||||||
|
site_url: { ...baseFieldDefinitions.site_url, readOnly: false, required: true, defaultValue: "" },
|
||||||
|
application_type: { ...baseFieldDefinitions.application_type, readOnly: false, required: true, defaultValue: "" },
|
||||||
|
application_for: { ...baseFieldDefinitions.application_for, readOnly: false, required: false, defaultValue: "EMP" },
|
||||||
|
description: { ...baseFieldDefinitions.description, readOnly: false, required: false, defaultValue: "" },
|
||||||
|
active: { ...baseFieldDefinitions.active, readOnly: false, required: false, defaultValue: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Field definitions for update mode
|
||||||
|
export const updateFieldDefinitions = {
|
||||||
|
uu_id: { ...baseFieldDefinitions.uu_id, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
name: { ...baseFieldDefinitions.name, readOnly: false, required: true, defaultValue: "" },
|
||||||
|
application_code: { ...baseFieldDefinitions.application_code, readOnly: false, required: true, defaultValue: "" },
|
||||||
|
site_url: { ...baseFieldDefinitions.site_url, readOnly: false, required: true, defaultValue: "" },
|
||||||
|
application_type: { ...baseFieldDefinitions.application_type, readOnly: false, required: true, defaultValue: "" },
|
||||||
|
application_for: { ...baseFieldDefinitions.application_for, readOnly: false, required: false, defaultValue: "" },
|
||||||
|
description: { ...baseFieldDefinitions.description, readOnly: false, required: false, defaultValue: "" },
|
||||||
|
active: { ...baseFieldDefinitions.active, readOnly: false, required: false, defaultValue: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Field definitions for view mode
|
||||||
|
export const viewFieldDefinitions = {
|
||||||
|
uu_id: { ...baseFieldDefinitions.uu_id, readOnly: true, required: false, defaultValue: 0 },
|
||||||
|
name: { ...baseFieldDefinitions.name, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
application_code: { ...baseFieldDefinitions.application_code, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
site_url: { ...baseFieldDefinitions.site_url, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
application_type: { ...baseFieldDefinitions.application_type, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
application_for: { ...baseFieldDefinitions.application_for, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
description: { ...baseFieldDefinitions.description, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
active: { ...baseFieldDefinitions.active, readOnly: true, required: false, defaultValue: true },
|
||||||
|
deleted: { ...baseFieldDefinitions.deleted, readOnly: true, required: false, defaultValue: false },
|
||||||
|
created_at: { ...baseFieldDefinitions.created_at, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
updated_at: { ...baseFieldDefinitions.updated_at, readOnly: true, required: false, defaultValue: "" },
|
||||||
|
};
|
||||||
|
|
||||||
|
// Combined field definitions for all modes
|
||||||
|
export const fieldDefinitions = {
|
||||||
|
...baseFieldDefinitions,
|
||||||
|
getDefinitionsByMode: (mode: "create" | "update" | "view") => {
|
||||||
|
switch (mode) {
|
||||||
|
case "create":
|
||||||
|
return createFieldDefinitions;
|
||||||
|
case "update":
|
||||||
|
return updateFieldDefinitions;
|
||||||
|
case "view":
|
||||||
|
return viewFieldDefinitions;
|
||||||
|
default:
|
||||||
|
return baseFieldDefinitions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fields to show based on mode - dynamically generated from field definitions
|
||||||
|
export const fieldsByMode = {
|
||||||
|
create: Object.keys(createFieldDefinitions),
|
||||||
|
update: Object.keys(updateFieldDefinitions),
|
||||||
|
view: Object.keys(viewFieldDefinitions),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchApplicationData = async ({
|
||||||
|
page = 1,
|
||||||
|
size = 10,
|
||||||
|
orderFields = ["name"],
|
||||||
|
orderTypes = ["asc"],
|
||||||
|
query = {},
|
||||||
|
}: {
|
||||||
|
page?: number;
|
||||||
|
size?: number;
|
||||||
|
orderFields?: string[];
|
||||||
|
orderTypes?: string[];
|
||||||
|
query?: Record<string, any>;
|
||||||
|
}) => {
|
||||||
|
// Call the actual API function
|
||||||
|
try {
|
||||||
|
const response = await listApplications({
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
orderField: orderFields,
|
||||||
|
orderType: orderTypes,
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: response.data,
|
||||||
|
pagination: response.pagination,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching application data:", error);
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
pagination: {
|
||||||
|
page,
|
||||||
|
size,
|
||||||
|
totalCount: 0,
|
||||||
|
totalItems: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
pageCount: 0,
|
||||||
|
orderField: orderFields || [],
|
||||||
|
orderType: orderTypes || [],
|
||||||
|
query: {},
|
||||||
|
} as PagePagination,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { LanguageTranslation } from "@/components/validations/translations/translation";
|
||||||
|
|
||||||
|
export interface ApplicationData {
|
||||||
|
id?: number;
|
||||||
|
name: string;
|
||||||
|
application_code: string;
|
||||||
|
site_url: string;
|
||||||
|
application_type: string;
|
||||||
|
application_for?: string;
|
||||||
|
description?: string;
|
||||||
|
active: boolean;
|
||||||
|
deleted?: boolean;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const translations: Record<string, LanguageTranslation> = {
|
||||||
|
typeSelection: {
|
||||||
|
en: "Type Selection",
|
||||||
|
tr: "Tür Seçimi",
|
||||||
|
},
|
||||||
|
filterSelection: {
|
||||||
|
en: "Filter Selection",
|
||||||
|
tr: "Filtre Seçimi",
|
||||||
|
},
|
||||||
|
employee: {
|
||||||
|
en: "Employee",
|
||||||
|
tr: "Çalışan",
|
||||||
|
},
|
||||||
|
occupant: {
|
||||||
|
en: "Occupant",
|
||||||
|
tr: "Sakin",
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
en: "Search",
|
||||||
|
tr: "Ara",
|
||||||
|
},
|
||||||
|
siteUrl: {
|
||||||
|
en: "Site URL",
|
||||||
|
tr: "Site URL",
|
||||||
|
},
|
||||||
|
applicationType: {
|
||||||
|
en: "Application Type",
|
||||||
|
tr: "Uygulama Türü",
|
||||||
|
},
|
||||||
|
availableApplications: {
|
||||||
|
en: "Available Applications",
|
||||||
|
tr: "Mevcut Uygulamalar",
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
en: "Code",
|
||||||
|
tr: "Kod",
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
en: "URL",
|
||||||
|
tr: "URL",
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
en: "Type",
|
||||||
|
tr: "Tür",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -110,6 +110,33 @@ services:
|
||||||
mem_limit: 512m
|
mem_limit: 512m
|
||||||
cpus: 0.5
|
cpus: 0.5
|
||||||
|
|
||||||
|
management_service:
|
||||||
|
container_name: management_service
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: ApiServices/ManagementService/Dockerfile
|
||||||
|
networks:
|
||||||
|
- wag-services
|
||||||
|
env_file:
|
||||||
|
- api_env.env
|
||||||
|
# depends_on:
|
||||||
|
# - initializer_service
|
||||||
|
environment:
|
||||||
|
- API_PATH=app:app
|
||||||
|
- API_HOST=0.0.0.0
|
||||||
|
- API_PORT=8004
|
||||||
|
- API_LOG_LEVEL=info
|
||||||
|
- API_RELOAD=1
|
||||||
|
- API_APP_NAME=evyos-management-api-gateway
|
||||||
|
- API_TITLE=WAG API Management Api Gateway
|
||||||
|
- API_FORGOT_LINK=https://management_service/forgot-password
|
||||||
|
- API_DESCRIPTION=This api is serves as web management api gateway only to evyos web services.
|
||||||
|
- API_APP_URL=https://management_service
|
||||||
|
ports:
|
||||||
|
- "8004:8004"
|
||||||
|
mem_limit: 512m
|
||||||
|
cpus: 0.5
|
||||||
|
|
||||||
initializer_service:
|
initializer_service:
|
||||||
container_name: initializer_service
|
container_name: initializer_service
|
||||||
build:
|
build:
|
||||||
|
|
@ -138,8 +165,6 @@ services:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
wag-services:
|
wag-services:
|
||||||
# client-frontend:
|
|
||||||
# management-frontend:
|
|
||||||
|
|
||||||
# template_service:
|
# template_service:
|
||||||
# container_name: template_service
|
# container_name: template_service
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue