added application page

This commit is contained in:
berkay 2025-04-28 02:30:06 +03:00
parent 346b132f4c
commit ac344773c5
27 changed files with 1796 additions and 347 deletions

View File

@ -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"]

View File

@ -0,0 +1 @@

View File

@ -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

View File

@ -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"),
]

View File

@ -0,0 +1,3 @@
__all__ = []

View File

View File

@ -0,0 +1 @@

View File

@ -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")

View File

@ -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"]

View File

@ -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(

View File

@ -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.")

View File

@ -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"},
)

View File

@ -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,
};

View File

@ -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"
// );

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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,
};
}

View File

@ -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];
}

View File

@ -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>
);
};

View File

@ -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,
};
}
};

View File

@ -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",
},
};

View File

@ -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