From ac344773c5deda18a0cb07e7ce9c08b35f4e132c Mon Sep 17 00:00:00 2001 From: berkay Date: Mon, 28 Apr 2025 02:30:06 +0300 Subject: [PATCH] added application page --- ApiServices/ManagementService/Dockerfile | 29 ++ .../ManagementService/Endpoints/__init__.py | 1 + .../Endpoints/application/route.py | 156 ++++++ .../ManagementService/Endpoints/routes.py | 17 + .../ManagementService/Events/__init__.py | 3 + ApiServices/ManagementService/Events/a.txt | 0 ApiServices/ManagementService/README.md | 0 .../ManagementService/Validations/__init__.py | 1 + .../Validations/application/validations.py | 12 + ApiServices/ManagementService/__init__.py | 13 + Controllers/Postgres/filter.py | 32 +- Controllers/Postgres/response.py | 2 - Schemas/building/build.py | 2 +- .../src/apicalls/application/application.tsx | 100 ++++ .../src/apicalls/basics.ts | 4 + .../application/ActionButtonsComponent.tsx | 26 + .../application/DataDisplayComponent.tsx | 85 ++++ .../Pages/application/FormComponent.tsx | 359 ++++++++++++++ .../application/PaginationToolsComponent.tsx | 149 ++++++ .../Pages/application/SearchComponent.tsx | 160 +++++++ .../Pages/application/SortingComponent.tsx | 57 +++ .../src/components/Pages/application/hooks.ts | 156 ++++++ .../components/Pages/application/language.ts | 42 ++ .../src/components/Pages/application/page.tsx | 451 +++++------------- .../components/Pages/application/schema.ts | 194 ++++++++ .../src/components/Pages/application/types.ts | 63 +++ docker-compose.yml | 29 +- 27 files changed, 1796 insertions(+), 347 deletions(-) create mode 100644 ApiServices/ManagementService/Dockerfile create mode 100644 ApiServices/ManagementService/Endpoints/__init__.py create mode 100644 ApiServices/ManagementService/Endpoints/application/route.py create mode 100644 ApiServices/ManagementService/Endpoints/routes.py create mode 100644 ApiServices/ManagementService/Events/__init__.py create mode 100644 ApiServices/ManagementService/Events/a.txt create mode 100644 ApiServices/ManagementService/README.md create mode 100644 ApiServices/ManagementService/Validations/__init__.py create mode 100644 ApiServices/ManagementService/Validations/application/validations.py create mode 100644 ApiServices/ManagementService/__init__.py create mode 100644 WebServices/management-frontend/src/apicalls/application/application.tsx create mode 100644 WebServices/management-frontend/src/components/Pages/application/ActionButtonsComponent.tsx create mode 100644 WebServices/management-frontend/src/components/Pages/application/DataDisplayComponent.tsx create mode 100644 WebServices/management-frontend/src/components/Pages/application/FormComponent.tsx create mode 100644 WebServices/management-frontend/src/components/Pages/application/PaginationToolsComponent.tsx create mode 100644 WebServices/management-frontend/src/components/Pages/application/SearchComponent.tsx create mode 100644 WebServices/management-frontend/src/components/Pages/application/SortingComponent.tsx create mode 100644 WebServices/management-frontend/src/components/Pages/application/hooks.ts create mode 100644 WebServices/management-frontend/src/components/Pages/application/language.ts create mode 100644 WebServices/management-frontend/src/components/Pages/application/schema.ts create mode 100644 WebServices/management-frontend/src/components/Pages/application/types.ts diff --git a/ApiServices/ManagementService/Dockerfile b/ApiServices/ManagementService/Dockerfile new file mode 100644 index 0000000..1f906df --- /dev/null +++ b/ApiServices/ManagementService/Dockerfile @@ -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"] diff --git a/ApiServices/ManagementService/Endpoints/__init__.py b/ApiServices/ManagementService/Endpoints/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/ApiServices/ManagementService/Endpoints/__init__.py @@ -0,0 +1 @@ + diff --git a/ApiServices/ManagementService/Endpoints/application/route.py b/ApiServices/ManagementService/Endpoints/application/route.py new file mode 100644 index 0000000..5309a10 --- /dev/null +++ b/ApiServices/ManagementService/Endpoints/application/route.py @@ -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 diff --git a/ApiServices/ManagementService/Endpoints/routes.py b/ApiServices/ManagementService/Endpoints/routes.py new file mode 100644 index 0000000..f9cd2d0 --- /dev/null +++ b/ApiServices/ManagementService/Endpoints/routes.py @@ -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"), + ] diff --git a/ApiServices/ManagementService/Events/__init__.py b/ApiServices/ManagementService/Events/__init__.py new file mode 100644 index 0000000..9015d41 --- /dev/null +++ b/ApiServices/ManagementService/Events/__init__.py @@ -0,0 +1,3 @@ + + +__all__ = [] \ No newline at end of file diff --git a/ApiServices/ManagementService/Events/a.txt b/ApiServices/ManagementService/Events/a.txt new file mode 100644 index 0000000..e69de29 diff --git a/ApiServices/ManagementService/README.md b/ApiServices/ManagementService/README.md new file mode 100644 index 0000000..e69de29 diff --git a/ApiServices/ManagementService/Validations/__init__.py b/ApiServices/ManagementService/Validations/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/ApiServices/ManagementService/Validations/__init__.py @@ -0,0 +1 @@ + diff --git a/ApiServices/ManagementService/Validations/application/validations.py b/ApiServices/ManagementService/Validations/application/validations.py new file mode 100644 index 0000000..b365e92 --- /dev/null +++ b/ApiServices/ManagementService/Validations/application/validations.py @@ -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") + diff --git a/ApiServices/ManagementService/__init__.py b/ApiServices/ManagementService/__init__.py new file mode 100644 index 0000000..f58d9b1 --- /dev/null +++ b/ApiServices/ManagementService/__init__.py @@ -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"] diff --git a/Controllers/Postgres/filter.py b/Controllers/Postgres/filter.py index e41a017..19c877f 100644 --- a/Controllers/Postgres/filter.py +++ b/Controllers/Postgres/filter.py @@ -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 - - return tuple(cls.filter_expr(**smart_options)) + 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( diff --git a/Controllers/Postgres/response.py b/Controllers/Postgres/response.py index f8fe4fc..3a8a84b 100644 --- a/Controllers/Postgres/response.py +++ b/Controllers/Postgres/response.py @@ -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.") diff --git a/Schemas/building/build.py b/Schemas/building/build.py index a41c288..bddfd1b 100644 --- a/Schemas/building/build.py +++ b/Schemas/building/build.py @@ -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"}, ) diff --git a/WebServices/management-frontend/src/apicalls/application/application.tsx b/WebServices/management-frontend/src/apicalls/application/application.tsx new file mode 100644 index 0000000..62be14f --- /dev/null +++ b/WebServices/management-frontend/src/apicalls/application/application.tsx @@ -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, +}; diff --git a/WebServices/management-frontend/src/apicalls/basics.ts b/WebServices/management-frontend/src/apicalls/basics.ts index 46f4d58..9503f59 100644 --- a/WebServices/management-frontend/src/apicalls/basics.ts +++ b/WebServices/management-frontend/src/apicalls/basics.ts @@ -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" // ); diff --git a/WebServices/management-frontend/src/components/Pages/application/ActionButtonsComponent.tsx b/WebServices/management-frontend/src/components/Pages/application/ActionButtonsComponent.tsx new file mode 100644 index 0000000..9cee034 --- /dev/null +++ b/WebServices/management-frontend/src/components/Pages/application/ActionButtonsComponent.tsx @@ -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; + lang: string; +} + +export const ActionButtonsComponent: React.FC = ({ + onCreateClick, + translations, + lang, +}) => { + return ( +
+ +
+ ); +}; diff --git a/WebServices/management-frontend/src/components/Pages/application/DataDisplayComponent.tsx b/WebServices/management-frontend/src/components/Pages/application/DataDisplayComponent.tsx new file mode 100644 index 0000000..7457bc5 --- /dev/null +++ b/WebServices/management-frontend/src/components/Pages/application/DataDisplayComponent.tsx @@ -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; + lang: string; +} + +export const DataDisplayComponent: React.FC = ({ + data, + loading, + error, + onUpdateClick, + translations, + lang, +}) => { + if (loading) { + return ( +
Loading applications...
+ ); + } + + if (error) { + return ( +
+ Error loading applications: {error.message} +
+ ); + } + + if (data.length === 0) { + return
No applications found
; + } + + return ( +
+ {data.map((app, index) => ( +
onUpdateClick(app)} + > + + +
{app.name}
+
+
+ + {translations.code && + translations.code[lang as keyof LanguageTranslation]} + : + + {app.application_code} +
+
+ + {translations.url && + translations.url[lang as keyof LanguageTranslation]} + : + + {app.site_url} +
+
+ + {translations.type && + translations.type[lang as keyof LanguageTranslation]} + : + + {app.application_type} +
+
+
+
+
+ ))} +
+ ); +}; diff --git a/WebServices/management-frontend/src/components/Pages/application/FormComponent.tsx b/WebServices/management-frontend/src/components/Pages/application/FormComponent.tsx new file mode 100644 index 0000000..dc71a54 --- /dev/null +++ b/WebServices/management-frontend/src/components/Pages/application/FormComponent.tsx @@ -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 = ({ + 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( + getInitialFormData() + ); + + const [validationErrors, setValidationErrors] = useState( + {} + ); + 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 ( +
+ + + {fieldDef.type === "text" && !fieldDef.readOnly && ( + <> + handleInputChange(fieldName, e.target.value)} + className={hasError ? "border-red-500" : ""} + /> + + )} + {fieldDef.type === "text" && fieldDef.readOnly && ( +

+ {(formData[fieldName as keyof ApplicationFormData] as string) || ""} +

+ )} + + {fieldDef.type === "textarea" && !fieldDef.readOnly && ( +