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,
|
||||
including pagination, ordering, and complex query building.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import arrow
|
||||
|
||||
from typing import Any, TypeVar, Type, Union, Optional
|
||||
|
|
@ -23,7 +23,7 @@ T = TypeVar("T", bound="QueryModel")
|
|||
class QueryModel:
|
||||
|
||||
__abstract__ = True
|
||||
pre_query = None
|
||||
pre_query: Optional[Query] = None
|
||||
|
||||
@classmethod
|
||||
def _query(cls: Type[T], db: Session) -> Query:
|
||||
|
|
@ -149,11 +149,29 @@ class QueryModel:
|
|||
Returns:
|
||||
Tuple of SQLAlchemy filter expressions or None if validation fails
|
||||
"""
|
||||
if validate_model is not None:
|
||||
# Add validation logic here if needed
|
||||
pass
|
||||
|
||||
try:
|
||||
# Let SQLAlchemy handle the validation by attempting to create the filter expressions
|
||||
return tuple(cls.filter_expr(**smart_options))
|
||||
except Exception as e:
|
||||
# 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
|
||||
def filter_by_one(
|
||||
|
|
|
|||
|
|
@ -122,8 +122,6 @@ class EndpointResponse(BaseModel):
|
|||
def response(self):
|
||||
"""Convert response to dictionary format."""
|
||||
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)
|
||||
if not result_pagination:
|
||||
raise ValueError("Invalid pagination result pagination.")
|
||||
|
|
|
|||
|
|
@ -343,7 +343,7 @@ class BuildParts(CrudCollection):
|
|||
)
|
||||
|
||||
__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"},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
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(
|
||||
// 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";
|
||||
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 { 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",
|
||||
},
|
||||
};
|
||||
import { PageProps } from "@/components/validations/translations/translation";
|
||||
import { useApplicationData } from "./hooks";
|
||||
import { ApplicationData, translations } from "./types";
|
||||
import { SearchComponent } from "./SearchComponent";
|
||||
import { ActionButtonsComponent } from "./ActionButtonsComponent";
|
||||
import { SortingComponent } from "./SortingComponent";
|
||||
import { PaginationToolsComponent } from "./PaginationToolsComponent";
|
||||
import { DataDisplayComponent } from "./DataDisplayComponent";
|
||||
import { FormComponent } from "./FormComponent";
|
||||
|
||||
const ApplicationPage: React.FC<PageProps> = ({ lang = "en" }) => {
|
||||
const [selectedType, setSelectedType] = useState<"employee" | "occupant">(
|
||||
"employee"
|
||||
// Use the custom hook for paginated data
|
||||
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
|
||||
const urlOptions = [
|
||||
|
|
@ -94,275 +35,115 @@ const ApplicationPage: React.FC<PageProps> = ({ lang = "en" }) => {
|
|||
"/settings",
|
||||
"/reports",
|
||||
];
|
||||
const typeOptions = ["info", "Dash", "Admin"];
|
||||
|
||||
// Handle selection button click
|
||||
const handleTypeSelect = (type: "employee" | "occupant") => {
|
||||
setSelectedType(type);
|
||||
const handleUpdateClick = (item: ApplicationData) => {
|
||||
setSelectedItem(item);
|
||||
setMode("update");
|
||||
};
|
||||
|
||||
// Handle application data input changes
|
||||
const handleInputChange = (name: string, value: string) => {
|
||||
setApplicationData({
|
||||
...applicationData,
|
||||
[name]: value,
|
||||
// Function to handle the create button click
|
||||
const handleCreateClick = () => {
|
||||
setSelectedItem(null);
|
||||
setMode("create");
|
||||
};
|
||||
|
||||
// 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
|
||||
const sampleApplications = [
|
||||
{
|
||||
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",
|
||||
},
|
||||
];
|
||||
// Handle sorting
|
||||
const handleSort = (field: string) => {
|
||||
let direction: "asc" | "desc" | null = "asc";
|
||||
|
||||
// Generate grid of application cards
|
||||
const renderApplicationGrid = () => {
|
||||
return sampleApplications.map((app, index) => (
|
||||
<div key={index} className="w-full sm:w-1/5 p-1">
|
||||
<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[lang]}:
|
||||
</span>
|
||||
<span className="truncate">{app.application_code}</span>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<span className="text-muted-foreground w-10">
|
||||
{translations.url[lang]}:
|
||||
</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>
|
||||
));
|
||||
if (sortField === field) {
|
||||
// Toggle direction if same field is clicked
|
||||
if (sortDirection === "asc") {
|
||||
direction = "desc";
|
||||
} else if (sortDirection === "desc") {
|
||||
// Clear sorting if already desc
|
||||
field = "";
|
||||
direction = null;
|
||||
}
|
||||
}
|
||||
|
||||
setSortField(field || null);
|
||||
setSortDirection(direction);
|
||||
|
||||
updatePagination({
|
||||
orderField: field ? [field] : [],
|
||||
orderType: direction ? [direction] : [],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4 space-y-6">
|
||||
{/* Selection Buttons */}
|
||||
<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[lang]}
|
||||
</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) */}
|
||||
<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[lang]}
|
||||
</div>
|
||||
|
||||
{/* Search input */}
|
||||
<div className="w-full">
|
||||
<label className="block text-xs font-medium mb-1">
|
||||
{translations.search[lang]}
|
||||
</label>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
placeholder={`${translations.search[lang]}...`}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-8 w-full h-10"
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
{/* List Mode - Show search, actions, and data display */}
|
||||
{mode === "list" && (
|
||||
<>
|
||||
{/* Search Component */}
|
||||
<SearchComponent
|
||||
onSearch={handleSearch}
|
||||
translations={translations}
|
||||
lang={lang}
|
||||
urlOptions={urlOptions}
|
||||
/>
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Site URL dropdown */}
|
||||
<div className="w-full">
|
||||
<label className="block text-xs font-medium mb-1">
|
||||
{translations.siteUrl[lang]}
|
||||
</label>
|
||||
<div className="w-full">
|
||||
<Select value={selectedUrl} onValueChange={setSelectedUrl}>
|
||||
<SelectTrigger className="w-full h-10">
|
||||
<SelectValue
|
||||
placeholder={`${translations.siteUrl[lang]}...`}
|
||||
{/* Action Buttons Component */}
|
||||
<ActionButtonsComponent
|
||||
onCreateClick={handleCreateClick}
|
||||
translations={translations}
|
||||
lang={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>
|
||||
{/* Sorting Component */}
|
||||
<SortingComponent
|
||||
sortField={sortField}
|
||||
sortDirection={sortDirection}
|
||||
onSort={handleSort}
|
||||
translations={translations}
|
||||
lang={lang}
|
||||
/>
|
||||
|
||||
{/* 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)}
|
||||
{/* Data Display Component */}
|
||||
<div className="mt-6">
|
||||
<DataDisplayComponent
|
||||
data={data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onUpdateClick={handleUpdateClick}
|
||||
translations={translations}
|
||||
lang={lang}
|
||||
/>
|
||||
</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 className="flex justify-end">
|
||||
<Button>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
Save Application
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Display current application data */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Current Application Data</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="bg-muted p-4 rounded-md overflow-auto">
|
||||
{JSON.stringify(applicationData, null, 2)}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* Pagination Tools Component */}
|
||||
<div className="mt-4">
|
||||
<PaginationToolsComponent
|
||||
pagination={pagination}
|
||||
updatePagination={updatePagination}
|
||||
loading={loading}
|
||||
lang={lang}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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:
|
||||
container_name: initializer_service
|
||||
build:
|
||||
|
|
@ -138,8 +165,6 @@ services:
|
|||
|
||||
networks:
|
||||
wag-services:
|
||||
# client-frontend:
|
||||
# management-frontend:
|
||||
|
||||
# template_service:
|
||||
# container_name: template_service
|
||||
|
|
|
|||
Loading…
Reference in New Issue