updated service binders updated
This commit is contained in:
@@ -17,7 +17,21 @@ def init_service_to_event_matches_for_super_user(super_user, db_session=None) ->
|
|||||||
db=db_session,
|
db=db_session,
|
||||||
).data
|
).data
|
||||||
list_of_all_events = Events.filter_all(db=db_session).data
|
list_of_all_events = Events.filter_all(db=db_session).data
|
||||||
|
Service2Events.filter_all(db=db_session).query.delete()
|
||||||
|
Service2Events.save(db=db_session)
|
||||||
|
|
||||||
for list_of_event_code in list_of_all_events:
|
for list_of_event_code in list_of_all_events:
|
||||||
|
service_to_event_found = Service2Events.filter_one_system(
|
||||||
|
Service2Events.event_id == list_of_event_code.id,
|
||||||
|
Service2Events.service_id == service_match.id,
|
||||||
|
db=db_session,
|
||||||
|
)
|
||||||
|
if service_to_event_found.data:
|
||||||
|
service_to_event_found.destroy(db=db_session)
|
||||||
|
print(
|
||||||
|
f"UUID: {service_to_event_found.uu_id} event is deleted from {service_match.uu_id}"
|
||||||
|
)
|
||||||
|
|
||||||
created_service = Service2Events.find_or_create(
|
created_service = Service2Events.find_or_create(
|
||||||
service_id=service_match.id,
|
service_id=service_match.id,
|
||||||
service_uu_id=str(service_match.uu_id),
|
service_uu_id=str(service_match.uu_id),
|
||||||
@@ -33,6 +47,7 @@ def init_service_to_event_matches_for_super_user(super_user, db_session=None) ->
|
|||||||
f"UUID: {created_service.uu_id} event is saved to {service_match.uu_id}"
|
f"UUID: {created_service.uu_id} event is saved to {service_match.uu_id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
employee_added_service = Event2Employee.find_or_create(
|
employee_added_service = Event2Employee.find_or_create(
|
||||||
event_service_id=service_match.id,
|
event_service_id=service_match.id,
|
||||||
event_service_uu_id=str(service_match.uu_id),
|
event_service_uu_id=str(service_match.uu_id),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from ApiControllers.providers.token_provider import TokenProvider
|
|||||||
|
|
||||||
from Controllers.Postgres.pagination import PaginateOnly, Pagination, PaginationResult
|
from Controllers.Postgres.pagination import PaginateOnly, Pagination, PaginationResult
|
||||||
from Controllers.Postgres.response import EndpointResponse
|
from Controllers.Postgres.response import EndpointResponse
|
||||||
|
from Events.service_endpoints.cluster import ServiceEndpointRouterCluster
|
||||||
|
|
||||||
|
|
||||||
# Create API router
|
# Create API router
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ def application_list_callable(list_options: PaginateOnly):
|
|||||||
data=applications_list,
|
data=applications_list,
|
||||||
pagination=pagination,
|
pagination=pagination,
|
||||||
# response_model="",
|
# response_model="",
|
||||||
).pagination.as_dict
|
)
|
||||||
return EndpointResponse(
|
return EndpointResponse(
|
||||||
message="MSG0003-LIST",
|
message="MSG0003-LIST",
|
||||||
pagination_result=pagination_result,
|
pagination_result=pagination_result,
|
||||||
|
|||||||
@@ -14,29 +14,21 @@ ServiceEndpointListEvent = Event(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def service_endpoint_list_callable(list_options: PaginateOnly):
|
def service_endpoint_list_callable(data: PaginateOnly):
|
||||||
"""
|
"""
|
||||||
Example callable method
|
List services endpoint callable method
|
||||||
"""
|
"""
|
||||||
list_options = PaginateOnly(**list_options.model_dump())
|
list_options = PaginateOnly(**data.model_dump())
|
||||||
with Services.new_session() as db_session:
|
with Services.new_session() as db_session:
|
||||||
if list_options.query:
|
if data.query:
|
||||||
services_list = Services.filter_all(
|
services_list = Services.filter_all_system(*Services.convert(data.query), db=db_session)
|
||||||
*Services.convert(list_options.query), db=db_session
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
services_list = Services.filter_all(db=db_session)
|
services_list = Services.filter_all_system(db=db_session)
|
||||||
pagination = Pagination(data=services_list)
|
pagination = Pagination(data=services_list)
|
||||||
pagination.change(**list_options.model_dump())
|
pagination.change(**data.model_dump())
|
||||||
pagination_result = PaginationResult(
|
pagination_result = PaginationResult(data=services_list, pagination=pagination)
|
||||||
data=services_list,
|
print("service pagination_result", pagination_result)
|
||||||
pagination=pagination,
|
return EndpointResponse(message="MSG0003-LIST", pagination_result=pagination_result).response
|
||||||
# response_model="",
|
|
||||||
).pagination.as_dict
|
|
||||||
return EndpointResponse(
|
|
||||||
message="MSG0003-LIST",
|
|
||||||
pagination_result=pagination_result,
|
|
||||||
).response
|
|
||||||
|
|
||||||
|
|
||||||
ServiceEndpointListEvent.event_callable = service_endpoint_list_callable
|
ServiceEndpointListEvent.event_callable = service_endpoint_list_callable
|
||||||
|
|||||||
@@ -121,7 +121,19 @@ class EndpointResponse(BaseModel):
|
|||||||
@property
|
@property
|
||||||
def response(self):
|
def response(self):
|
||||||
"""Convert response to dictionary format."""
|
"""Convert response to dictionary format."""
|
||||||
resutl_data = getattr(self.pagination_result, "data", None)
|
result_data = getattr(self.pagination_result, "data", None)
|
||||||
|
if not result_data:
|
||||||
|
return {
|
||||||
|
"completed": False,
|
||||||
|
"message": "MSG0004-NODATA",
|
||||||
|
"data": None,
|
||||||
|
"pagination": {
|
||||||
|
"page": 1,
|
||||||
|
"size": 10,
|
||||||
|
"total_count": 0,
|
||||||
|
"total_pages": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
result_pagination = getattr(self.pagination_result, "pagination", None)
|
result_pagination = getattr(self.pagination_result, "pagination", None)
|
||||||
if not result_pagination:
|
if not result_pagination:
|
||||||
raise ValueError("Invalid pagination result pagination.")
|
raise ValueError("Invalid pagination result pagination.")
|
||||||
@@ -131,7 +143,7 @@ class EndpointResponse(BaseModel):
|
|||||||
return {
|
return {
|
||||||
"completed": self.completed,
|
"completed": self.completed,
|
||||||
"message": self.message,
|
"message": self.message,
|
||||||
"data": resutl_data,
|
"data": result_data,
|
||||||
"pagination": pagination_dict,
|
"pagination": pagination_dict,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -295,14 +295,17 @@ class Event2Employee(CrudCollection):
|
|||||||
db=db,
|
db=db,
|
||||||
).data
|
).data
|
||||||
service_ids = list(set([event.event_service_id for event in employee_events]))
|
service_ids = list(set([event.event_service_id for event in employee_events]))
|
||||||
|
print("service_ids", service_ids)
|
||||||
active_event_ids = Service2Events.filter_all(
|
active_event_ids = Service2Events.filter_all(
|
||||||
Service2Events.service_id.in_(service_ids),
|
Service2Events.service_id.in_(service_ids),
|
||||||
db=db,
|
db=db,
|
||||||
).data
|
).data
|
||||||
|
print("active_event_ids", active_event_ids)
|
||||||
active_events = Events.filter_all(
|
active_events = Events.filter_all(
|
||||||
Events.id.in_([event.event_id for event in active_event_ids]),
|
Events.id.in_([event.event_id for event in active_event_ids]),
|
||||||
db=db,
|
db=db,
|
||||||
).data
|
).data
|
||||||
|
print("active_events", active_events)
|
||||||
if extra_events := Event2EmployeeExtra.filter_all(
|
if extra_events := Event2EmployeeExtra.filter_all(
|
||||||
Event2EmployeeExtra.employee_id == employee_id,
|
Event2EmployeeExtra.employee_id == employee_id,
|
||||||
db=db,
|
db=db,
|
||||||
@@ -318,6 +321,7 @@ class Event2Employee(CrudCollection):
|
|||||||
events_dict[str(event.endpoint_code)] = str(event.function_code)
|
events_dict[str(event.endpoint_code)] = str(event.function_code)
|
||||||
else:
|
else:
|
||||||
ValueError("Duplicate event code found for single endpoint")
|
ValueError("Duplicate event code found for single endpoint")
|
||||||
|
print("events_dict", events_dict)
|
||||||
return events_dict
|
return events_dict
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Plus } from "lucide-react";
|
|
||||||
|
|
||||||
interface CreateButtonProps {
|
|
||||||
onClick: () => void;
|
|
||||||
translations: Record<string, any>;
|
|
||||||
lang: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CreateButton: React.FC<CreateButtonProps> = ({
|
|
||||||
onClick,
|
|
||||||
translations,
|
|
||||||
lang,
|
|
||||||
}) => {
|
|
||||||
const t = translations[lang] || {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button onClick={onClick} className="flex items-center">
|
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
|
||||||
{t.create || "Create"}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { CustomButton } from "./types";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
|
|
||||||
interface CustomButtonComponentProps {
|
|
||||||
button: CustomButton;
|
|
||||||
isSelected: boolean;
|
|
||||||
onClick: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CustomButtonComponent: React.FC<CustomButtonComponentProps> = ({
|
|
||||||
button,
|
|
||||||
isSelected,
|
|
||||||
onClick,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
onClick={onClick}
|
|
||||||
variant={button.variant || "default"}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center",
|
|
||||||
isSelected && "ring-2 ring-primary ring-offset-2"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{button.icon && <span className="mr-2">{button.icon}</span>}
|
|
||||||
{button.label}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * from './CreateButton';
|
|
||||||
export * from './CustomButtonComponent';
|
|
||||||
export * from './types';
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { ReactNode } from "react";
|
|
||||||
|
|
||||||
export interface CustomButton {
|
|
||||||
id: string;
|
|
||||||
label: string;
|
|
||||||
onClick: () => void;
|
|
||||||
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
|
|
||||||
icon?: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ActionButtonsProps {
|
|
||||||
onCreateClick: () => void;
|
|
||||||
translations: Record<string, any>;
|
|
||||||
lang: string;
|
|
||||||
customButtons?: CustomButton[];
|
|
||||||
defaultSelectedButtonId?: string;
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import { CardItem } from "./CardItem";
|
|
||||||
import { CardSkeleton } from "./CardSkeleton";
|
|
||||||
import { getFieldValue, getGridClasses } from "./utils";
|
|
||||||
import { CardDisplayProps } from "./schema";
|
|
||||||
|
|
||||||
export function CardDisplay<T>({
|
|
||||||
showFields,
|
|
||||||
data,
|
|
||||||
lang,
|
|
||||||
translations,
|
|
||||||
error,
|
|
||||||
loading,
|
|
||||||
titleField = "name",
|
|
||||||
onCardClick,
|
|
||||||
renderCustomField,
|
|
||||||
gridCols = 4,
|
|
||||||
showViewIcon = false,
|
|
||||||
showUpdateIcon = false,
|
|
||||||
onViewClick,
|
|
||||||
onUpdateClick,
|
|
||||||
}: CardDisplayProps<T>) {
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className="p-6 text-center text-red-500">
|
|
||||||
{error.message || "An error occurred while fetching data."}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={getGridClasses(gridCols)}>
|
|
||||||
{loading ? (
|
|
||||||
// Loading skeletons
|
|
||||||
Array.from({ length: 10 }).map((_, index) => (
|
|
||||||
<CardSkeleton
|
|
||||||
key={`loading-${index}`}
|
|
||||||
index={index}
|
|
||||||
showFields={showFields}
|
|
||||||
showViewIcon={showViewIcon}
|
|
||||||
showUpdateIcon={showUpdateIcon}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
) : data.length === 0 ? (
|
|
||||||
<div className="col-span-full text-center py-6">
|
|
||||||
{(translations[lang] || {}).noData || "No data found"}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
data.map((item, index) => (
|
|
||||||
<CardItem
|
|
||||||
key={index}
|
|
||||||
item={item}
|
|
||||||
index={index}
|
|
||||||
showFields={showFields}
|
|
||||||
titleField={titleField}
|
|
||||||
lang={lang}
|
|
||||||
translations={translations}
|
|
||||||
onCardClick={onCardClick}
|
|
||||||
renderCustomField={renderCustomField}
|
|
||||||
showViewIcon={showViewIcon}
|
|
||||||
showUpdateIcon={showUpdateIcon}
|
|
||||||
onViewClick={onViewClick}
|
|
||||||
onUpdateClick={onUpdateClick}
|
|
||||||
getFieldValue={getFieldValue}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Eye, Edit } from "lucide-react";
|
|
||||||
import { CardItemProps, CardActionsProps, CardFieldProps } from "./schema";
|
|
||||||
|
|
||||||
export function CardItem<T>({
|
|
||||||
item,
|
|
||||||
index,
|
|
||||||
showFields,
|
|
||||||
titleField,
|
|
||||||
lang,
|
|
||||||
translations,
|
|
||||||
onCardClick,
|
|
||||||
renderCustomField,
|
|
||||||
showViewIcon,
|
|
||||||
showUpdateIcon,
|
|
||||||
onViewClick,
|
|
||||||
onUpdateClick,
|
|
||||||
getFieldValue,
|
|
||||||
}: CardItemProps<T>) {
|
|
||||||
return (
|
|
||||||
<div key={index} className="w-full p-1">
|
|
||||||
<Card
|
|
||||||
className={`h-full ${onCardClick ? 'cursor-pointer hover:shadow-md transition-shadow' : ''}`}
|
|
||||||
onClick={onCardClick ? () => onCardClick(item) : undefined}
|
|
||||||
>
|
|
||||||
<CardHeader className="p-3 pb-0 flex justify-between items-start">
|
|
||||||
<h3 className="text-lg font-semibold">
|
|
||||||
{getFieldValue(item, titleField)}
|
|
||||||
</h3>
|
|
||||||
<CardActions
|
|
||||||
item={item}
|
|
||||||
showViewIcon={showViewIcon}
|
|
||||||
showUpdateIcon={showUpdateIcon}
|
|
||||||
onViewClick={onViewClick}
|
|
||||||
onUpdateClick={onUpdateClick}
|
|
||||||
/>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-3">
|
|
||||||
<div className="space-y-2">
|
|
||||||
{showFields.map((field) => (
|
|
||||||
<CardField
|
|
||||||
key={`${index}-${field}`}
|
|
||||||
item={item}
|
|
||||||
field={field}
|
|
||||||
lang={lang}
|
|
||||||
translations={translations}
|
|
||||||
renderCustomField={renderCustomField}
|
|
||||||
getFieldValue={getFieldValue}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interface moved to schema.ts
|
|
||||||
|
|
||||||
function CardActions<T>({
|
|
||||||
item,
|
|
||||||
showViewIcon,
|
|
||||||
showUpdateIcon,
|
|
||||||
onViewClick,
|
|
||||||
onUpdateClick,
|
|
||||||
}: CardActionsProps<T>) {
|
|
||||||
if (!showViewIcon && !showUpdateIcon) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex space-x-1">
|
|
||||||
{showViewIcon && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (onViewClick) onViewClick(item);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{showUpdateIcon && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (onUpdateClick) onUpdateClick(item);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Edit className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Interface moved to schema.ts
|
|
||||||
|
|
||||||
function CardField<T>({
|
|
||||||
item,
|
|
||||||
field,
|
|
||||||
lang,
|
|
||||||
translations,
|
|
||||||
renderCustomField,
|
|
||||||
getFieldValue,
|
|
||||||
}: CardFieldProps<T>) {
|
|
||||||
return (
|
|
||||||
<div className="flex">
|
|
||||||
<span className="font-medium mr-2 min-w-[80px]">
|
|
||||||
{translations[field]?.[lang] || field}:
|
|
||||||
</span>
|
|
||||||
<span className="flex-1">
|
|
||||||
{renderCustomField
|
|
||||||
? renderCustomField(item, field)
|
|
||||||
: getFieldValue(item, field)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
|
||||||
import { CardSkeletonProps } from "./schema";
|
|
||||||
|
|
||||||
// Interface moved to schema.ts
|
|
||||||
|
|
||||||
export function CardSkeleton({
|
|
||||||
index,
|
|
||||||
showFields,
|
|
||||||
showViewIcon,
|
|
||||||
showUpdateIcon,
|
|
||||||
}: CardSkeletonProps) {
|
|
||||||
return (
|
|
||||||
<div key={`loading-${index}`} className="w-full p-1">
|
|
||||||
<Card className="h-full">
|
|
||||||
<CardHeader className="p-3 pb-0 flex justify-between items-start">
|
|
||||||
<Skeleton className="h-5 w-3/4" />
|
|
||||||
<div className="flex space-x-1">
|
|
||||||
{showViewIcon && (
|
|
||||||
<Skeleton className="h-8 w-8 rounded-full" />
|
|
||||||
)}
|
|
||||||
{showUpdateIcon && (
|
|
||||||
<Skeleton className="h-8 w-8 rounded-full" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-3">
|
|
||||||
<div className="space-y-2">
|
|
||||||
{showFields.map((field, fieldIndex) => (
|
|
||||||
<div key={`loading-${index}-${field}`} className="flex">
|
|
||||||
<Skeleton className="h-4 w-10 mr-2" />
|
|
||||||
<Skeleton className="h-4 w-full" />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { CardDisplay } from './CardDisplay';
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
/**
|
|
||||||
* CardDisplay component interfaces
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main props for the CardDisplay component
|
|
||||||
*/
|
|
||||||
export interface CardDisplayProps<T> {
|
|
||||||
/** Fields to display in each card */
|
|
||||||
showFields: string[];
|
|
||||||
/** Array of data items to display */
|
|
||||||
data: T[];
|
|
||||||
/** Current language code */
|
|
||||||
lang: string;
|
|
||||||
/** Translations object for field labels and messages */
|
|
||||||
translations: Record<string, any>;
|
|
||||||
/** Error object if data fetching failed */
|
|
||||||
error: Error | null;
|
|
||||||
/** Loading state indicator */
|
|
||||||
loading: boolean;
|
|
||||||
/** Field to use as the card title (default: "name") */
|
|
||||||
titleField?: string;
|
|
||||||
/** Handler for when a card is clicked */
|
|
||||||
onCardClick?: (item: T) => void;
|
|
||||||
/** Custom renderer for specific fields */
|
|
||||||
renderCustomField?: (item: T, field: string) => React.ReactNode;
|
|
||||||
/** Number of columns in the grid (1-6) */
|
|
||||||
gridCols?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
||||||
/** Whether to show the view icon */
|
|
||||||
showViewIcon?: boolean;
|
|
||||||
/** Whether to show the update/edit icon */
|
|
||||||
showUpdateIcon?: boolean;
|
|
||||||
/** Handler for when the view icon is clicked */
|
|
||||||
onViewClick?: (item: T) => void;
|
|
||||||
/** Handler for when the update/edit icon is clicked */
|
|
||||||
onUpdateClick?: (item: T) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Props for the CardItem component
|
|
||||||
*/
|
|
||||||
export interface CardItemProps<T> {
|
|
||||||
/** Data item to display */
|
|
||||||
item: T;
|
|
||||||
/** Index of the item in the data array */
|
|
||||||
index: number;
|
|
||||||
/** Fields to display in the card */
|
|
||||||
showFields: string[];
|
|
||||||
/** Field to use as the card title */
|
|
||||||
titleField: string;
|
|
||||||
/** Current language code */
|
|
||||||
lang: string;
|
|
||||||
/** Translations object for field labels */
|
|
||||||
translations: Record<string, any>;
|
|
||||||
/** Handler for when the card is clicked */
|
|
||||||
onCardClick?: (item: T) => void;
|
|
||||||
/** Custom renderer for specific fields */
|
|
||||||
renderCustomField?: (item: T, field: string) => React.ReactNode;
|
|
||||||
/** Whether to show the view icon */
|
|
||||||
showViewIcon: boolean;
|
|
||||||
/** Whether to show the update/edit icon */
|
|
||||||
showUpdateIcon: boolean;
|
|
||||||
/** Handler for when the view icon is clicked */
|
|
||||||
onViewClick?: (item: T) => void;
|
|
||||||
/** Handler for when the update/edit icon is clicked */
|
|
||||||
onUpdateClick?: (item: T) => void;
|
|
||||||
/** Function to get field values from the item */
|
|
||||||
getFieldValue: (item: any, field: string) => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Props for the CardActions component
|
|
||||||
*/
|
|
||||||
export interface CardActionsProps<T> {
|
|
||||||
/** Data item the actions apply to */
|
|
||||||
item: T;
|
|
||||||
/** Whether to show the view icon */
|
|
||||||
showViewIcon: boolean;
|
|
||||||
/** Whether to show the update/edit icon */
|
|
||||||
showUpdateIcon: boolean;
|
|
||||||
/** Handler for when the view icon is clicked */
|
|
||||||
onViewClick?: (item: T) => void;
|
|
||||||
/** Handler for when the update/edit icon is clicked */
|
|
||||||
onUpdateClick?: (item: T) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Props for the CardField component
|
|
||||||
*/
|
|
||||||
export interface CardFieldProps<T> {
|
|
||||||
/** Data item the field belongs to */
|
|
||||||
item: T;
|
|
||||||
/** Field name to display */
|
|
||||||
field: string;
|
|
||||||
/** Current language code */
|
|
||||||
lang: string;
|
|
||||||
/** Translations object for field labels */
|
|
||||||
translations: Record<string, any>;
|
|
||||||
/** Custom renderer for specific fields */
|
|
||||||
renderCustomField?: (item: T, field: string) => React.ReactNode;
|
|
||||||
/** Function to get field values from the item */
|
|
||||||
getFieldValue: (item: any, field: string) => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Props for the CardSkeleton component
|
|
||||||
*/
|
|
||||||
export interface CardSkeletonProps {
|
|
||||||
/** Index of the skeleton in the loading array */
|
|
||||||
index: number;
|
|
||||||
/** Fields to create skeleton placeholders for */
|
|
||||||
showFields: string[];
|
|
||||||
/** Whether to show a skeleton for the view icon */
|
|
||||||
showViewIcon: boolean;
|
|
||||||
/** Whether to show a skeleton for the update/edit icon */
|
|
||||||
showUpdateIcon: boolean;
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
/**
|
|
||||||
* Safely gets a field value from an item, supporting nested fields with dot notation
|
|
||||||
*/
|
|
||||||
export function getFieldValue(item: any, field: string): any {
|
|
||||||
if (!item) return "";
|
|
||||||
|
|
||||||
// Handle nested fields with dot notation (e.g., "user.name")
|
|
||||||
if (field.includes(".")) {
|
|
||||||
const parts = field.split(".");
|
|
||||||
let value = item;
|
|
||||||
for (const part of parts) {
|
|
||||||
if (value === null || value === undefined) return "";
|
|
||||||
value = value[part];
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return item[field];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a field label from translations or formats the field name
|
|
||||||
*/
|
|
||||||
export function getFieldLabel(field: string, translations: Record<string, any>, lang: string): string {
|
|
||||||
const t = translations[lang] || {};
|
|
||||||
return t[field] || field.charAt(0).toUpperCase() + field.slice(1).replace(/_/g, " ");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates responsive grid classes based on the gridCols prop
|
|
||||||
*/
|
|
||||||
export function getGridClasses(gridCols: 1 | 2 | 3 | 4 | 5 | 6): string {
|
|
||||||
const baseClass = "grid grid-cols-1 gap-4";
|
|
||||||
|
|
||||||
// Map gridCols to responsive classes
|
|
||||||
const colClasses: Record<number, string> = {
|
|
||||||
1: "",
|
|
||||||
2: "sm:grid-cols-2",
|
|
||||||
3: "sm:grid-cols-2 md:grid-cols-3",
|
|
||||||
4: "sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4",
|
|
||||||
5: "sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5",
|
|
||||||
6: "sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6"
|
|
||||||
};
|
|
||||||
|
|
||||||
return `${baseClass} ${colClasses[gridCols]}`;
|
|
||||||
}
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
|
||||||
import { CreateComponentProps, FieldDefinition } from "./types";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useForm, SubmitHandler } from "react-hook-form";
|
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
||||||
import { AlertCircle } from "lucide-react";
|
|
||||||
|
|
||||||
export function CreateComponent<T>({
|
|
||||||
refetch,
|
|
||||||
setMode,
|
|
||||||
setSelectedItem,
|
|
||||||
onCancel,
|
|
||||||
lang,
|
|
||||||
translations,
|
|
||||||
formProps = {},
|
|
||||||
apiUrl,
|
|
||||||
}: CreateComponentProps<T>) {
|
|
||||||
const t = translations[lang as keyof typeof translations] || {};
|
|
||||||
|
|
||||||
// Get field definitions from formProps if available
|
|
||||||
const fieldDefinitions = formProps.fieldDefinitions || {};
|
|
||||||
const validationSchema = formProps.validationSchema;
|
|
||||||
|
|
||||||
// Group fields by their group property
|
|
||||||
const [groupedFields, setGroupedFields] = useState<Record<string, FieldDefinition[]>>({});
|
|
||||||
|
|
||||||
// Process field definitions to group them
|
|
||||||
useEffect(() => {
|
|
||||||
if (Object.keys(fieldDefinitions).length > 0) {
|
|
||||||
const groups: Record<string, FieldDefinition[]> = {};
|
|
||||||
|
|
||||||
// Group fields by their group property
|
|
||||||
Object.entries(fieldDefinitions).forEach(([fieldName, definition]) => {
|
|
||||||
const def = definition as FieldDefinition;
|
|
||||||
if (!groups[def.group]) {
|
|
||||||
groups[def.group] = [];
|
|
||||||
}
|
|
||||||
groups[def.group].push({ ...def, name: fieldName });
|
|
||||||
});
|
|
||||||
|
|
||||||
setGroupedFields(groups);
|
|
||||||
}
|
|
||||||
}, [fieldDefinitions]);
|
|
||||||
|
|
||||||
// Initialize form with default values from field definitions
|
|
||||||
const defaultValues: Record<string, any> = {};
|
|
||||||
Object.entries(fieldDefinitions).forEach(([key, def]) => {
|
|
||||||
const fieldDef = def as FieldDefinition;
|
|
||||||
defaultValues[key] = fieldDef.defaultValue !== undefined ? fieldDef.defaultValue : "";
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup form with validation schema if available
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors },
|
|
||||||
setValue,
|
|
||||||
watch,
|
|
||||||
reset,
|
|
||||||
} = useForm<Record<string, any>>({
|
|
||||||
defaultValues,
|
|
||||||
resolver: validationSchema ? zodResolver(validationSchema) : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const formValues = watch();
|
|
||||||
|
|
||||||
// Get language-specific validation schema if available
|
|
||||||
useEffect(() => {
|
|
||||||
if (formProps.schemaPath) {
|
|
||||||
const loadLanguageValidationSchema = async () => {
|
|
||||||
try {
|
|
||||||
// Dynamic import of the schema module
|
|
||||||
const schemaModule = await import(formProps.schemaPath);
|
|
||||||
|
|
||||||
// Check if language-specific schema functions are available
|
|
||||||
if (schemaModule.getCreateApplicationSchema) {
|
|
||||||
const langValidationSchema = schemaModule.getCreateApplicationSchema(lang as "en" | "tr");
|
|
||||||
|
|
||||||
// Reset the form with the current values
|
|
||||||
reset(defaultValues);
|
|
||||||
|
|
||||||
// Update the validation schema in formProps for future validations
|
|
||||||
formProps.validationSchema = langValidationSchema;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading language-specific validation schema:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadLanguageValidationSchema();
|
|
||||||
}
|
|
||||||
}, [lang, formProps.schemaPath, reset, defaultValues]);
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const onSubmit: SubmitHandler<Record<string, any>> = async (data) => {
|
|
||||||
try {
|
|
||||||
if (apiUrl) {
|
|
||||||
const createUrl = `${apiUrl}/create`;
|
|
||||||
const response = await fetch(createUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data),
|
|
||||||
});
|
|
||||||
console.log("Response:", response.ok);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`API error: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdItem = await response.json();
|
|
||||||
console.log("Created item:", createdItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (refetch) refetch();
|
|
||||||
setMode("list");
|
|
||||||
setSelectedItem(null);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error saving form:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle select changes
|
|
||||||
const handleSelectChange = (name: string, value: string) => {
|
|
||||||
setValue(name, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle checkbox changes
|
|
||||||
const handleCheckboxChange = (name: string, checked: boolean) => {
|
|
||||||
setValue(name, checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Translate group names for display dynamically
|
|
||||||
const getGroupTitle = (groupName: string) => {
|
|
||||||
// Check if we have a translation for this group name
|
|
||||||
if (t[groupName]) {
|
|
||||||
return t[groupName];
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no translation is found, just format the name as a fallback
|
|
||||||
const formattedName = groupName
|
|
||||||
.replace(/([A-Z])/g, ' $1')
|
|
||||||
.replace(/_/g, ' ')
|
|
||||||
.replace(/^./, (str) => str.toUpperCase())
|
|
||||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
||||||
return formattedName;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render a field based on its type
|
|
||||||
const renderField = (fieldName: string, field: FieldDefinition) => {
|
|
||||||
const errorMessage = errors[fieldName]?.message as string;
|
|
||||||
|
|
||||||
switch (field.type) {
|
|
||||||
case "text":
|
|
||||||
return (
|
|
||||||
<div className="space-y-2" key={fieldName}>
|
|
||||||
<Label htmlFor={fieldName}>
|
|
||||||
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
|
||||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id={fieldName}
|
|
||||||
{...register(fieldName)}
|
|
||||||
placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
|
|
||||||
disabled={field.readOnly}
|
|
||||||
/>
|
|
||||||
{errorMessage && (
|
|
||||||
<p className="text-sm text-red-500">{errorMessage}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case "textarea":
|
|
||||||
return (
|
|
||||||
<div className="space-y-2" key={fieldName}>
|
|
||||||
<Label htmlFor={fieldName}>
|
|
||||||
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
|
||||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
|
||||||
</Label>
|
|
||||||
<Textarea
|
|
||||||
id={fieldName}
|
|
||||||
{...register(fieldName)}
|
|
||||||
placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
|
|
||||||
rows={3}
|
|
||||||
disabled={field.readOnly}
|
|
||||||
/>
|
|
||||||
{errorMessage && (
|
|
||||||
<p className="text-sm text-red-500">{errorMessage}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case "select":
|
|
||||||
return (
|
|
||||||
<div className="space-y-2" key={fieldName}>
|
|
||||||
<Label htmlFor={fieldName}>
|
|
||||||
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
|
||||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={formValues[fieldName]}
|
|
||||||
onValueChange={(value) => handleSelectChange(fieldName, value)}
|
|
||||||
disabled={field.readOnly}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder={t[fieldName] || field.label[lang as "en" | "tr"]} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{field.options?.map((option) => (
|
|
||||||
<SelectItem key={option} value={option}>
|
|
||||||
{t[option] || option}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{errorMessage && (
|
|
||||||
<p className="text-sm text-red-500">{errorMessage}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case "checkbox":
|
|
||||||
return (
|
|
||||||
<div className="flex items-center space-x-2" key={fieldName}>
|
|
||||||
<Checkbox
|
|
||||||
id={fieldName}
|
|
||||||
checked={formValues[fieldName]}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
handleCheckboxChange(fieldName, checked as boolean)
|
|
||||||
}
|
|
||||||
disabled={field.readOnly}
|
|
||||||
/>
|
|
||||||
<Label htmlFor={fieldName}>
|
|
||||||
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
|
||||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
|
||||||
</Label>
|
|
||||||
{errorMessage && (
|
|
||||||
<p className="text-sm text-red-500">{errorMessage}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<div className="grid grid-cols-2 gap-4 pt-6 my-4">
|
|
||||||
<Button type="button" variant="outline" onClick={onCancel} className="w-full">
|
|
||||||
{t.cancel || "Cancel"}
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" className="w-full">
|
|
||||||
{t.save || "Save"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<Card className="w-full mb-6">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{t.create || "Create"}</CardTitle>
|
|
||||||
<CardDescription>{t.createDescription || "Create a new item"}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{/* Display validation errors summary if any */}
|
|
||||||
{Object.keys(errors).length > 0 && (
|
|
||||||
<Alert variant="destructive" className="mb-4">
|
|
||||||
<AlertCircle className="h-4 w-4" />
|
|
||||||
<AlertDescription>
|
|
||||||
{t.formErrors || "Please correct the errors in the form"}
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Render fields grouped by their group property */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
{Object.entries(groupedFields).map(([groupName, fields]) => (
|
|
||||||
<Card key={groupName} className="shadow-sm">
|
|
||||||
<CardHeader className="py-3">
|
|
||||||
<CardTitle className="text-lg">{getGroupTitle(groupName)}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{fields.map((field: any) => renderField(field.name, field))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { CreateComponent } from "./CreateComponent";
|
|
||||||
import { UpdateComponent } from "./UpdateComponent";
|
|
||||||
import { ViewComponent } from "./ViewComponent";
|
|
||||||
import { FormDisplayProps } from "./types";
|
|
||||||
|
|
||||||
export function FormDisplay<T>({
|
|
||||||
initialData,
|
|
||||||
mode,
|
|
||||||
refetch,
|
|
||||||
setMode,
|
|
||||||
setSelectedItem,
|
|
||||||
onCancel,
|
|
||||||
lang,
|
|
||||||
translations,
|
|
||||||
formProps = {},
|
|
||||||
apiUrl,
|
|
||||||
}: FormDisplayProps<T>) {
|
|
||||||
const [enhancedFormProps, setEnhancedFormProps] = useState(formProps);
|
|
||||||
|
|
||||||
// Update form props when language or mode changes
|
|
||||||
useEffect(() => {
|
|
||||||
const updateFormProps = async () => {
|
|
||||||
try {
|
|
||||||
// Check if schemaPath is provided in formProps
|
|
||||||
if (formProps.schemaPath) {
|
|
||||||
// Dynamic import of the schema module
|
|
||||||
const schemaModule = await import(formProps.schemaPath);
|
|
||||||
|
|
||||||
// Get the appropriate field definitions based on mode
|
|
||||||
let fieldDefs;
|
|
||||||
if (schemaModule.fieldDefinitions?.getDefinitionsByMode) {
|
|
||||||
fieldDefs = schemaModule.fieldDefinitions.getDefinitionsByMode(mode);
|
|
||||||
} else if (mode === "create" && schemaModule.createFieldDefinitions) {
|
|
||||||
fieldDefs = schemaModule.createFieldDefinitions;
|
|
||||||
} else if (mode === "update" && schemaModule.updateFieldDefinitions) {
|
|
||||||
fieldDefs = schemaModule.updateFieldDefinitions;
|
|
||||||
} else if (mode === "view" && schemaModule.viewFieldDefinitions) {
|
|
||||||
fieldDefs = schemaModule.viewFieldDefinitions;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the appropriate validation schema based on mode and language
|
|
||||||
let validationSchema;
|
|
||||||
if (mode === "create" && schemaModule.getCreateApplicationSchema) {
|
|
||||||
// Use language-aware schema factory function
|
|
||||||
validationSchema = schemaModule.getCreateApplicationSchema(lang as "en" | "tr");
|
|
||||||
} else if (mode === "update" && schemaModule.getUpdateApplicationSchema) {
|
|
||||||
// Use language-aware schema factory function
|
|
||||||
validationSchema = schemaModule.getUpdateApplicationSchema(lang as "en" | "tr");
|
|
||||||
} else if (mode === "view" && schemaModule.ViewApplicationSchema) {
|
|
||||||
validationSchema = schemaModule.ViewApplicationSchema;
|
|
||||||
} else if (schemaModule.ApplicationSchema) {
|
|
||||||
validationSchema = schemaModule.ApplicationSchema;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the grouped field definitions structure if available
|
|
||||||
const groupedFieldDefs = schemaModule.baseFieldDefinitions || {};
|
|
||||||
|
|
||||||
// Update form props with schema information and current language
|
|
||||||
setEnhancedFormProps({
|
|
||||||
...formProps,
|
|
||||||
fieldDefinitions: fieldDefs || {},
|
|
||||||
validationSchema,
|
|
||||||
fieldsByMode: schemaModule.fieldsByMode || {},
|
|
||||||
groupedFieldDefinitions: groupedFieldDefs,
|
|
||||||
// Add current language to force child components to recognize changes
|
|
||||||
currentLang: lang,
|
|
||||||
// Add schema path for dynamic imports in child components
|
|
||||||
schemaPath: formProps.schemaPath
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// If no schema path, just update with current language
|
|
||||||
setEnhancedFormProps({
|
|
||||||
...formProps,
|
|
||||||
currentLang: lang
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading schema definitions:", error);
|
|
||||||
// Even on error, update the language
|
|
||||||
setEnhancedFormProps({
|
|
||||||
...formProps,
|
|
||||||
currentLang: lang
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateFormProps();
|
|
||||||
}, [formProps, mode, lang]); // Lang dependency ensures re-fetch when language changes
|
|
||||||
|
|
||||||
// Debug the props received by FormDisplay
|
|
||||||
// FormDisplay component renders different form modes based on the mode prop
|
|
||||||
|
|
||||||
// Render the appropriate component based on the mode
|
|
||||||
switch (mode) {
|
|
||||||
case "create":
|
|
||||||
return (
|
|
||||||
<CreateComponent<T>
|
|
||||||
key={`create-${lang}`} // Add key with lang to force re-render on language change
|
|
||||||
refetch={refetch}
|
|
||||||
setMode={setMode}
|
|
||||||
setSelectedItem={setSelectedItem}
|
|
||||||
onCancel={onCancel}
|
|
||||||
lang={lang}
|
|
||||||
translations={translations}
|
|
||||||
formProps={enhancedFormProps}
|
|
||||||
apiUrl={apiUrl}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "update":
|
|
||||||
// Create a stable key for the component to ensure proper re-rendering
|
|
||||||
const updateKey = `update-${lang}-${(initialData as any)?.uu_id || 'new'}`;
|
|
||||||
|
|
||||||
return initialData ? (
|
|
||||||
<UpdateComponent<T>
|
|
||||||
key={updateKey} // Add key with lang and item ID to force re-render
|
|
||||||
initialData={initialData}
|
|
||||||
refetch={refetch}
|
|
||||||
setMode={setMode}
|
|
||||||
setSelectedItem={setSelectedItem}
|
|
||||||
onCancel={onCancel}
|
|
||||||
lang={lang}
|
|
||||||
translations={translations}
|
|
||||||
formProps={enhancedFormProps}
|
|
||||||
apiUrl={apiUrl}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
case "view":
|
|
||||||
return initialData ? (
|
|
||||||
<ViewComponent<T>
|
|
||||||
key={`view-${lang}`} // Add key with lang to force re-render on language change
|
|
||||||
initialData={initialData}
|
|
||||||
refetch={refetch}
|
|
||||||
setMode={setMode}
|
|
||||||
setSelectedItem={setSelectedItem}
|
|
||||||
onCancel={onCancel}
|
|
||||||
lang={lang}
|
|
||||||
translations={translations}
|
|
||||||
formProps={enhancedFormProps} // Changed from formProps to enhancedFormProps for consistency
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,411 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React, { useState, useEffect, useMemo } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
|
||||||
import { UpdateComponentProps, FieldDefinition } from "./types";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
||||||
import { AlertCircle } from "lucide-react";
|
|
||||||
|
|
||||||
export function UpdateComponent<T>({
|
|
||||||
initialData,
|
|
||||||
refetch,
|
|
||||||
setMode,
|
|
||||||
setSelectedItem,
|
|
||||||
onCancel,
|
|
||||||
lang,
|
|
||||||
translations,
|
|
||||||
apiUrl,
|
|
||||||
formProps = {},
|
|
||||||
}: UpdateComponentProps<T>) {
|
|
||||||
const t = translations[lang as keyof typeof translations] || {};
|
|
||||||
|
|
||||||
// Get field definitions from formProps if available
|
|
||||||
const fieldDefinitions = formProps.fieldDefinitions || {};
|
|
||||||
const validationSchema = formProps.validationSchema;
|
|
||||||
|
|
||||||
// Ensure field definitions are processed only once
|
|
||||||
const processedFieldDefinitions = useMemo(() => {
|
|
||||||
const processed = { ...fieldDefinitions };
|
|
||||||
// Make all fields editable except system fields
|
|
||||||
Object.entries(processed).forEach(([fieldName, definition]) => {
|
|
||||||
if (fieldName !== 'uu_id' && fieldName !== 'created_at' && fieldName !== 'updated_at') {
|
|
||||||
(processed[fieldName] as FieldDefinition).readOnly = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return processed;
|
|
||||||
}, [fieldDefinitions]);
|
|
||||||
|
|
||||||
const [groupedFields, setGroupedFields] = useState<Record<string, FieldDefinition[]>>({});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (Object.keys(processedFieldDefinitions).length > 0) {
|
|
||||||
const groups: Record<string, FieldDefinition[]> = {};
|
|
||||||
|
|
||||||
// Group the processed field definitions
|
|
||||||
Object.entries(processedFieldDefinitions).forEach(([fieldName, definition]) => {
|
|
||||||
// Convert to FieldDefinition type
|
|
||||||
const def = definition as FieldDefinition;
|
|
||||||
|
|
||||||
// Add the field name to the definition
|
|
||||||
const fieldDef = { ...def, name: fieldName };
|
|
||||||
|
|
||||||
// Add to the appropriate group
|
|
||||||
if (!groups[def.group]) {
|
|
||||||
groups[def.group] = [];
|
|
||||||
}
|
|
||||||
groups[def.group].push(fieldDef);
|
|
||||||
});
|
|
||||||
setGroupedFields(groups);
|
|
||||||
}
|
|
||||||
}, [processedFieldDefinitions]);
|
|
||||||
|
|
||||||
const defaultValues: Record<string, any> = {};
|
|
||||||
Object.entries(processedFieldDefinitions).forEach(([key, def]) => {
|
|
||||||
const fieldDef = def as FieldDefinition;
|
|
||||||
defaultValues[key] = fieldDef.defaultValue !== undefined ? fieldDef.defaultValue : "";
|
|
||||||
});
|
|
||||||
|
|
||||||
if (initialData) {
|
|
||||||
Object.assign(defaultValues, initialData as Record<string, any>);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track the current language to detect changes
|
|
||||||
const [currentLang, setCurrentLang] = useState(lang);
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors, isSubmitting, isValid },
|
|
||||||
setValue,
|
|
||||||
watch,
|
|
||||||
reset,
|
|
||||||
trigger,
|
|
||||||
} = useForm({
|
|
||||||
defaultValues,
|
|
||||||
resolver: validationSchema ? zodResolver(validationSchema) : undefined,
|
|
||||||
mode: "onChange",
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (Object.keys(errors).length > 0) {
|
|
||||||
console.log("Form errors:", errors);
|
|
||||||
}
|
|
||||||
}, [errors]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (initialData) {
|
|
||||||
reset({ ...initialData as Record<string, any> });
|
|
||||||
}
|
|
||||||
}, [initialData, reset]);
|
|
||||||
|
|
||||||
// Detect language changes and update validation schema
|
|
||||||
useEffect(() => {
|
|
||||||
// If language has changed, update the form
|
|
||||||
if (currentLang !== lang || formProps.currentLang !== lang) {
|
|
||||||
const updateValidationForLanguage = async () => {
|
|
||||||
try {
|
|
||||||
// If we have a schema path, dynamically load the schema for the current language
|
|
||||||
if (formProps.schemaPath) {
|
|
||||||
// Dynamic import of the schema module
|
|
||||||
const schemaModule = await import(formProps.schemaPath);
|
|
||||||
|
|
||||||
// Check if language-specific schema functions are available
|
|
||||||
if (schemaModule.getUpdateApplicationSchema) {
|
|
||||||
// Get the schema for the current language
|
|
||||||
const langValidationSchema = schemaModule.getUpdateApplicationSchema(lang as "en" | "tr");
|
|
||||||
|
|
||||||
// Save current form values
|
|
||||||
const formValues = watch();
|
|
||||||
|
|
||||||
// Reset the form with current values but clear errors
|
|
||||||
reset(formValues, {
|
|
||||||
keepDirty: true,
|
|
||||||
keepValues: true,
|
|
||||||
keepErrors: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Manually trigger validation after reset
|
|
||||||
setTimeout(() => {
|
|
||||||
// Trigger validation for all fields to show updated error messages
|
|
||||||
Object.keys(formValues).forEach(fieldName => {
|
|
||||||
trigger(fieldName);
|
|
||||||
});
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
// Update our tracked language
|
|
||||||
setCurrentLang(lang);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error updating validation schema for language:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateValidationForLanguage();
|
|
||||||
}
|
|
||||||
}, [lang, formProps.currentLang, currentLang, formProps.schemaPath, reset, watch, trigger]);
|
|
||||||
|
|
||||||
const formValues = watch();
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const onSubmit = async (data: any) => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
const isFormValid = await trigger();
|
|
||||||
if (!isFormValid) {
|
|
||||||
console.error("Form validation failed - stopping submission");
|
|
||||||
return; // Stop submission if validation fails
|
|
||||||
}
|
|
||||||
if (!apiUrl) {
|
|
||||||
console.error("API URL is missing or undefined");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const uuid = initialData ? (initialData as any).uuid || (initialData as any).uu_id : null;
|
|
||||||
if (!uuid) {
|
|
||||||
console.error("UUID not found in initialData");
|
|
||||||
throw new Error("UUID is required for update operations");
|
|
||||||
}
|
|
||||||
const dataToSend = { ...data };
|
|
||||||
Object.entries(fieldDefinitions).forEach(([key, def]) => {
|
|
||||||
const fieldDef = def as FieldDefinition;
|
|
||||||
if (fieldDef.readOnly) {
|
|
||||||
delete dataToSend[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateUrl = `${apiUrl}/update?uuid=${uuid}`;
|
|
||||||
try {
|
|
||||||
let response = await fetch(updateUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(dataToSend),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
console.error("API error response:", errorText);
|
|
||||||
throw new Error(`API error: ${response.status}`);
|
|
||||||
}
|
|
||||||
if (refetch) refetch();
|
|
||||||
setMode("list");
|
|
||||||
setSelectedItem(null);
|
|
||||||
} catch (fetchError) {
|
|
||||||
console.error("Error during fetch:", fetchError);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error details:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle select changes
|
|
||||||
const handleSelectChange = (name: string, value: string) => {
|
|
||||||
setValue(name, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle checkbox changes
|
|
||||||
const handleCheckboxChange = (name: string, checked: boolean) => {
|
|
||||||
setValue(name, checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Translate group names for display dynamically
|
|
||||||
const getGroupTitle = (groupName: string) => {
|
|
||||||
// Check if we have a translation for this group name
|
|
||||||
if (t[groupName]) {
|
|
||||||
return t[groupName];
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no translation is found, just format the name as a fallback
|
|
||||||
const formattedName = groupName
|
|
||||||
.replace(/([A-Z])/g, ' $1')
|
|
||||||
.replace(/_/g, ' ')
|
|
||||||
.replace(/^./, (str) => str.toUpperCase())
|
|
||||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
||||||
|
|
||||||
return formattedName;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderField = (fieldName: string, field: FieldDefinition) => {
|
|
||||||
const errorMessage = errors[fieldName]?.message as string;
|
|
||||||
const fieldValue = formValues[fieldName];
|
|
||||||
|
|
||||||
const renderLabel = () => (
|
|
||||||
<Label htmlFor={fieldName}>
|
|
||||||
{t[fieldName] || field.label[lang as "en" | "tr"]}
|
|
||||||
{field.required && <span className="text-red-500 ml-1">*</span>}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (field.readOnly) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-2" key={fieldName}>
|
|
||||||
{renderLabel()}
|
|
||||||
<div className="p-2 bg-gray-50 rounded border border-gray-200">
|
|
||||||
{field.type === "checkbox" ?
|
|
||||||
(fieldValue ? "Yes" : "No") :
|
|
||||||
(fieldValue || "-")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For editable fields, render the appropriate input type
|
|
||||||
switch (field.type) {
|
|
||||||
case "text":
|
|
||||||
return (
|
|
||||||
<div className="space-y-2" key={fieldName}>
|
|
||||||
{renderLabel()}
|
|
||||||
<Input
|
|
||||||
id={fieldName}
|
|
||||||
{...register(fieldName)}
|
|
||||||
placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
|
|
||||||
/>
|
|
||||||
{errorMessage && (
|
|
||||||
<p className="text-sm text-red-500">{errorMessage}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case "textarea":
|
|
||||||
return (
|
|
||||||
<div className="space-y-2" key={fieldName}>
|
|
||||||
{renderLabel()}
|
|
||||||
<Textarea
|
|
||||||
id={fieldName}
|
|
||||||
{...register(fieldName)}
|
|
||||||
placeholder={t[fieldName] || field.label[lang as "en" | "tr"]}
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
{errorMessage && (
|
|
||||||
<p className="text-sm text-red-500">{errorMessage}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case "select":
|
|
||||||
return (
|
|
||||||
<div className="space-y-2" key={fieldName}>
|
|
||||||
{renderLabel()}
|
|
||||||
<Select
|
|
||||||
value={fieldValue}
|
|
||||||
onValueChange={(value) => handleSelectChange(fieldName, value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder={t[fieldName] || field.label[lang as "en" | "tr"]} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{field.options?.map((option) => (
|
|
||||||
<SelectItem key={option} value={option}>
|
|
||||||
{t[option] || option}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
{errorMessage && (
|
|
||||||
<p className="text-sm text-red-500">{errorMessage}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case "checkbox":
|
|
||||||
return (
|
|
||||||
<div className="flex items-center space-x-2" key={fieldName}>
|
|
||||||
<Checkbox
|
|
||||||
id={fieldName}
|
|
||||||
checked={fieldValue}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
handleCheckboxChange(fieldName, checked as boolean)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{renderLabel()}
|
|
||||||
{errorMessage && (
|
|
||||||
<p className="text-sm text-red-500">{errorMessage}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case "date":
|
|
||||||
return (
|
|
||||||
<div className="space-y-2" key={fieldName}>
|
|
||||||
{renderLabel()}
|
|
||||||
<Input
|
|
||||||
id={fieldName}
|
|
||||||
type="date"
|
|
||||||
{...register(fieldName)}
|
|
||||||
/>
|
|
||||||
{errorMessage && (
|
|
||||||
<p className="text-sm text-red-500">{errorMessage}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<Card className="w-full mb-6">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{t.update || "Update"}</CardTitle>
|
|
||||||
<CardDescription>{t.updateDescription || "Update existing item"}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
|
|
||||||
{/* Display validation errors summary if any */}
|
|
||||||
{Object.keys(errors).length > 0 && (
|
|
||||||
<Alert variant="destructive" className="mb-4">
|
|
||||||
<AlertCircle className="h-4 w-4" />
|
|
||||||
<AlertDescription>
|
|
||||||
{t.formErrors || "Please correct the errors in the form"}
|
|
||||||
<ul className="mt-2 list-disc pl-5">
|
|
||||||
{Object.entries(errors).map(([field, error]) => (
|
|
||||||
<li key={field}>
|
|
||||||
{t[field] || field}: {(error as any)?.message || 'Invalid value'}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 pt-6 my-4">
|
|
||||||
<Button type="button" variant="outline" onClick={onCancel} className="w-full">
|
|
||||||
{t.cancel || "Cancel"}
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" className="w-full">
|
|
||||||
{t.save || "Save"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Render fields grouped by their group property */}
|
|
||||||
<div className="space-y-6">
|
|
||||||
{Object.entries(groupedFields).map(([groupName, fields]) => (
|
|
||||||
<Card key={groupName} className="shadow-sm">
|
|
||||||
<CardHeader className="py-3">
|
|
||||||
<CardTitle className="text-lg">{getGroupTitle(groupName)}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{fields.map((field: any) => renderField(field.name, field))}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
|
||||||
import { ViewComponentProps, FieldDefinition } from "./types";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
|
|
||||||
// Utility function to format field label
|
|
||||||
const formatFieldLabel = (fieldName: string) =>
|
|
||||||
fieldName
|
|
||||||
.replace(/([A-Z])/g, ' $1')
|
|
||||||
.replace(/_/g, ' ')
|
|
||||||
.replace(/^./, (str) => str.toUpperCase())
|
|
||||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
||||||
|
|
||||||
// Component for rendering a single field
|
|
||||||
const ViewField: React.FC<{
|
|
||||||
fieldName: string;
|
|
||||||
value: any;
|
|
||||||
label: string;
|
|
||||||
lang: string;
|
|
||||||
translations: any;
|
|
||||||
hasError?: string;
|
|
||||||
}> = ({ fieldName, value, label, lang, translations: t, hasError }) => {
|
|
||||||
const formatFieldValue = () => {
|
|
||||||
if (value === undefined || value === null) return "-";
|
|
||||||
|
|
||||||
switch (true) {
|
|
||||||
case typeof value === 'string' && !isNaN(Date.parse(value)):
|
|
||||||
return new Date(value).toLocaleString(lang === "tr" ? "tr-TR" : "en-US");
|
|
||||||
case typeof value === 'boolean':
|
|
||||||
return value ? (
|
|
||||||
<span className="inline-flex items-center rounded-md bg-green-50 px-2 py-1 text-xs font-medium text-green-700">
|
|
||||||
{t.yes || "Yes"}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="inline-flex items-center rounded-md bg-red-50 px-2 py-1 text-xs font-medium text-red-700">
|
|
||||||
{t.no || "No"}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
case fieldName === "application_type" && (value === "employee" || value === "occupant"):
|
|
||||||
return t[value] || value;
|
|
||||||
default:
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-1">
|
|
||||||
<Label className="text-sm font-medium">
|
|
||||||
{t[fieldName] || label}
|
|
||||||
</Label>
|
|
||||||
<div className={`rounded-md border ${hasError ? 'border-red-500' : 'border-input'} bg-background px-3 py-2 text-sm min-h-[2.5rem] flex items-center`}>
|
|
||||||
{formatFieldValue()}
|
|
||||||
</div>
|
|
||||||
{hasError && (
|
|
||||||
<p className="text-sm text-red-500">{hasError}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Component for rendering a group of fields
|
|
||||||
const ViewFieldGroup: React.FC<{
|
|
||||||
groupName: string;
|
|
||||||
fields: FieldDefinition[];
|
|
||||||
initialData: any;
|
|
||||||
lang: string;
|
|
||||||
translations: any;
|
|
||||||
validationErrors: Record<string, string>;
|
|
||||||
}> = ({ groupName, fields, initialData, lang, translations, validationErrors }) => {
|
|
||||||
const getGroupTitle = (name: string) => {
|
|
||||||
return translations[name] || formatFieldLabel(name);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="shadow-sm">
|
|
||||||
<CardHeader className="py-3">
|
|
||||||
<CardTitle className="text-lg">{getGroupTitle(groupName)}</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{fields.map((field) => {
|
|
||||||
const fieldName = field.name || "";
|
|
||||||
const value = initialData ? (initialData as any)[fieldName] : undefined;
|
|
||||||
const hasError = validationErrors[fieldName];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ViewField
|
|
||||||
key={fieldName}
|
|
||||||
fieldName={fieldName}
|
|
||||||
value={value}
|
|
||||||
label={field.label[lang as "en" | "tr"]}
|
|
||||||
lang={lang}
|
|
||||||
translations={translations}
|
|
||||||
hasError={hasError}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Main ViewComponent
|
|
||||||
export function ViewComponent<T>({
|
|
||||||
initialData,
|
|
||||||
setMode,
|
|
||||||
setSelectedItem,
|
|
||||||
onCancel,
|
|
||||||
lang,
|
|
||||||
translations,
|
|
||||||
formProps = {},
|
|
||||||
}: ViewComponentProps<T>) {
|
|
||||||
const t = translations[lang as keyof typeof translations] || {};
|
|
||||||
|
|
||||||
// Get field definitions and validation schema from formProps if available
|
|
||||||
const fieldDefinitions = formProps.fieldDefinitions || {};
|
|
||||||
const validationSchema = formProps.validationSchema as z.ZodObject<any> | undefined;
|
|
||||||
|
|
||||||
// Group fields by their group property
|
|
||||||
const [groupedFields, setGroupedFields] = useState<Record<string, FieldDefinition[]>>({});
|
|
||||||
// State to track validation errors if any
|
|
||||||
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
|
|
||||||
|
|
||||||
// Process field definitions to group them
|
|
||||||
useEffect(() => {
|
|
||||||
if (Object.keys(fieldDefinitions).length > 0) {
|
|
||||||
const groups: Record<string, FieldDefinition[]> = {};
|
|
||||||
|
|
||||||
// Group fields by their group property
|
|
||||||
Object.entries(fieldDefinitions).forEach(([fieldName, definition]) => {
|
|
||||||
const def = definition as FieldDefinition;
|
|
||||||
if (!groups[def.group]) {
|
|
||||||
groups[def.group] = [];
|
|
||||||
}
|
|
||||||
groups[def.group].push({ ...def, name: fieldName });
|
|
||||||
});
|
|
||||||
|
|
||||||
setGroupedFields(groups);
|
|
||||||
}
|
|
||||||
}, [fieldDefinitions]);
|
|
||||||
|
|
||||||
// For view mode, we don't need strict validation
|
|
||||||
// Just log validation issues but don't show errors to the user
|
|
||||||
useEffect(() => {
|
|
||||||
if (validationSchema && initialData) {
|
|
||||||
try {
|
|
||||||
// Try to parse the data through the Zod schema
|
|
||||||
validationSchema.parse(initialData);
|
|
||||||
// Clear any previous validation errors if successful
|
|
||||||
setValidationErrors({});
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
// Just log the errors but don't set them in the state
|
|
||||||
// This prevents showing validation errors in view mode
|
|
||||||
console.warn('View data validation issues (not shown to user):', error.errors);
|
|
||||||
|
|
||||||
// Clear any previous validation errors
|
|
||||||
setValidationErrors({});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [initialData, validationSchema]);
|
|
||||||
|
|
||||||
const handleEdit = () => {
|
|
||||||
setMode("update");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="w-full mb-6">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{t.view || "View"}</CardTitle>
|
|
||||||
<CardDescription>{t.viewDescription || "View item details"}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-6">
|
|
||||||
{/* If we have grouped fields from schema, use them */}
|
|
||||||
{Object.keys(groupedFields).length > 0 ? (
|
|
||||||
// Render fields grouped by their group property
|
|
||||||
Object.entries(groupedFields).map(([groupName, fields]) => (
|
|
||||||
<ViewFieldGroup
|
|
||||||
key={groupName}
|
|
||||||
groupName={groupName}
|
|
||||||
fields={fields}
|
|
||||||
initialData={initialData}
|
|
||||||
lang={lang}
|
|
||||||
translations={t}
|
|
||||||
validationErrors={validationErrors}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
// Fallback to the default view if no field definitions are available
|
|
||||||
initialData && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
{validationSchema ? (
|
|
||||||
// If we have a validation schema, use its shape to determine fields
|
|
||||||
Object.entries(validationSchema.shape || {}).map(([fieldName, _]) => {
|
|
||||||
const value = (initialData as any)[fieldName];
|
|
||||||
// Skip undefined or null values
|
|
||||||
if (value === undefined || value === null) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ViewField
|
|
||||||
key={fieldName}
|
|
||||||
fieldName={fieldName}
|
|
||||||
value={value}
|
|
||||||
label={formatFieldLabel(fieldName)}
|
|
||||||
lang={lang}
|
|
||||||
translations={t}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
// If no schema, just iterate over the data keys
|
|
||||||
Object.entries(initialData as Record<string, any>).map(([fieldName, value]) => {
|
|
||||||
// Skip undefined or null values
|
|
||||||
if (value === undefined || value === null) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ViewField
|
|
||||||
key={fieldName}
|
|
||||||
fieldName={fieldName}
|
|
||||||
value={value}
|
|
||||||
label={formatFieldLabel(fieldName)}
|
|
||||||
lang={lang}
|
|
||||||
translations={t}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex justify-end space-x-2 pt-4">
|
|
||||||
<Button variant="outline" onClick={onCancel}>
|
|
||||||
{t.back || "Back"}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleEdit}>
|
|
||||||
{t.edit || "Edit"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
// Export the main components
|
|
||||||
export { FormDisplay } from "./FormDisplay";
|
|
||||||
|
|
||||||
// Export types
|
|
||||||
export type { FormMode } from "./types";
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
// Import field definitions type
|
|
||||||
export interface FieldDefinition {
|
|
||||||
type: string;
|
|
||||||
group: string;
|
|
||||||
label: { tr: string; en: string };
|
|
||||||
options?: string[];
|
|
||||||
readOnly?: boolean;
|
|
||||||
required?: boolean;
|
|
||||||
defaultValue?: any;
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define the FormMode type to ensure consistency
|
|
||||||
export type FormMode = "list" | "create" | "update" | "view";
|
|
||||||
|
|
||||||
export interface BaseFormProps<T> {
|
|
||||||
initialData?: T;
|
|
||||||
refetch?: () => void;
|
|
||||||
setMode: React.Dispatch<React.SetStateAction<FormMode>>;
|
|
||||||
setSelectedItem: React.Dispatch<React.SetStateAction<T | null>>;
|
|
||||||
onCancel: () => void;
|
|
||||||
lang: string;
|
|
||||||
translations: Record<string, Record<string, string>>;
|
|
||||||
formProps?: Record<string, any>;
|
|
||||||
apiUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateComponentProps<T> extends BaseFormProps<T> {}
|
|
||||||
|
|
||||||
export interface UpdateComponentProps<T> extends BaseFormProps<T> {
|
|
||||||
initialData: T; // Required for update
|
|
||||||
apiUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ViewComponentProps<T> extends BaseFormProps<T> {
|
|
||||||
initialData: T; // Required for view
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FormDisplayProps<T> {
|
|
||||||
mode: FormMode;
|
|
||||||
initialData?: T;
|
|
||||||
refetch?: () => void;
|
|
||||||
setMode: React.Dispatch<React.SetStateAction<FormMode>>;
|
|
||||||
setSelectedItem: React.Dispatch<React.SetStateAction<T | null>>;
|
|
||||||
onCancel: () => void;
|
|
||||||
lang: string;
|
|
||||||
translations: Record<string, Record<string, string>>;
|
|
||||||
formProps?: Record<string, any>;
|
|
||||||
apiUrl: string;
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export type GridSize = 1 | 2 | 3 | 4 | 5 | 6;
|
|
||||||
|
|
||||||
interface GridSelectionComponentProps {
|
|
||||||
gridCols: GridSize;
|
|
||||||
setGridCols: (size: GridSize) => void;
|
|
||||||
translations?: Record<string, any>;
|
|
||||||
lang?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GridSelectionComponent: React.FC<GridSelectionComponentProps> = ({
|
|
||||||
gridCols,
|
|
||||||
setGridCols,
|
|
||||||
translations,
|
|
||||||
lang = "en",
|
|
||||||
}) => {
|
|
||||||
const t = translations?.[lang] || {};
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
||||||
setGridCols(Number(e.target.value) as GridSize);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center">
|
|
||||||
<span className="mr-2 text-sm">{t.gridSize || "Grid Size:"}:</span>
|
|
||||||
<select
|
|
||||||
value={gridCols}
|
|
||||||
onChange={handleChange}
|
|
||||||
className="p-2 border rounded"
|
|
||||||
>
|
|
||||||
<option value="1">{t.oneColumn || "1 Column"}</option>
|
|
||||||
<option value="2">{t.twoColumns || "2 Columns"}</option>
|
|
||||||
<option value="3">{t.threeColumns || "3 Columns"}</option>
|
|
||||||
<option value="4">{t.fourColumns || "4 Columns"}</option>
|
|
||||||
<option value="5">{t.fiveColumns || "5 Columns"}</option>
|
|
||||||
<option value="6">{t.sixColumns || "6 Columns"}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import { Language, LanguageSelectionComponentProps } from "@/components/common/schemas";
|
|
||||||
|
|
||||||
export const LanguageSelectionComponent: React.FC<LanguageSelectionComponentProps> = ({
|
|
||||||
lang,
|
|
||||||
setLang,
|
|
||||||
translations,
|
|
||||||
className = "p-2 border rounded",
|
|
||||||
}) => {
|
|
||||||
const t = translations?.[lang] || {};
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
||||||
setLang(e.target.value as Language);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center">
|
|
||||||
<select
|
|
||||||
value={lang}
|
|
||||||
onChange={handleChange}
|
|
||||||
className={className}
|
|
||||||
>
|
|
||||||
<option value="en">{t.english || "English"}</option>
|
|
||||||
<option value="tr">{t.turkish || "Turkish"}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { PaginationBaseProps } from "./types";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate the page numbers to display in pagination
|
|
||||||
* @param currentPage Current active page
|
|
||||||
* @param totalPages Total number of pages
|
|
||||||
* @param maxButtons Maximum number of page buttons to show (default: 5)
|
|
||||||
* @returns Array of page numbers to display
|
|
||||||
*/
|
|
||||||
const getPageNumbers = (currentPage: number, totalPages: number, maxButtons: number = 5): number[] => {
|
|
||||||
const pageNumbers: number[] = [];
|
|
||||||
|
|
||||||
// If we have fewer pages than the maximum buttons, show all pages
|
|
||||||
if (totalPages <= maxButtons) {
|
|
||||||
for (let i = 1; i <= totalPages; i++) {
|
|
||||||
pageNumbers.push(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If we're near the beginning, show first maxButtons pages
|
|
||||||
else if (currentPage <= 3) {
|
|
||||||
for (let i = 1; i <= maxButtons; i++) {
|
|
||||||
pageNumbers.push(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If we're near the end, show last maxButtons pages
|
|
||||||
else if (currentPage >= totalPages - 2) {
|
|
||||||
for (let i = totalPages - maxButtons + 1; i <= totalPages; i++) {
|
|
||||||
pageNumbers.push(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Otherwise, show pages centered around current page
|
|
||||||
else {
|
|
||||||
for (let i = currentPage - 2; i <= currentPage + 2; i++) {
|
|
||||||
pageNumbers.push(i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pageNumbers;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PageNavigation: React.FC<PaginationBaseProps> = ({
|
|
||||||
pagination,
|
|
||||||
updatePagination,
|
|
||||||
loading = false,
|
|
||||||
lang,
|
|
||||||
translations,
|
|
||||||
}) => {
|
|
||||||
const t = translations[lang] || {};
|
|
||||||
|
|
||||||
const handlePageChange = (newPage: number) => {
|
|
||||||
if (newPage >= 1 && newPage <= pagination.totalPages) {
|
|
||||||
updatePagination({ page: newPage });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get the page numbers to display
|
|
||||||
const pageNumbers = getPageNumbers(pagination.page, pagination.totalPages);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
{pagination.back ? (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handlePageChange(pagination.page - 1)}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{t.previous || "Previous"}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button variant="ghost" size="sm" disabled>
|
|
||||||
{t.previous || "Previous"}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Page number buttons */}
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
{pageNumbers.map((pageNum) => (
|
|
||||||
<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>
|
|
||||||
|
|
||||||
{pagination.page < pagination.totalPages ? (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handlePageChange(pagination.page + 1)}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{t.next || "Next"}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button variant="ghost" size="sm" disabled>
|
|
||||||
{t.next || "Next"}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Page text display */}
|
|
||||||
<span className="px-4 py-1 text-sm text-muted-foreground">
|
|
||||||
{t.page || "Page"} {pagination.page} {t.of || "of"} {pagination.totalPages}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { PaginationBaseProps } from "./types";
|
|
||||||
|
|
||||||
interface PageSizeSelectorProps extends PaginationBaseProps {
|
|
||||||
pageSizeOptions?: number[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PageSizeSelector: React.FC<PageSizeSelectorProps> = ({
|
|
||||||
pagination,
|
|
||||||
updatePagination,
|
|
||||||
lang,
|
|
||||||
translations,
|
|
||||||
pageSizeOptions = [5, 10, 20, 50],
|
|
||||||
}) => {
|
|
||||||
const t = translations[lang] || {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
{t.itemsPerPage || "Items per page"}
|
|
||||||
</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>
|
|
||||||
{pageSizeOptions.map((size) => (
|
|
||||||
<SelectItem key={size} value={size.toString()}>
|
|
||||||
{size}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import { PaginationBaseProps } from "./types";
|
|
||||||
|
|
||||||
export const PaginationStats: React.FC<PaginationBaseProps> = ({
|
|
||||||
pagination,
|
|
||||||
lang,
|
|
||||||
translations,
|
|
||||||
}) => {
|
|
||||||
const t = translations[lang] || {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
<div>
|
|
||||||
{t.showing || "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 || "of"} {pagination.totalCount || pagination.allCount || 0} {t.items || "items"}
|
|
||||||
</div>
|
|
||||||
{pagination.totalCount &&
|
|
||||||
pagination.totalCount !== (pagination.allCount || 0) && (
|
|
||||||
<div>
|
|
||||||
{t.total || "Total"}: {pagination.allCount || 0} {t.items || "items"} ({t.filtered || "Filtered"}:{" "}
|
|
||||||
{pagination.totalCount} {t.items || "items"})
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import { PaginationBaseProps } from "./types";
|
|
||||||
import { PaginationStats } from "./PaginationStats";
|
|
||||||
import { PageNavigation } from "./PageNavigation";
|
|
||||||
import { PageSizeSelector } from "./PageSizeSelector";
|
|
||||||
|
|
||||||
export const PaginationToolsComponent: React.FC<PaginationBaseProps> = ({
|
|
||||||
pagination,
|
|
||||||
updatePagination,
|
|
||||||
loading = false,
|
|
||||||
lang,
|
|
||||||
translations,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-wrap justify-between items-center mt-6 gap-4">
|
|
||||||
{/* Pagination stats - left side */}
|
|
||||||
<PaginationStats
|
|
||||||
pagination={pagination}
|
|
||||||
updatePagination={updatePagination}
|
|
||||||
lang={lang}
|
|
||||||
translations={translations}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Navigation buttons - center */}
|
|
||||||
<PageNavigation
|
|
||||||
pagination={pagination}
|
|
||||||
updatePagination={updatePagination}
|
|
||||||
loading={loading}
|
|
||||||
lang={lang}
|
|
||||||
translations={translations}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Items per page selector - right side */}
|
|
||||||
<PageSizeSelector
|
|
||||||
pagination={pagination}
|
|
||||||
updatePagination={updatePagination}
|
|
||||||
lang={lang}
|
|
||||||
translations={translations}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export * from './PaginationToolsComponent';
|
|
||||||
export * from './PaginationStats';
|
|
||||||
export * from './PageNavigation';
|
|
||||||
export * from './PageSizeSelector';
|
|
||||||
export * from './types';
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { PagePagination } from "../hooks/useDataFetching";
|
|
||||||
|
|
||||||
export interface ResponseMetadata {
|
|
||||||
totalCount: number;
|
|
||||||
totalItems: number;
|
|
||||||
totalPages: number;
|
|
||||||
pageCount: number;
|
|
||||||
allCount?: number;
|
|
||||||
next: boolean;
|
|
||||||
back: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaginationBaseProps {
|
|
||||||
pagination: PagePagination;
|
|
||||||
updatePagination: (updates: Partial<PagePagination>) => void;
|
|
||||||
loading?: boolean;
|
|
||||||
lang: string;
|
|
||||||
translations: Record<string, any>;
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React, { useCallback } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { SelectQueryModifierProps } from "./types";
|
|
||||||
|
|
||||||
export const SelectQueryModifier: React.FC<SelectQueryModifierProps> = ({
|
|
||||||
fieldKey,
|
|
||||||
value,
|
|
||||||
label,
|
|
||||||
options,
|
|
||||||
placeholder,
|
|
||||||
onQueryChange,
|
|
||||||
translations,
|
|
||||||
lang,
|
|
||||||
}) => {
|
|
||||||
const t = translations[lang] || {};
|
|
||||||
|
|
||||||
const handleChange = useCallback((newValue: string) => {
|
|
||||||
const formattedValue = newValue.trim() ? `%${newValue.trim()}%` : null;
|
|
||||||
onQueryChange(`${fieldKey}__ilike`, formattedValue);
|
|
||||||
}, [fieldKey, onQueryChange]);
|
|
||||||
|
|
||||||
const handleClear = useCallback(() => {
|
|
||||||
onQueryChange(fieldKey, null);
|
|
||||||
onQueryChange(`${fieldKey}__ilike`, null);
|
|
||||||
}, [fieldKey, onQueryChange]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<label className="block text-xs font-medium">
|
|
||||||
{label || t[fieldKey] || fieldKey}
|
|
||||||
</label>
|
|
||||||
{value && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="h-5 px-2 text-xs"
|
|
||||||
onClick={handleClear}
|
|
||||||
>
|
|
||||||
{t.clear || "Clear"}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="w-full mt-1">
|
|
||||||
<Select
|
|
||||||
value={value}
|
|
||||||
onValueChange={handleChange}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full h-10">
|
|
||||||
<SelectValue
|
|
||||||
placeholder={placeholder || `${label || t[fieldKey] || fieldKey}...`}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{options.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React, { useCallback } from "react";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Search, X } from "lucide-react";
|
|
||||||
import { TextQueryModifierProps } from "./types";
|
|
||||||
|
|
||||||
export const TextQueryModifier: React.FC<TextQueryModifierProps> = ({
|
|
||||||
fieldKey,
|
|
||||||
value,
|
|
||||||
label,
|
|
||||||
placeholder,
|
|
||||||
onQueryChange,
|
|
||||||
translations,
|
|
||||||
lang,
|
|
||||||
}) => {
|
|
||||||
const t = translations[lang] || {};
|
|
||||||
|
|
||||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const newValue = e.target.value;
|
|
||||||
onQueryChange(fieldKey, newValue);
|
|
||||||
}, [fieldKey, onQueryChange]);
|
|
||||||
|
|
||||||
const handleClear = useCallback(() => {
|
|
||||||
// Clear both the regular field and the ilike filter
|
|
||||||
onQueryChange(fieldKey, null);
|
|
||||||
onQueryChange(`${fieldKey}__ilike`, null);
|
|
||||||
}, [fieldKey, onQueryChange]);
|
|
||||||
|
|
||||||
const handleKeyUp = useCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
// Apply the search immediately on Enter
|
|
||||||
const formattedValue = value.trim() ? `%${value.trim()}%` : null;
|
|
||||||
onQueryChange(`${fieldKey}__ilike`, formattedValue);
|
|
||||||
}
|
|
||||||
}, [fieldKey, value, onQueryChange]);
|
|
||||||
|
|
||||||
const handleSearch = useCallback(() => {
|
|
||||||
const formattedValue = value.trim() ? `%${value.trim()}%` : null;
|
|
||||||
onQueryChange(`${fieldKey}__ilike`, formattedValue);
|
|
||||||
}, [fieldKey, value, onQueryChange]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<label className="block text-xs font-medium mb-1">
|
|
||||||
{label || t[fieldKey] || fieldKey}
|
|
||||||
</label>
|
|
||||||
<div className="relative w-full flex">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
placeholder={placeholder || `${label || t[fieldKey] || fieldKey}...`}
|
|
||||||
value={value || ""}
|
|
||||||
onChange={handleChange}
|
|
||||||
onKeyUp={handleKeyUp}
|
|
||||||
className="pl-8 pr-8 w-full h-10"
|
|
||||||
/>
|
|
||||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
||||||
{value && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="absolute right-1 top-1 h-8 w-8"
|
|
||||||
onClick={handleClear}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
className="ml-2"
|
|
||||||
onClick={handleSearch}
|
|
||||||
>
|
|
||||||
<Search className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React, { useCallback, useEffect } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { User } from "lucide-react";
|
|
||||||
import { TypeQueryModifierProps } from "./types";
|
|
||||||
|
|
||||||
export const TypeQueryModifier: React.FC<TypeQueryModifierProps & { defaultValue?: string }> = ({
|
|
||||||
fieldKey,
|
|
||||||
value,
|
|
||||||
options,
|
|
||||||
onQueryChange,
|
|
||||||
translations,
|
|
||||||
lang,
|
|
||||||
defaultValue,
|
|
||||||
}) => {
|
|
||||||
const t = translations[lang] || {};
|
|
||||||
|
|
||||||
const handleTypeSelect = useCallback((selectedValue: string) => {
|
|
||||||
const formattedValue = selectedValue.trim() ? `%${selectedValue.trim()}%` : null;
|
|
||||||
onQueryChange(`${fieldKey}__ilike`, formattedValue);
|
|
||||||
}, [fieldKey, onQueryChange]);
|
|
||||||
|
|
||||||
// Apply default value on initial render if no value is set
|
|
||||||
useEffect(() => {
|
|
||||||
if (defaultValue && !value && options.some(opt => opt.value === defaultValue)) {
|
|
||||||
handleTypeSelect(defaultValue);
|
|
||||||
}
|
|
||||||
}, [defaultValue, value, options, handleTypeSelect]);
|
|
||||||
|
|
||||||
if (!options || options.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full flex flex-col space-y-4">
|
|
||||||
<div className="font-medium text-sm flex items-center">
|
|
||||||
<User className="mr-2 h-4 w-4" />
|
|
||||||
{t.typeSelection || "Type Selection"}
|
|
||||||
</div>
|
|
||||||
{options?.map((option) => (
|
|
||||||
<Button
|
|
||||||
key={option.value}
|
|
||||||
variant={value === option.value ? "default" : "outline"}
|
|
||||||
className="justify-start"
|
|
||||||
onClick={() => handleTypeSelect(option.value)}
|
|
||||||
>
|
|
||||||
{option.icon}
|
|
||||||
{option.label}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export * from './types';
|
|
||||||
export * from './TextQueryModifier';
|
|
||||||
export * from './SelectQueryModifier';
|
|
||||||
export * from './TypeQueryModifier';
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
|
|
||||||
export interface QueryModifierProps {
|
|
||||||
onQueryChange: (key: string, value: string | null) => void;
|
|
||||||
translations: Record<string, Record<string, string>>;
|
|
||||||
lang: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TextQueryModifierProps extends QueryModifierProps {
|
|
||||||
fieldKey: string;
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
placeholder?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SelectQueryModifierProps extends QueryModifierProps {
|
|
||||||
fieldKey: string;
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
options: { value: string; label: string }[];
|
|
||||||
placeholder?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TypeQueryModifierProps extends QueryModifierProps {
|
|
||||||
fieldKey: string;
|
|
||||||
value: string;
|
|
||||||
options: { value: string; label: string; icon?: React.ReactNode }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QueryModifierValue {
|
|
||||||
key: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type QueryModifierResult = Record<string, string>;
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
# EVYOS Management Frontend - Common Components
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This directory contains modular, reusable components for building consistent UIs across the EVYOS Management Frontend. These components follow a modular design pattern where complex functionality is broken down into smaller, focused components with clear responsibilities.
|
|
||||||
|
|
||||||
## Component Structure
|
|
||||||
|
|
||||||
### ActionButtonsDisplay
|
|
||||||
- **CreateButton**: A button component for triggering create actions with translation support
|
|
||||||
- **CustomButtonComponent**: Configurable button component with selection state support
|
|
||||||
- **types.ts**: Shared type definitions for button components
|
|
||||||
|
|
||||||
### CardDisplay
|
|
||||||
- **CardDisplay**: Main component for displaying data in a responsive grid layout
|
|
||||||
- **CardItem**: Individual card component with customizable fields and actions
|
|
||||||
- **CardSkeleton**: Loading state placeholder for cards
|
|
||||||
- **schema.ts**: API response and data schemas
|
|
||||||
- **utils.ts**: Helper functions for card operations
|
|
||||||
|
|
||||||
### FormDisplay
|
|
||||||
- **FormDisplay**: Container component that handles form mode switching
|
|
||||||
- **CreateComponent**: Form implementation for creating new records
|
|
||||||
- **UpdateComponent**: Form implementation for updating existing records
|
|
||||||
- **ViewComponent**: Read-only view of record details
|
|
||||||
- **types.ts**: Type definitions for form components and modes
|
|
||||||
|
|
||||||
### HeaderSelections
|
|
||||||
- **GridSelectionComponent**: Controls the number of columns in card grid layouts
|
|
||||||
- **LanguageSelectionComponent**: Language switcher with translation support
|
|
||||||
|
|
||||||
### PaginationModifiers
|
|
||||||
- **PaginationToolsComponent**: Main container for pagination controls
|
|
||||||
- **PaginationStats**: Displays record count information
|
|
||||||
- **PageNavigation**: Handles page navigation buttons with smart page number calculation
|
|
||||||
- **PageSizeSelector**: Controls items per page selection
|
|
||||||
- **types.ts**: Type definitions including ResponseMetadata interface
|
|
||||||
|
|
||||||
### QueryModifiers
|
|
||||||
- **TextQueryModifier**: Text search input with clear functionality
|
|
||||||
- **SelectQueryModifier**: Dropdown selection for filtering
|
|
||||||
- **TypeQueryModifier**: Button-based type selection
|
|
||||||
- **types.ts**: Shared interfaces for query components
|
|
||||||
|
|
||||||
### Hooks
|
|
||||||
- **useApiData**: Custom hook for fetching and managing API data with pagination
|
|
||||||
- **useDataFetching**: Base hook for data fetching with pagination, sorting, and filtering
|
|
||||||
|
|
||||||
## Usage Example
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
// Import components
|
|
||||||
import { CardDisplay } from "@/components/common/CardDisplay";
|
|
||||||
import { TextQueryModifier, SelectQueryModifier, TypeQueryModifier } from "@/components/common/QueryModifiers";
|
|
||||||
import { CreateButton } from "@/components/common/ActionButtonsDisplay/CreateButton";
|
|
||||||
import { PaginationToolsComponent } from "@/components/common/PaginationModifiers/PaginationToolsComponent";
|
|
||||||
import { GridSelectionComponent, GridSize } from "@/components/common/HeaderSelections/GridSelectionComponent";
|
|
||||||
import { LanguageSelectionComponent, Language } from "@/components/common/HeaderSelections/LanguageSelectionComponent";
|
|
||||||
import { FormDisplay } from "@/components/common/FormDisplay/FormDisplay";
|
|
||||||
import { useApiData } from "@/components/common/hooks/useApiData";
|
|
||||||
|
|
||||||
// Use the API data hook
|
|
||||||
const {
|
|
||||||
data,
|
|
||||||
pagination,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
updatePagination,
|
|
||||||
refetch
|
|
||||||
} = useApiData<YourDataType>("/api/your-endpoint");
|
|
||||||
|
|
||||||
// Define fields to display
|
|
||||||
const showFields = ["field1", "field2", "field3"];
|
|
||||||
|
|
||||||
// Example component usage
|
|
||||||
<Card>
|
|
||||||
<CardContent>
|
|
||||||
<TextQueryModifier
|
|
||||||
fieldKey="name"
|
|
||||||
value={pagination.query["name__ilike"] ? pagination.query["name__ilike"].replace(/%/g, "") : ""}
|
|
||||||
label="Search"
|
|
||||||
onQueryChange={handleQueryChange}
|
|
||||||
translations={translations}
|
|
||||||
lang={lang}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<CardDisplay
|
|
||||||
showFields={showFields}
|
|
||||||
data={data}
|
|
||||||
lang={lang}
|
|
||||||
translations={translations}
|
|
||||||
error={error}
|
|
||||||
loading={loading}
|
|
||||||
titleField="name"
|
|
||||||
onCardClick={handleCardClick}
|
|
||||||
gridCols={gridCols}
|
|
||||||
showViewIcon={true}
|
|
||||||
showUpdateIcon={true}
|
|
||||||
onViewClick={handleViewClick}
|
|
||||||
onUpdateClick={handleUpdateClick}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Response Structure
|
|
||||||
|
|
||||||
Components expect API responses in this format:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface ApiResponse<T> {
|
|
||||||
data: T[];
|
|
||||||
pagination: {
|
|
||||||
page: number;
|
|
||||||
size: number;
|
|
||||||
totalCount: number;
|
|
||||||
totalItems: number;
|
|
||||||
totalPages: number;
|
|
||||||
pageCount: number;
|
|
||||||
allCount?: number;
|
|
||||||
orderField: string[];
|
|
||||||
orderType: string[];
|
|
||||||
query: Record<string, any>;
|
|
||||||
next: boolean;
|
|
||||||
back: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB |
@@ -1,49 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { ArrowDown, ArrowUp } from "lucide-react";
|
|
||||||
import { SortingComponentProps, SortField } from "./types";
|
|
||||||
|
|
||||||
export const SortingComponent: React.FC<SortingComponentProps> = ({
|
|
||||||
sortField,
|
|
||||||
sortDirection,
|
|
||||||
onSort,
|
|
||||||
translations,
|
|
||||||
lang,
|
|
||||||
sortFields = [
|
|
||||||
{ key: "name", label: "Name" },
|
|
||||||
{ key: "code", label: "Code" },
|
|
||||||
{ key: "type", label: "Type" },
|
|
||||||
{ key: "created_at", label: "Created" },
|
|
||||||
],
|
|
||||||
}) => {
|
|
||||||
const t = translations?.[lang] || {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-wrap gap-2 my-4">
|
|
||||||
<div className="text-sm font-medium mr-2 flex items-center">
|
|
||||||
{t.sortBy || "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"
|
|
||||||
>
|
|
||||||
{t[field.key] || 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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './SortingComponent';
|
|
||||||
export * from './types';
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
export interface SortField {
|
|
||||||
key: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SortingComponentProps {
|
|
||||||
sortField: string | null;
|
|
||||||
sortDirection: "asc" | "desc" | null;
|
|
||||||
onSort: (field: string) => void;
|
|
||||||
translations?: Record<string, any>;
|
|
||||||
lang: string;
|
|
||||||
sortFields?: SortField[];
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import { useDataFetching, ApiResponse } from "./useDataFetching";
|
|
||||||
import { RequestParams } from "../schemas";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook for fetching data from Next.js API routes
|
|
||||||
* @param endpoint The API endpoint to fetch data from (e.g., '/api/applications')
|
|
||||||
* @param initialParams Initial request parameters
|
|
||||||
* @returns Object containing data, pagination, loading, error, updatePagination, and refetch
|
|
||||||
*/
|
|
||||||
export function useApiData<T>(
|
|
||||||
endpoint: string,
|
|
||||||
initialParams: Partial<RequestParams> = {}
|
|
||||||
) {
|
|
||||||
// Define the fetch function that will be passed to useDataFetching
|
|
||||||
const fetchFromApi = async (
|
|
||||||
params: RequestParams
|
|
||||||
): Promise<ApiResponse<T>> => {
|
|
||||||
try {
|
|
||||||
// Prepare the request body with pagination parameters
|
|
||||||
const requestBody = {
|
|
||||||
page: params.page,
|
|
||||||
size: params.size,
|
|
||||||
orderField: params.orderField,
|
|
||||||
orderType: params.orderType,
|
|
||||||
query: params.query,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Construct the list endpoint URL
|
|
||||||
const listEndpoint = `${endpoint}/list`;
|
|
||||||
|
|
||||||
// Make the API request using POST
|
|
||||||
const response = await fetch(listEndpoint, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(requestBody),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`API request failed with status ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching data from API:", error);
|
|
||||||
|
|
||||||
// Return empty data with pagination info on error
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
pagination: {
|
|
||||||
page: params.page,
|
|
||||||
size: params.size,
|
|
||||||
totalCount: 0,
|
|
||||||
totalItems: 0,
|
|
||||||
totalPages: 0,
|
|
||||||
pageCount: 0,
|
|
||||||
orderField: params.orderField,
|
|
||||||
orderType: params.orderType,
|
|
||||||
query: params.query,
|
|
||||||
next: false,
|
|
||||||
back: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use the generic data fetching hook with our API-specific fetch function
|
|
||||||
return useDataFetching<T>(fetchFromApi, initialParams);
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { retrievePageByUrl } from "@/eventRouters/pageRetriever";
|
|
||||||
import { PageProps } from "@/validations/translations/translation";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export interface DashboardPageParams {
|
|
||||||
pageUrl: string;
|
|
||||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DashboardPageResult {
|
|
||||||
activePage: string;
|
|
||||||
searchParamsInstance: { [key: string]: string | undefined };
|
|
||||||
lang: "en" | "tr";
|
|
||||||
PageComponent: React.FC<PageProps>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook to retrieve and prepare dashboard page data
|
|
||||||
* Throws errors for Next.js error boundary to catch
|
|
||||||
*
|
|
||||||
* @param params The dashboard page parameters
|
|
||||||
* @returns The processed dashboard page data
|
|
||||||
* @throws Error if page URL is invalid or page component is not found
|
|
||||||
*/
|
|
||||||
export async function useDashboardPage({
|
|
||||||
pageUrl,
|
|
||||||
searchParams,
|
|
||||||
}: DashboardPageParams): Promise<DashboardPageResult> {
|
|
||||||
let searchParamsInstance: { [key: string]: string | undefined } = {};
|
|
||||||
const defaultLang = "en";
|
|
||||||
|
|
||||||
if (!pageUrl || typeof pageUrl !== "string") {
|
|
||||||
throw new Error(`Invalid page URL: ${pageUrl}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
searchParamsInstance = await searchParams;
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error resolving search parameters:", err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lang = (searchParamsInstance?.lang as "en" | "tr") || defaultLang;
|
|
||||||
if (lang !== "en" && lang !== "tr") {
|
|
||||||
console.warn(
|
|
||||||
`Invalid language "${lang}" specified, falling back to "${defaultLang}"`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const PageComponent = retrievePageByUrl(pageUrl, lang);
|
|
||||||
if (!PageComponent) {
|
|
||||||
throw new Error(`Page component not found for URL: ${pageUrl}`);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
activePage: pageUrl,
|
|
||||||
searchParamsInstance,
|
|
||||||
lang,
|
|
||||||
PageComponent,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useDashboardPage;
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
|
||||||
import { RequestParams, ResponseMetadata } from "../schemas";
|
|
||||||
|
|
||||||
export interface PagePagination extends RequestParams, ResponseMetadata {}
|
|
||||||
|
|
||||||
export interface ApiResponse<T> {
|
|
||||||
data: T[];
|
|
||||||
pagination: PagePagination;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generic data fetching hook that can be used with any API endpoint
|
|
||||||
* @param fetchFunction - The API function to call for fetching data
|
|
||||||
* @param initialParams - Initial request parameters
|
|
||||||
* @returns Object containing data, pagination, loading, error, updatePagination, and refetch
|
|
||||||
*/
|
|
||||||
export function useDataFetching<T>(
|
|
||||||
fetchFunction: (params: RequestParams) => Promise<ApiResponse<T>>,
|
|
||||||
initialParams: Partial<RequestParams> = {}
|
|
||||||
) {
|
|
||||||
const [data, setData] = useState<T[]>([]);
|
|
||||||
|
|
||||||
// Request parameters - these are controlled by the user
|
|
||||||
const [requestParams, setRequestParams] = useState<RequestParams>({
|
|
||||||
page: initialParams.page || 1,
|
|
||||||
size: initialParams.size || 10,
|
|
||||||
orderField: initialParams.orderField || ["name"],
|
|
||||||
orderType: initialParams.orderType || ["asc"],
|
|
||||||
query: initialParams.query || {},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Response metadata - these come from the API
|
|
||||||
const [responseMetadata, setResponseMetadata] = useState<ResponseMetadata>({
|
|
||||||
totalCount: 0,
|
|
||||||
totalItems: 0,
|
|
||||||
totalPages: 0,
|
|
||||||
pageCount: 0,
|
|
||||||
next: true,
|
|
||||||
back: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<Error | null>(null);
|
|
||||||
|
|
||||||
const fetchDataFromApi = useCallback(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const result = await fetchFunction({
|
|
||||||
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,
|
|
||||||
next: result.pagination.next || false,
|
|
||||||
back: result.pagination.back || false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setError(null);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err : new Error("Unknown error"));
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
fetchFunction,
|
|
||||||
requestParams.page,
|
|
||||||
requestParams.size,
|
|
||||||
requestParams.orderField,
|
|
||||||
requestParams.orderType,
|
|
||||||
requestParams.query,
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Track if this is the initial mount
|
|
||||||
const initialMountRef = useRef(true);
|
|
||||||
|
|
||||||
// Track previous request params to avoid unnecessary fetches
|
|
||||||
const prevRequestParamsRef = useRef<RequestParams>(requestParams);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Only fetch on mount or when request params actually change
|
|
||||||
const paramsChanged =
|
|
||||||
JSON.stringify(prevRequestParamsRef.current) !==
|
|
||||||
JSON.stringify(requestParams);
|
|
||||||
|
|
||||||
if (initialMountRef.current || paramsChanged) {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
fetchDataFromApi();
|
|
||||||
initialMountRef.current = false;
|
|
||||||
prevRequestParamsRef.current = { ...requestParams };
|
|
||||||
}, 300); // Debounce
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, [fetchDataFromApi, requestParams]);
|
|
||||||
|
|
||||||
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,
|
|
||||||
next: true,
|
|
||||||
back: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
}));
|
|
||||||
fetchDataFromApi();
|
|
||||||
}, [fetchDataFromApi]);
|
|
||||||
|
|
||||||
// Combine request params and response metadata
|
|
||||||
const pagination: PagePagination = {
|
|
||||||
...requestParams,
|
|
||||||
...responseMetadata,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
pagination,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
updatePagination,
|
|
||||||
refetch,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cache options for the fetch request
|
|
||||||
*/
|
|
||||||
export type CacheOptions = {
|
|
||||||
/** Whether to cache the request (default: true) */
|
|
||||||
cache?: boolean;
|
|
||||||
/** Revalidate time in seconds (if not provided, uses Next.js defaults) */
|
|
||||||
revalidate?: number;
|
|
||||||
/** Force cache to be revalidated (equivalent to cache: 'no-store' in fetch) */
|
|
||||||
noStore?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request options for the fetch
|
|
||||||
*/
|
|
||||||
export type FetchOptions = {
|
|
||||||
/** HTTP method (default: 'GET') */
|
|
||||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
||||||
/** Request headers */
|
|
||||||
headers?: HeadersInit;
|
|
||||||
/** Request body (for POST, PUT, PATCH) */
|
|
||||||
body?: any;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A hook for fetching data from an API endpoint without pagination using Next.js fetch
|
|
||||||
* @param url The API endpoint URL
|
|
||||||
* @param initialParams Initial query parameters
|
|
||||||
* @param options Additional fetch options
|
|
||||||
* @param cacheOptions Cache control options
|
|
||||||
* @returns Object containing data, loading state, error state, and refetch function
|
|
||||||
*/
|
|
||||||
export function useStandardApiFetch<T>(
|
|
||||||
url: string,
|
|
||||||
initialParams: Record<string, any> = {},
|
|
||||||
options: FetchOptions = {},
|
|
||||||
cacheOptions: CacheOptions = { cache: true }
|
|
||||||
) {
|
|
||||||
const [data, setData] = useState<T | null>(null);
|
|
||||||
const [params, setParams] = useState<Record<string, any>>(initialParams);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<Error | null>(null);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds the URL with query parameters
|
|
||||||
*/
|
|
||||||
const buildUrl = () => {
|
|
||||||
const queryParams = new URLSearchParams();
|
|
||||||
|
|
||||||
// Add all non-null and non-empty params
|
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
|
||||||
if (value !== null && value !== '') {
|
|
||||||
queryParams.append(key, String(value));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const queryString = queryParams.toString();
|
|
||||||
return queryString ? `${url}?${queryString}` : url;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configure fetch options including cache settings
|
|
||||||
*/
|
|
||||||
const getFetchOptions = (): RequestInit => {
|
|
||||||
const { method = 'GET', headers = {}, body } = options;
|
|
||||||
|
|
||||||
const fetchOptions: RequestInit = {
|
|
||||||
method,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...headers,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add body for non-GET requests if provided
|
|
||||||
if (method !== 'GET' && body) {
|
|
||||||
fetchOptions.body = typeof body === 'string' ? body : JSON.stringify(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure cache options
|
|
||||||
if (!cacheOptions.cache) {
|
|
||||||
fetchOptions.cache = 'no-store';
|
|
||||||
} else if (cacheOptions.noStore) {
|
|
||||||
fetchOptions.cache = 'no-store';
|
|
||||||
} else if (cacheOptions.revalidate !== undefined) {
|
|
||||||
fetchOptions.next = { revalidate: cacheOptions.revalidate };
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetchOptions;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const fullUrl = buildUrl();
|
|
||||||
const fetchOptions = getFetchOptions();
|
|
||||||
|
|
||||||
const response = await fetch(fullUrl, fetchOptions);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const responseData = await response.json();
|
|
||||||
setData(responseData);
|
|
||||||
setError(null);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err : new Error('An unknown error occurred'));
|
|
||||||
setData(null);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchData();
|
|
||||||
}, [url, JSON.stringify(params), JSON.stringify(options), JSON.stringify(cacheOptions)]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the query parameters and trigger a refetch
|
|
||||||
* @param newParams New parameters to merge with existing ones
|
|
||||||
*/
|
|
||||||
const updateParams = (newParams: Record<string, any>) => {
|
|
||||||
// Filter out null or empty string values
|
|
||||||
const filteredParams = Object.entries(newParams).reduce((acc, [key, value]) => {
|
|
||||||
if (value !== null && value !== '') {
|
|
||||||
acc[key] = value;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, any>);
|
|
||||||
|
|
||||||
setParams(prev => ({
|
|
||||||
...prev,
|
|
||||||
...filteredParams
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset all parameters to initial values
|
|
||||||
*/
|
|
||||||
const resetParams = () => {
|
|
||||||
setParams(initialParams);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manually trigger a refetch of the data
|
|
||||||
*/
|
|
||||||
const refetch = () => {
|
|
||||||
fetchData();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
updateParams,
|
|
||||||
resetParams,
|
|
||||||
refetch
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// // Basic usage (with default caching)
|
|
||||||
// const { data, loading, error, refetch } = useStandardApiFetch<schema.YourDataType>('/api/your-endpoint');
|
|
||||||
|
|
||||||
// // With no caching (for data that changes frequently)
|
|
||||||
// const { data, loading, error, refetch } = useStandardApiFetch<schema.YourDataType>(
|
|
||||||
// '/api/your-endpoint',
|
|
||||||
// {},
|
|
||||||
// {},
|
|
||||||
// { cache: false }
|
|
||||||
// );
|
|
||||||
|
|
||||||
// // With specific revalidation time
|
|
||||||
// const { data, loading, error, refetch } = useStandardApiFetch<schema.YourDataType>(
|
|
||||||
// '/api/your-endpoint',
|
|
||||||
// {},
|
|
||||||
// {},
|
|
||||||
// { revalidate: 60 } // Revalidate every 60 seconds
|
|
||||||
// );
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
// Export all components from the common directory
|
|
||||||
export { CardDisplay } from "./CardDisplay";
|
|
||||||
export { SortingComponent, type SortingComponentProps, type SortField } from "./SortingComponent";
|
|
||||||
|
|
||||||
// Export QueryModifiers
|
|
||||||
export {
|
|
||||||
TextQueryModifier,
|
|
||||||
SelectQueryModifier,
|
|
||||||
TypeQueryModifier,
|
|
||||||
type QueryModifierProps,
|
|
||||||
type TextQueryModifierProps,
|
|
||||||
type SelectQueryModifierProps,
|
|
||||||
type TypeQueryModifierProps,
|
|
||||||
type QueryModifierValue,
|
|
||||||
type QueryModifierResult
|
|
||||||
} from "./QueryModifiers";
|
|
||||||
|
|
||||||
// Export hooks
|
|
||||||
export {
|
|
||||||
useDataFetching,
|
|
||||||
type RequestParams,
|
|
||||||
type ResponseMetadata,
|
|
||||||
type PagePagination,
|
|
||||||
type ApiResponse,
|
|
||||||
} from "./hooks/useDataFetching";
|
|
||||||
export { useApiData } from "./hooks/useApiData";
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
// Carried schemas from any request and response
|
|
||||||
|
|
||||||
// Common request parameters interface
|
|
||||||
export interface RequestParams {
|
|
||||||
page: number;
|
|
||||||
size: number;
|
|
||||||
orderField: string[];
|
|
||||||
orderType: string[];
|
|
||||||
query: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Common response metadata interface
|
|
||||||
export interface ResponseMetadata {
|
|
||||||
totalCount: number;
|
|
||||||
totalItems: number;
|
|
||||||
totalPages: number;
|
|
||||||
pageCount: number;
|
|
||||||
allCount?: number;
|
|
||||||
next: boolean;
|
|
||||||
back: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generic API response interface
|
|
||||||
export interface ApiResponse<T> {
|
|
||||||
data: T[];
|
|
||||||
metadata: ResponseMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pagination state interface
|
|
||||||
export interface PagePagination {
|
|
||||||
page: number;
|
|
||||||
size: number;
|
|
||||||
orderField: string[];
|
|
||||||
orderType: string[];
|
|
||||||
query: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Language = "en" | "tr";
|
|
||||||
|
|
||||||
export interface LanguageSelectionComponentProps {
|
|
||||||
lang: Language;
|
|
||||||
setLang: (lang: Language) => void;
|
|
||||||
translations?: Record<string, any>;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import { retrieveAccessToken } from "@/apicalls/cookies/token";
|
import { retrieveAccessToken } from "@/apicalls/cookies/endpoints";
|
||||||
import {
|
import {
|
||||||
DEFAULT_RESPONSE,
|
DEFAULT_RESPONSE,
|
||||||
defaultHeaders,
|
defaultHeaders,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { fetchDataWithToken } from "../api-fetcher";
|
import { fetchDataWithToken } from "../api-fetcher";
|
||||||
import { baseUrlAuth, tokenSecret } from "../basics";
|
import { baseUrlAuth, tokenSecret } from "../basics";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
import NextCrypto from "next-crypto";
|
import NextCrypto from "next-crypto";
|
||||||
|
|
||||||
const checkToken = `${baseUrlAuth}/authentication/token/check`;
|
const checkToken = `${baseUrlAuth}/authentication/token/check`;
|
||||||
@@ -17,14 +16,14 @@ async function checkAccessTokenIsValid() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function retrievePageList() {
|
async function retrievePageList() {
|
||||||
const response = await fetchDataWithToken(siteUrls, {}, "GET", false);
|
const response: any = await fetchDataWithToken(siteUrls, {}, "GET", false);
|
||||||
return response?.status === 200 || response?.status === 202
|
return response?.status === 200 || response?.status === 202
|
||||||
? response.data?.sites
|
? response.data?.sites
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function retrievePagebyUrl(pageUrl: string) {
|
async function retrievePagebyUrl(pageUrl: string) {
|
||||||
const response = await fetchDataWithToken(
|
const response: any = await fetchDataWithToken(
|
||||||
pageValid,
|
pageValid,
|
||||||
{
|
{
|
||||||
page_url: pageUrl,
|
page_url: pageUrl,
|
||||||
@@ -88,15 +87,10 @@ async function retrieveUserSelection() {
|
|||||||
} else if (decrpytUserSelection?.user_type === "occupant") {
|
} else if (decrpytUserSelection?.user_type === "occupant") {
|
||||||
const buildingsList = accessObjects?.selectionList;
|
const buildingsList = accessObjects?.selectionList;
|
||||||
|
|
||||||
// Iterate through all buildings
|
|
||||||
if (buildingsList) {
|
if (buildingsList) {
|
||||||
// Loop through each building
|
|
||||||
for (const buildKey in buildingsList) {
|
for (const buildKey in buildingsList) {
|
||||||
const building = buildingsList[buildKey];
|
const building = buildingsList[buildKey];
|
||||||
|
|
||||||
// Check if the building has occupants
|
|
||||||
if (building.occupants && building.occupants.length > 0) {
|
if (building.occupants && building.occupants.length > 0) {
|
||||||
// Find the occupant with the matching build_living_space_uu_id
|
|
||||||
const occupant = building.occupants.find(
|
const occupant = building.occupants.find(
|
||||||
(occ: any) => occ.build_living_space_uu_id === userSelection
|
(occ: any) => occ.build_living_space_uu_id === userSelection
|
||||||
);
|
);
|
||||||
@@ -121,21 +115,6 @@ async function retrieveUserSelection() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// const avatarInfo = await retrieveAvatarInfo();
|
|
||||||
// lang: avatarInfo?.data?.lang
|
|
||||||
// ? String(avatarInfo?.data?.lang).toLowerCase()
|
|
||||||
// : undefined,
|
|
||||||
// avatar: avatarInfo?.data?.avatar,
|
|
||||||
// fullName: avatarInfo?.data?.full_name,
|
|
||||||
// async function retrieveAvatarInfo() {
|
|
||||||
// const response = await fetchDataWithToken(
|
|
||||||
// `${baseUrlAuth}/authentication/avatar`,
|
|
||||||
// {},
|
|
||||||
// "POST"
|
|
||||||
// );
|
|
||||||
// return response;
|
|
||||||
// }
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
checkAccessTokenIsValid,
|
checkAccessTokenIsValid,
|
||||||
retrieveAccessToken,
|
retrieveAccessToken,
|
||||||
@@ -144,5 +123,4 @@ export {
|
|||||||
retrieveUserSelection,
|
retrieveUserSelection,
|
||||||
retrievePagebyUrl,
|
retrievePagebyUrl,
|
||||||
retrievePageList,
|
retrievePageList,
|
||||||
// retrieveavailablePages,
|
|
||||||
};
|
};
|
||||||
@@ -53,7 +53,7 @@ async function loginViaAccessKeys(payload: LoginViaAccessKeys) {
|
|||||||
);
|
);
|
||||||
console.log("response", response);
|
console.log("response", response);
|
||||||
if (response.status === 200 || response.status === 202) {
|
if (response.status === 200 || response.status === 202) {
|
||||||
const loginRespone = response?.data;
|
const loginRespone: any = response?.data;
|
||||||
const accessToken = await nextCrypto.encrypt(loginRespone.access_token);
|
const accessToken = await nextCrypto.encrypt(loginRespone.access_token);
|
||||||
const accessObject = await nextCrypto.encrypt(
|
const accessObject = await nextCrypto.encrypt(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { fetchDataWithToken } from "../api-fetcher";
|
||||||
|
import { baseUrlApplication } from "../basics";
|
||||||
|
import { PaginationParams } from "../schemas/list";
|
||||||
|
import type { PaginatedApiResponse } from "@/app/api/utils/types";
|
||||||
|
|
||||||
|
const servicesListEndpoint = `${baseUrlApplication}/service/list`;
|
||||||
|
|
||||||
|
async function listServices(payload: PaginationParams): Promise<PaginatedApiResponse<any>> {
|
||||||
|
try {
|
||||||
|
const response = await fetchDataWithToken(
|
||||||
|
servicesListEndpoint,
|
||||||
|
{
|
||||||
|
page: payload.page,
|
||||||
|
size: payload.size,
|
||||||
|
order_field: payload.orderField,
|
||||||
|
order_type: payload.orderType,
|
||||||
|
query: payload.query,
|
||||||
|
},
|
||||||
|
"POST",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response?.status === 200 || response?.status === 202) {
|
||||||
|
const responseData = response.data as PaginatedApiResponse<any>;
|
||||||
|
return {
|
||||||
|
data: responseData.data || [],
|
||||||
|
pagination: {
|
||||||
|
page: responseData.pagination?.page || 1,
|
||||||
|
size: responseData.pagination?.size || 10,
|
||||||
|
totalCount: responseData.pagination?.totalCount || 0,
|
||||||
|
totalItems: responseData.pagination?.totalItems || 0,
|
||||||
|
totalPages: responseData.pagination?.totalPages || 0,
|
||||||
|
pageCount: responseData.pagination?.pageCount || 0,
|
||||||
|
orderField: responseData.pagination?.orderField || ['name'],
|
||||||
|
orderType: responseData.pagination?.orderType || ['asc'],
|
||||||
|
query: responseData.pagination?.query || {},
|
||||||
|
next: responseData.pagination?.next || false,
|
||||||
|
back: responseData.pagination?.back || false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
totalCount: 0,
|
||||||
|
totalItems: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
pageCount: 0,
|
||||||
|
orderField: ['name'],
|
||||||
|
orderType: ['asc'],
|
||||||
|
query: {},
|
||||||
|
next: false,
|
||||||
|
back: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching application list:", error);
|
||||||
|
// Return a default empty response instead of null to match the expected return type
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
totalCount: 0,
|
||||||
|
totalItems: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
pageCount: 0,
|
||||||
|
orderField: ['name'],
|
||||||
|
orderType: ['asc'],
|
||||||
|
query: {},
|
||||||
|
next: false,
|
||||||
|
back: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
listServices,
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ import React from "react";
|
|||||||
import {
|
import {
|
||||||
checkAccessTokenIsValid,
|
checkAccessTokenIsValid,
|
||||||
retrieveUserType,
|
retrieveUserType,
|
||||||
} from "@/apicalls/cookies/token";
|
} from "@/apicalls/cookies/endpoints";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import LoginEmployee from "@/components/auth/LoginEmployee";
|
import LoginEmployee from "@/components/auth/LoginEmployee";
|
||||||
import LoginOccupant from "@/components/auth/LoginOccupant";
|
import LoginOccupant from "@/components/auth/LoginOccupant";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { checkAccessTokenIsValid } from "@/apicalls/cookies/token";
|
import { checkAccessTokenIsValid } from "@/apicalls/cookies/endpoints";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { listApplications } from "@/apicalls/application/endpoints";
|
||||||
|
import { createListHandler } from "@/app/api/utils";
|
||||||
|
|
||||||
|
export const POST = createListHandler(listApplications);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createCreateHandler } from "@/app/api/utils";
|
import { createCreateHandler } from "@/app/api/utils";
|
||||||
import { createApplication } from "@/apicalls/application/application";
|
import { createApplication } from "@/apicalls/application/endpoints";
|
||||||
|
|
||||||
export const POST = createCreateHandler(createApplication);
|
export const POST = createCreateHandler(createApplication);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { listApplications } from "@/apicalls/application/application";
|
import { listApplications } from "@/apicalls/application/endpoints";
|
||||||
import { createListHandler } from "@/app/api/utils";
|
import { createListHandler } from "@/app/api/utils";
|
||||||
|
|
||||||
export const POST = createListHandler(listApplications);
|
export const POST = createListHandler(listApplications);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createUpdateHandler } from "../../utils";
|
import { createUpdateHandler } from "../../utils";
|
||||||
import { updateApplication } from "@/apicalls/application/application";
|
import { updateApplication } from "@/apicalls/application/endpoints";
|
||||||
|
|
||||||
export const POST = createUpdateHandler(updateApplication);
|
export const POST = createUpdateHandler(updateApplication);
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { listApplications } from "@/apicalls/application/endpoints";
|
||||||
|
import { createListHandler } from "@/app/api/utils";
|
||||||
|
|
||||||
|
export const POST = createListHandler(listApplications);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { listServices } from "@/apicalls/services/endpoints";
|
||||||
|
import { createListHandler } from "@/app/api/utils";
|
||||||
|
|
||||||
|
export const POST = createListHandler(listServices);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { loginSelectEmployee } from "@/apicalls/login/login";
|
import { loginSelectEmployee } from "@/apicalls/login/endpoints";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Company } from "./types";
|
import { Company } from "./types";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { loginSelectOccupant } from "@/apicalls/login/login";
|
import { loginSelectOccupant } from "@/apicalls/login/endpoints";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { BuildingMap } from "./types";
|
import { BuildingMap } from "./types";
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useState, useTransition } from "react";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { loginViaAccessKeys } from "@/apicalls/login/login";
|
import { loginViaAccessKeys } from "@/apicalls/login/endpoints";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const loginSchema = z.object({
|
const loginSchema = z.object({
|
||||||
@@ -53,8 +53,8 @@ function Login() {
|
|||||||
}
|
}
|
||||||
return dataResponse;
|
return dataResponse;
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => { });
|
||||||
} catch (error) {}
|
} catch (error) { }
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Login error:", error);
|
console.error("Login error:", error);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React from "react";
|
|||||||
import {
|
import {
|
||||||
loginSelectEmployee,
|
loginSelectEmployee,
|
||||||
loginSelectOccupant,
|
loginSelectOccupant,
|
||||||
} from "@/apicalls/login/login";
|
} from "@/apicalls/login/endpoints";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import LoginEmployee from "./LoginEmployee";
|
import LoginEmployee from "./LoginEmployee";
|
||||||
import LoginOccupant from "./LoginOccupant";
|
import LoginOccupant from "./LoginOccupant";
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { CardItem } from "./CardItem";
|
|||||||
import { CardSkeleton } from "./CardSkeleton";
|
import { CardSkeleton } from "./CardSkeleton";
|
||||||
import { getFieldValue, getGridClasses } from "./utils";
|
import { getFieldValue, getGridClasses } from "./utils";
|
||||||
import { CardDisplayProps } from "./schema";
|
import { CardDisplayProps } from "./schema";
|
||||||
|
import { GridSize } from "../HeaderSelections/GridSelectionComponent";
|
||||||
|
|
||||||
export function CardDisplay<T>({
|
export function CardDisplay<T>({
|
||||||
showFields,
|
showFields,
|
||||||
@@ -12,7 +13,7 @@ export function CardDisplay<T>({
|
|||||||
translations,
|
translations,
|
||||||
error,
|
error,
|
||||||
loading,
|
loading,
|
||||||
titleField = "name",
|
titleField,
|
||||||
onCardClick,
|
onCardClick,
|
||||||
renderCustomField,
|
renderCustomField,
|
||||||
gridCols = 4,
|
gridCols = 4,
|
||||||
@@ -30,7 +31,7 @@ export function CardDisplay<T>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={getGridClasses(gridCols)}>
|
<div className={getGridClasses(gridCols as GridSize)}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
// Loading skeletons
|
// Loading skeletons
|
||||||
Array.from({ length: 10 }).map((_, index) => (
|
Array.from({ length: 10 }).map((_, index) => (
|
||||||
@@ -44,7 +45,7 @@ export function CardDisplay<T>({
|
|||||||
))
|
))
|
||||||
) : data.length === 0 ? (
|
) : data.length === 0 ? (
|
||||||
<div className="col-span-full text-center py-6">
|
<div className="col-span-full text-center py-6">
|
||||||
{(translations[lang] || {}).noData || "No data found"}
|
{translations[lang].noData || "No data found"}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
data.map((item: T, index: number) => (
|
data.map((item: T, index: number) => (
|
||||||
@@ -56,10 +57,10 @@ export function CardDisplay<T>({
|
|||||||
titleField={titleField}
|
titleField={titleField}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
translations={translations}
|
translations={translations}
|
||||||
onCardClick={onCardClick}
|
|
||||||
renderCustomField={renderCustomField}
|
|
||||||
showViewIcon={showViewIcon}
|
showViewIcon={showViewIcon}
|
||||||
showUpdateIcon={showUpdateIcon}
|
showUpdateIcon={showUpdateIcon}
|
||||||
|
onCardClick={onCardClick}
|
||||||
|
renderCustomField={renderCustomField}
|
||||||
onViewClick={onViewClick}
|
onViewClick={onViewClick}
|
||||||
onUpdateClick={onUpdateClick}
|
onUpdateClick={onUpdateClick}
|
||||||
getFieldValue={getFieldValue}
|
getFieldValue={getFieldValue}
|
||||||
|
|||||||
@@ -24,16 +24,16 @@ export function CardItem<T>({
|
|||||||
onUpdateClick,
|
onUpdateClick,
|
||||||
getFieldValue,
|
getFieldValue,
|
||||||
}: CardItemProps<T>) {
|
}: CardItemProps<T>) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<div key={index} className="w-full p-1">
|
<div key={index} className="w-full p-1">
|
||||||
<Card
|
<Card
|
||||||
className={`h-full ${onCardClick ? 'cursor-pointer hover:shadow-md transition-shadow' : ''}`}
|
className={`h-full ${onCardClick ? 'cursor-pointer hover:shadow-md transition-shadow' : ''}`}
|
||||||
onClick={onCardClick ? () => onCardClick(item) : undefined}
|
onClick={onCardClick ? () => onCardClick(item) : undefined}
|
||||||
>
|
>
|
||||||
<CardHeader className="p-3 pb-0 flex justify-between items-start">
|
<CardHeader className="p-3 pb-0 flex justify-between items-start">
|
||||||
<h3 className="text-lg font-semibold">
|
<h3 className="text-lg font-semibold">{getFieldValue(item, titleField)}</h3>
|
||||||
{getFieldValue(item, titleField)}
|
|
||||||
</h3>
|
|
||||||
<CardActions
|
<CardActions
|
||||||
item={item}
|
item={item}
|
||||||
showViewIcon={showViewIcon}
|
showViewIcon={showViewIcon}
|
||||||
|
|||||||
@@ -1,117 +1,57 @@
|
|||||||
/**
|
import { GridSize } from "../HeaderSelections/GridSelectionComponent";
|
||||||
* CardDisplay component interfaces
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main props for the CardDisplay component
|
|
||||||
*/
|
|
||||||
export interface CardDisplayProps<T> {
|
export interface CardDisplayProps<T> {
|
||||||
/** Fields to display in each card */
|
|
||||||
showFields: string[];
|
showFields: string[];
|
||||||
/** Array of data items to display */
|
|
||||||
data: T[];
|
data: T[];
|
||||||
/** Current language code */
|
|
||||||
lang: string;
|
lang: string;
|
||||||
/** Translations object for field labels and messages */
|
|
||||||
translations: Record<string, any>;
|
translations: Record<string, any>;
|
||||||
/** Error object if data fetching failed */
|
|
||||||
error: Error | null;
|
error: Error | null;
|
||||||
/** Loading state indicator */
|
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
/** Field to use as the card title (default: "name") */
|
|
||||||
titleField?: string;
|
|
||||||
/** Handler for when a card is clicked */
|
|
||||||
onCardClick?: (item: T) => void;
|
|
||||||
/** Custom renderer for specific fields */
|
|
||||||
renderCustomField?: (item: T, field: string) => React.ReactNode;
|
|
||||||
/** Number of columns in the grid (1-6) */
|
|
||||||
gridCols?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
||||||
/** Whether to show the view icon */
|
|
||||||
showViewIcon?: boolean;
|
|
||||||
/** Whether to show the update/edit icon */
|
|
||||||
showUpdateIcon?: boolean;
|
|
||||||
/** Handler for when the view icon is clicked */
|
|
||||||
onViewClick?: (item: T) => void;
|
|
||||||
/** Handler for when the update/edit icon is clicked */
|
|
||||||
onUpdateClick?: (item: T) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Props for the CardItem component
|
|
||||||
*/
|
|
||||||
export interface CardItemProps<T> {
|
|
||||||
/** Data item to display */
|
|
||||||
item: T;
|
|
||||||
/** Index of the item in the data array */
|
|
||||||
index: number;
|
|
||||||
/** Fields to display in the card */
|
|
||||||
showFields: string[];
|
|
||||||
/** Field to use as the card title */
|
|
||||||
titleField: string;
|
titleField: string;
|
||||||
/** Current language code */
|
|
||||||
lang: string;
|
|
||||||
/** Translations object for field labels */
|
|
||||||
translations: Record<string, any>;
|
|
||||||
/** Handler for when the card is clicked */
|
|
||||||
onCardClick?: (item: T) => void;
|
onCardClick?: (item: T) => void;
|
||||||
/** Custom renderer for specific fields */
|
|
||||||
renderCustomField?: (item: T, field: string) => React.ReactNode;
|
renderCustomField?: (item: T, field: string) => React.ReactNode;
|
||||||
/** Whether to show the view icon */
|
gridCols?: number | GridSize;
|
||||||
showViewIcon: boolean;
|
showViewIcon?: boolean;
|
||||||
/** Whether to show the update/edit icon */
|
showUpdateIcon?: boolean;
|
||||||
showUpdateIcon: boolean;
|
|
||||||
/** Handler for when the view icon is clicked */
|
|
||||||
onViewClick?: (item: T) => void;
|
onViewClick?: (item: T) => void;
|
||||||
/** Handler for when the update/edit icon is clicked */
|
|
||||||
onUpdateClick?: (item: T) => void;
|
|
||||||
/** Function to get field values from the item */
|
|
||||||
getFieldValue: (item: any, field: string) => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Props for the CardActions component
|
|
||||||
*/
|
|
||||||
export interface CardActionsProps<T> {
|
|
||||||
/** Data item the actions apply to */
|
|
||||||
item: T;
|
|
||||||
/** Whether to show the view icon */
|
|
||||||
showViewIcon: boolean;
|
|
||||||
/** Whether to show the update/edit icon */
|
|
||||||
showUpdateIcon: boolean;
|
|
||||||
/** Handler for when the view icon is clicked */
|
|
||||||
onViewClick?: (item: T) => void;
|
|
||||||
/** Handler for when the update/edit icon is clicked */
|
|
||||||
onUpdateClick?: (item: T) => void;
|
onUpdateClick?: (item: T) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export interface CardItemProps<T> {
|
||||||
* Props for the CardField component
|
|
||||||
*/
|
|
||||||
export interface CardFieldProps<T> {
|
|
||||||
/** Data item the field belongs to */
|
|
||||||
item: T;
|
item: T;
|
||||||
/** Field name to display */
|
|
||||||
field: string;
|
|
||||||
/** Current language code */
|
|
||||||
lang: string;
|
|
||||||
/** Translations object for field labels */
|
|
||||||
translations: Record<string, any>;
|
|
||||||
/** Custom renderer for specific fields */
|
|
||||||
renderCustomField?: (item: T, field: string) => React.ReactNode;
|
|
||||||
/** Function to get field values from the item */
|
|
||||||
getFieldValue: (item: any, field: string) => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Props for the CardSkeleton component
|
|
||||||
*/
|
|
||||||
export interface CardSkeletonProps {
|
|
||||||
/** Index of the skeleton in the loading array */
|
|
||||||
index: number;
|
index: number;
|
||||||
/** Fields to create skeleton placeholders for */
|
|
||||||
showFields: string[];
|
showFields: string[];
|
||||||
/** Whether to show a skeleton for the view icon */
|
titleField: string;
|
||||||
|
lang: string;
|
||||||
|
translations: Record<string, any>;
|
||||||
|
onCardClick?: (item: T) => void;
|
||||||
|
renderCustomField?: (item: T, field: string) => React.ReactNode;
|
||||||
|
showViewIcon: boolean;
|
||||||
|
showUpdateIcon: boolean;
|
||||||
|
onViewClick?: (item: T) => void;
|
||||||
|
onUpdateClick?: (item: T) => void;
|
||||||
|
getFieldValue: (item: any, field: string) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardActionsProps<T> {
|
||||||
|
item: T;
|
||||||
|
showViewIcon: boolean;
|
||||||
|
showUpdateIcon: boolean;
|
||||||
|
onViewClick?: (item: T) => void;
|
||||||
|
onUpdateClick?: (item: T) => void;
|
||||||
|
}
|
||||||
|
export interface CardFieldProps<T> {
|
||||||
|
item: T;
|
||||||
|
field: string;
|
||||||
|
lang: string;
|
||||||
|
translations: Record<string, any>;
|
||||||
|
renderCustomField?: (item: T, field: string) => React.ReactNode;
|
||||||
|
getFieldValue: (item: any, field: string) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardSkeletonProps {
|
||||||
|
index: number;
|
||||||
|
showFields: string[];
|
||||||
showViewIcon: boolean;
|
showViewIcon: boolean;
|
||||||
/** Whether to show a skeleton for the update/edit icon */
|
|
||||||
showUpdateIcon: boolean;
|
showUpdateIcon: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
/**
|
|
||||||
* Safely gets a field value from an item, supporting nested fields with dot notation
|
|
||||||
*/
|
|
||||||
export function getFieldValue(item: any, field: string): any {
|
export function getFieldValue(item: any, field: string): any {
|
||||||
if (!item) return "";
|
if (!item) return "";
|
||||||
|
|
||||||
// Handle nested fields with dot notation (e.g., "user.name")
|
|
||||||
if (field.includes(".")) {
|
if (field.includes(".")) {
|
||||||
const parts = field.split(".");
|
const parts = field.split(".");
|
||||||
let value = item;
|
let value = item;
|
||||||
@@ -14,32 +9,30 @@ export function getFieldValue(item: any, field: string): any {
|
|||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return item[field];
|
return item[field];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function getFieldLabel(
|
||||||
* Gets a field label from translations or formats the field name
|
field: string,
|
||||||
*/
|
translations: Record<string, any>,
|
||||||
export function getFieldLabel(field: string, translations: Record<string, any>, lang: string): string {
|
lang: string
|
||||||
|
): string {
|
||||||
const t = translations[lang] || {};
|
const t = translations[lang] || {};
|
||||||
return t[field] || field.charAt(0).toUpperCase() + field.slice(1).replace(/_/g, " ");
|
return (
|
||||||
|
t[field] ||
|
||||||
|
field.charAt(0).toUpperCase() + field.slice(1).replace(/_/g, " ")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates responsive grid classes based on the gridCols prop
|
|
||||||
*/
|
|
||||||
export function getGridClasses(gridCols: 1 | 2 | 3 | 4 | 5 | 6): string {
|
export function getGridClasses(gridCols: 1 | 2 | 3 | 4 | 5 | 6): string {
|
||||||
const baseClass = "grid grid-cols-1 gap-4";
|
const baseClass = "grid grid-cols-1 gap-4";
|
||||||
|
|
||||||
// Map gridCols to responsive classes
|
|
||||||
const colClasses: Record<number, string> = {
|
const colClasses: Record<number, string> = {
|
||||||
1: "",
|
1: "",
|
||||||
2: "sm:grid-cols-2",
|
2: "sm:grid-cols-2",
|
||||||
3: "sm:grid-cols-2 md:grid-cols-3",
|
3: "sm:grid-cols-2 md:grid-cols-3",
|
||||||
4: "sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4",
|
4: "sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4",
|
||||||
5: "sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5",
|
5: "sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5",
|
||||||
6: "sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6"
|
6: "sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6",
|
||||||
};
|
};
|
||||||
|
|
||||||
return `${baseClass} ${colClasses[gridCols]}`;
|
return `${baseClass} ${colClasses[gridCols]}`;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export function FormDisplay<T>({
|
|||||||
}: FormDisplayProps<T>) {
|
}: FormDisplayProps<T>) {
|
||||||
const [enhancedFormProps, setEnhancedFormProps] = useState(formProps);
|
const [enhancedFormProps, setEnhancedFormProps] = useState(formProps);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateFormProps = async () => {
|
const updateFormProps = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -91,12 +92,10 @@ export function FormDisplay<T>({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case "update":
|
case "update":
|
||||||
// Create a stable key for the component to ensure proper re-rendering
|
|
||||||
const updateKey = `update-${lang}-${(initialData as any)?.uu_id || 'new'}`;
|
const updateKey = `update-${lang}-${(initialData as any)?.uu_id || 'new'}`;
|
||||||
|
|
||||||
return initialData ? (
|
return initialData ? (
|
||||||
<UpdateComponent<T>
|
<UpdateComponent<T>
|
||||||
key={updateKey} // Add key with lang and item ID to force re-render
|
key={updateKey}
|
||||||
initialData={initialData}
|
initialData={initialData}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
setMode={setMode}
|
setMode={setMode}
|
||||||
@@ -111,7 +110,7 @@ export function FormDisplay<T>({
|
|||||||
case "view":
|
case "view":
|
||||||
return initialData ? (
|
return initialData ? (
|
||||||
<ViewComponent<T>
|
<ViewComponent<T>
|
||||||
key={`view-${lang}`} // Add key with lang to force re-render on language change
|
key={`view-${lang}`}
|
||||||
initialData={initialData}
|
initialData={initialData}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
setMode={setMode}
|
setMode={setMode}
|
||||||
@@ -119,7 +118,7 @@ export function FormDisplay<T>({
|
|||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
translations={translations}
|
translations={translations}
|
||||||
formProps={enhancedFormProps} // Changed from formProps to enhancedFormProps for consistency
|
formProps={enhancedFormProps}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -8,12 +8,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
|
|
||||||
// Utility function to format field label
|
// Utility function to format field label
|
||||||
const formatFieldLabel = (fieldName: string) =>
|
const formatFieldLabel = (fieldName: string) => fieldName.replace(/_/g, ' ').replace(/^./, (str) => str.toUpperCase()).replace(/\b\w/g, (c) => c.toUpperCase());
|
||||||
fieldName
|
|
||||||
.replace(/([A-Z])/g, ' $1')
|
|
||||||
.replace(/_/g, ' ')
|
|
||||||
.replace(/^./, (str) => str.toUpperCase())
|
|
||||||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
||||||
|
|
||||||
// Component for rendering a single field
|
// Component for rendering a single field
|
||||||
const ViewField: React.FC<{
|
const ViewField: React.FC<{
|
||||||
@@ -55,9 +50,7 @@ const ViewField: React.FC<{
|
|||||||
<div className={`rounded-md border ${hasError ? 'border-red-500' : 'border-input'} bg-background px-3 py-2 text-sm min-h-[2.5rem] flex items-center`}>
|
<div className={`rounded-md border ${hasError ? 'border-red-500' : 'border-input'} bg-background px-3 py-2 text-sm min-h-[2.5rem] flex items-center`}>
|
||||||
{formatFieldValue()}
|
{formatFieldValue()}
|
||||||
</div>
|
</div>
|
||||||
{hasError && (
|
{hasError && <p className="text-sm text-red-500">{hasError}</p>}
|
||||||
<p className="text-sm text-red-500">{hasError}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -71,9 +64,7 @@ const ViewFieldGroup: React.FC<{
|
|||||||
translations: any;
|
translations: any;
|
||||||
validationErrors: Record<string, string>;
|
validationErrors: Record<string, string>;
|
||||||
}> = ({ groupName, fields, initialData, lang, translations, validationErrors }) => {
|
}> = ({ groupName, fields, initialData, lang, translations, validationErrors }) => {
|
||||||
const getGroupTitle = (name: string) => {
|
const getGroupTitle = (name: string) => { return translations[name] || formatFieldLabel(name); };
|
||||||
return translations[name] || formatFieldLabel(name);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="shadow-sm">
|
<Card className="shadow-sm">
|
||||||
@@ -86,7 +77,6 @@ const ViewFieldGroup: React.FC<{
|
|||||||
const fieldName = field.name || "";
|
const fieldName = field.name || "";
|
||||||
const value = initialData ? (initialData as any)[fieldName] : undefined;
|
const value = initialData ? (initialData as any)[fieldName] : undefined;
|
||||||
const hasError = validationErrors[fieldName];
|
const hasError = validationErrors[fieldName];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewField
|
<ViewField
|
||||||
key={fieldName}
|
key={fieldName}
|
||||||
@@ -105,7 +95,6 @@ const ViewFieldGroup: React.FC<{
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Main ViewComponent
|
|
||||||
export function ViewComponent<T>({
|
export function ViewComponent<T>({
|
||||||
initialData,
|
initialData,
|
||||||
setMode,
|
setMode,
|
||||||
@@ -117,26 +106,19 @@ export function ViewComponent<T>({
|
|||||||
}: ViewComponentProps<T>) {
|
}: ViewComponentProps<T>) {
|
||||||
const t = translations[lang as keyof typeof translations] || {};
|
const t = translations[lang as keyof typeof translations] || {};
|
||||||
|
|
||||||
// Get field definitions and validation schema from formProps if available
|
|
||||||
const fieldDefinitions = formProps.fieldDefinitions || {};
|
const fieldDefinitions = formProps.fieldDefinitions || {};
|
||||||
const validationSchema = formProps.validationSchema as z.ZodObject<any> | undefined;
|
const validationSchema = formProps.validationSchema as z.ZodObject<any> | undefined;
|
||||||
|
|
||||||
// Group fields by their group property
|
|
||||||
const [groupedFields, setGroupedFields] = useState<Record<string, FieldDefinition[]>>({});
|
const [groupedFields, setGroupedFields] = useState<Record<string, FieldDefinition[]>>({});
|
||||||
// State to track validation errors if any
|
|
||||||
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
|
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
// Process field definitions to group them
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Object.keys(fieldDefinitions).length > 0) {
|
if (Object.keys(fieldDefinitions).length > 0) {
|
||||||
const groups: Record<string, FieldDefinition[]> = {};
|
const groups: Record<string, FieldDefinition[]> = {};
|
||||||
|
|
||||||
// Group fields by their group property
|
|
||||||
Object.entries(fieldDefinitions).forEach(([fieldName, definition]) => {
|
Object.entries(fieldDefinitions).forEach(([fieldName, definition]) => {
|
||||||
const def = definition as FieldDefinition;
|
const def = definition as FieldDefinition;
|
||||||
if (!groups[def.group]) {
|
if (!groups[def.group]) { groups[def.group] = []; }
|
||||||
groups[def.group] = [];
|
|
||||||
}
|
|
||||||
groups[def.group].push({ ...def, name: fieldName });
|
groups[def.group].push({ ...def, name: fieldName });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -144,31 +126,24 @@ export function ViewComponent<T>({
|
|||||||
}
|
}
|
||||||
}, [fieldDefinitions]);
|
}, [fieldDefinitions]);
|
||||||
|
|
||||||
// For view mode, we don't need strict validation
|
|
||||||
// Just log validation issues but don't show errors to the user
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (validationSchema && initialData) {
|
if (validationSchema && initialData) {
|
||||||
try {
|
try {
|
||||||
// Try to parse the data through the Zod schema
|
|
||||||
validationSchema.parse(initialData);
|
validationSchema.parse(initialData);
|
||||||
// Clear any previous validation errors if successful
|
|
||||||
setValidationErrors({});
|
setValidationErrors({});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof z.ZodError) {
|
if (error instanceof z.ZodError) {
|
||||||
// Just log the errors but don't set them in the state
|
|
||||||
// This prevents showing validation errors in view mode
|
|
||||||
console.warn('View data validation issues (not shown to user):', error.errors);
|
console.warn('View data validation issues (not shown to user):', error.errors);
|
||||||
|
|
||||||
// Clear any previous validation errors
|
|
||||||
setValidationErrors({});
|
setValidationErrors({});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [initialData, validationSchema]);
|
}, [initialData, validationSchema]);
|
||||||
|
|
||||||
const handleEdit = () => {
|
const handleEdit = () => { setMode("update") };
|
||||||
setMode("update");
|
|
||||||
};
|
console.log("Grouped Fields", groupedFields);
|
||||||
|
console.log("Validation Errors", validationErrors);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full mb-6">
|
<Card className="w-full mb-6">
|
||||||
@@ -177,10 +152,17 @@ export function ViewComponent<T>({
|
|||||||
<CardDescription>{t.viewDescription || "View item details"}</CardDescription>
|
<CardDescription>{t.viewDescription || "View item details"}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
<div className="flex justify-end space-x-2 pt-4 my-4">
|
||||||
|
<Button variant="outline" onClick={onCancel}>
|
||||||
|
{t.back || "Back"}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleEdit}>
|
||||||
|
{t.edit || "Edit"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* If we have grouped fields from schema, use them */}
|
{
|
||||||
{Object.keys(groupedFields).length > 0 ? (
|
Object.keys(groupedFields).length > 0 ? (
|
||||||
// Render fields grouped by their group property
|
|
||||||
Object.entries(groupedFields).map(([groupName, fields]) => (
|
Object.entries(groupedFields).map(([groupName, fields]) => (
|
||||||
<ViewFieldGroup
|
<ViewFieldGroup
|
||||||
key={groupName}
|
key={groupName}
|
||||||
@@ -193,16 +175,12 @@ export function ViewComponent<T>({
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
// Fallback to the default view if no field definitions are available
|
|
||||||
initialData && (
|
initialData && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{validationSchema ? (
|
{validationSchema ? (
|
||||||
// If we have a validation schema, use its shape to determine fields
|
|
||||||
Object.entries(validationSchema.shape || {}).map(([fieldName, _]) => {
|
Object.entries(validationSchema.shape || {}).map(([fieldName, _]) => {
|
||||||
const value = (initialData as any)[fieldName];
|
const value = (initialData as any)[fieldName];
|
||||||
// Skip undefined or null values
|
|
||||||
if (value === undefined || value === null) return null;
|
if (value === undefined || value === null) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewField
|
<ViewField
|
||||||
key={fieldName}
|
key={fieldName}
|
||||||
@@ -215,11 +193,8 @@ export function ViewComponent<T>({
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
// If no schema, just iterate over the data keys
|
|
||||||
Object.entries(initialData as Record<string, any>).map(([fieldName, value]) => {
|
Object.entries(initialData as Record<string, any>).map(([fieldName, value]) => {
|
||||||
// Skip undefined or null values
|
|
||||||
if (value === undefined || value === null) return null;
|
if (value === undefined || value === null) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewField
|
<ViewField
|
||||||
key={fieldName}
|
key={fieldName}
|
||||||
@@ -236,14 +211,7 @@ export function ViewComponent<T>({
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex justify-end space-x-2 pt-4">
|
|
||||||
<Button variant="outline" onClick={onCancel}>
|
|
||||||
{t.back || "Back"}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleEdit}>
|
|
||||||
{t.edit || "Edit"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -11,12 +11,10 @@ export function useApiData<T>(
|
|||||||
endpoint: string,
|
endpoint: string,
|
||||||
initialParams: Partial<RequestParams> = {}
|
initialParams: Partial<RequestParams> = {}
|
||||||
) {
|
) {
|
||||||
// Define the fetch function that will be passed to useDataFetching
|
|
||||||
const fetchFromApi = async (
|
const fetchFromApi = async (
|
||||||
params: RequestParams
|
params: RequestParams
|
||||||
): Promise<ApiResponse<T>> => {
|
): Promise<ApiResponse<T>> => {
|
||||||
try {
|
try {
|
||||||
// Prepare the request body with pagination parameters
|
|
||||||
const requestBody = {
|
const requestBody = {
|
||||||
page: params.page,
|
page: params.page,
|
||||||
size: params.size,
|
size: params.size,
|
||||||
@@ -25,10 +23,7 @@ export function useApiData<T>(
|
|||||||
query: params.query,
|
query: params.query,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Construct the list endpoint URL
|
|
||||||
const listEndpoint = `${endpoint}/list`;
|
const listEndpoint = `${endpoint}/list`;
|
||||||
|
|
||||||
// Make the API request using POST
|
|
||||||
const response = await fetch(listEndpoint, {
|
const response = await fetch(listEndpoint, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { searchPlaceholder, menuLanguage } from "@/app/commons/pageDefaults";
|
import { searchPlaceholder, menuLanguage } from "@/app/commons/pageDefaults";
|
||||||
import { logoutActiveSession } from "@/apicalls/login/login";
|
import { logoutActiveSession } from "@/apicalls/login/endpoints";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { LanguageSelectionComponent } from "../common/HeaderSelections/LanguageSelectionComponent";
|
import { LanguageSelectionComponent } from "../common/HeaderSelections/LanguageSelectionComponent";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Briefcase } from "lucide-react";
|
import { Briefcase } from "lucide-react";
|
||||||
import { retrieveAccessObjects } from "@/apicalls/cookies/token";
|
import { retrieveAccessObjects } from "@/apicalls/cookies/endpoints";
|
||||||
import { loginSelectEmployee } from "@/apicalls/login/login";
|
import { loginSelectEmployee } from "@/apicalls/login/endpoints";
|
||||||
import { EmployeeProfileLanguage } from "./language";
|
import { EmployeeProfileLanguage } from "./language";
|
||||||
import { EmployeeProfileSectionProps, CompanyInfo } from "./type";
|
import { EmployeeProfileSectionProps, CompanyInfo } from "./type";
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Building, Home, ChevronDown } from "lucide-react";
|
import { Building, Home, ChevronDown } from "lucide-react";
|
||||||
import { retrieveAccessObjects } from "@/apicalls/cookies/token";
|
import { retrieveAccessObjects } from "@/apicalls/cookies/endpoints";
|
||||||
import { loginSelectOccupant } from "@/apicalls/login/login";
|
import { loginSelectOccupant } from "@/apicalls/login/endpoints";
|
||||||
import { OccupantProfileLanguage } from "./language";
|
import { OccupantProfileLanguage } from "./language";
|
||||||
import { OccupantSelectionList, BuildingInfo, OccupantInfo, OccupantDetails, OccupantProfileSectionProps } from "./type";
|
import { OccupantSelectionList, BuildingInfo, OccupantInfo, OccupantDetails, OccupantProfileSectionProps } from "./type";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React, { useEffect, useState, Suspense, JSX } from "react";
|
import React, { useEffect, useState, Suspense, JSX } from "react";
|
||||||
import { retrieveUserSelection } from "@/apicalls/cookies/token";
|
import { retrieveUserSelection } from "@/apicalls/cookies/endpoints";
|
||||||
import { ClientMenuProps, UserSelection } from "@/validations/menu/menu";
|
import { ClientMenuProps, UserSelection } from "@/validations/menu/menu";
|
||||||
import { dashboardLanguage } from "./language";
|
import { dashboardLanguage } from "./language";
|
||||||
import EmployeeProfileSection from "./EmployeeProfileSection";
|
import EmployeeProfileSection from "./EmployeeProfileSection";
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ import {
|
|||||||
TranslationSet,
|
TranslationSet,
|
||||||
} from "@/validations/translations/translation";
|
} from "@/validations/translations/translation";
|
||||||
import {
|
import {
|
||||||
ApplicationBaseTranslationEn,
|
ServiceBaseTranslationEn,
|
||||||
ApplicationBaseTranslationTr,
|
ServiceBaseTranslationTr,
|
||||||
} from "./schemaList/schema";
|
} from "./schemaList/services";
|
||||||
|
|
||||||
// Define translations as a flat object structure to match the common components expectations
|
|
||||||
export const translations = {
|
export const translations = {
|
||||||
en: {
|
en: {
|
||||||
...ApplicationBaseTranslationEn,
|
...ServiceBaseTranslationEn,
|
||||||
// Page title
|
// Page title
|
||||||
mainTitle: "Services",
|
mainTitle: "Services",
|
||||||
|
|
||||||
@@ -75,7 +74,7 @@ export const translations = {
|
|||||||
},
|
},
|
||||||
tr: {
|
tr: {
|
||||||
// Page title
|
// Page title
|
||||||
...ApplicationBaseTranslationTr,
|
...ServiceBaseTranslationTr,
|
||||||
mainTitle: "Servisler",
|
mainTitle: "Servisler",
|
||||||
|
|
||||||
// Common actions
|
// Common actions
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { Card, CardContent } from '@/components/ui/card';
|
|||||||
import { TextQueryModifier } from '@/components/common/QueryModifiers/TextQueryModifier';
|
import { TextQueryModifier } from '@/components/common/QueryModifiers/TextQueryModifier';
|
||||||
import { PaginationToolsComponent } from '@/components/common/PaginationModifiers/PaginationToolsComponent';
|
import { PaginationToolsComponent } from '@/components/common/PaginationModifiers/PaginationToolsComponent';
|
||||||
import { CardDisplay } from '@/components/common/CardDisplay/CardDisplay';
|
import { CardDisplay } from '@/components/common/CardDisplay/CardDisplay';
|
||||||
import { ListComponentProps } from './type';
|
import { ListComponentApplicationProps, ListComponentServiceProps } from './type';
|
||||||
|
|
||||||
const ListComponent: React.FC<ListComponentProps> = ({
|
const ListComponentServices: React.FC<ListComponentServiceProps> = ({
|
||||||
lang,
|
lang,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
@@ -16,12 +16,13 @@ const ListComponent: React.FC<ListComponentProps> = ({
|
|||||||
pagination,
|
pagination,
|
||||||
showFields,
|
showFields,
|
||||||
gridCols,
|
gridCols,
|
||||||
|
titleField,
|
||||||
handleQueryChange,
|
handleQueryChange,
|
||||||
updatePagination,
|
updatePagination,
|
||||||
handleCardClick,
|
handleCardClick,
|
||||||
handleViewClick,
|
handleViewClick,
|
||||||
}) => {
|
}) => {
|
||||||
|
const fieldKey = "service_name";
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Search Filters */}
|
{/* Search Filters */}
|
||||||
@@ -37,8 +38,82 @@ const ListComponent: React.FC<ListComponentProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
{/* Search input */}
|
{/* Search input */}
|
||||||
<TextQueryModifier
|
<TextQueryModifier
|
||||||
fieldKey="name"
|
fieldKey={fieldKey}
|
||||||
value={pagination.query["name__ilike"] ? pagination.query["name__ilike"].replace(/%/g, "") : ""}
|
value={pagination.query[`${fieldKey}__ilike`] ? pagination.query[`${fieldKey}__ilike`].replace(/%/g, "") : ""}
|
||||||
|
label={translations[lang].search}
|
||||||
|
onQueryChange={handleQueryChange}
|
||||||
|
translations={translations}
|
||||||
|
lang={lang}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Pagination Tools Component */}
|
||||||
|
<Card className="my-4">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<PaginationToolsComponent
|
||||||
|
pagination={pagination}
|
||||||
|
updatePagination={updatePagination}
|
||||||
|
loading={loading}
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Card Display Component */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<CardDisplay
|
||||||
|
showFields={showFields}
|
||||||
|
data={data}
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
error={error}
|
||||||
|
loading={loading}
|
||||||
|
titleField={titleField}
|
||||||
|
onCardClick={handleCardClick}
|
||||||
|
gridCols={gridCols}
|
||||||
|
showViewIcon={true}
|
||||||
|
onViewClick={handleViewClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ListComponentEvents: React.FC<ListComponentApplicationProps> = ({
|
||||||
|
lang,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
data,
|
||||||
|
pagination,
|
||||||
|
showFields,
|
||||||
|
gridCols,
|
||||||
|
handleQueryChange,
|
||||||
|
updatePagination,
|
||||||
|
handleCardClick,
|
||||||
|
handleViewClick,
|
||||||
|
}) => {
|
||||||
|
const fieldKey = "description";
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Search Filters */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="flex flex-col md:flex-row">
|
||||||
|
{/* Filters on the right */}
|
||||||
|
<div className={`w-full flex flex-col space-y-4`}>
|
||||||
|
<div className="font-medium text-sm mb-2 flex justify-between items-center">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Filter className="mr-2 h-4 w-4" />{translations[lang].filterSelection}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Search input */}
|
||||||
|
<TextQueryModifier
|
||||||
|
fieldKey={fieldKey}
|
||||||
|
value={pagination.query[`${fieldKey}__ilike`] ? pagination.query[`${fieldKey}__ilike`].replace(/%/g, "") : ""}
|
||||||
label={translations[lang].search}
|
label={translations[lang].search}
|
||||||
onQueryChange={handleQueryChange}
|
onQueryChange={handleQueryChange}
|
||||||
translations={translations}
|
translations={translations}
|
||||||
@@ -75,7 +150,6 @@ const ListComponent: React.FC<ListComponentProps> = ({
|
|||||||
onCardClick={handleCardClick}
|
onCardClick={handleCardClick}
|
||||||
gridCols={gridCols}
|
gridCols={gridCols}
|
||||||
showViewIcon={true}
|
showViewIcon={true}
|
||||||
showUpdateIcon={true}
|
|
||||||
onViewClick={handleViewClick}
|
onViewClick={handleViewClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,4 +157,7 @@ const ListComponent: React.FC<ListComponentProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ListComponent;
|
export {
|
||||||
|
ListComponentEvents,
|
||||||
|
ListComponentServices,
|
||||||
|
};
|
||||||
@@ -2,8 +2,10 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import * as schemaServices from "./schemaList/services";
|
||||||
import * as schema from "./schemaList/schema";
|
import * as schema from "./schemaList/schema";
|
||||||
import ListComponent from "./listComponent";
|
|
||||||
|
import { ListComponentEvents, ListComponentServices } from "./listComponent";
|
||||||
import { translations } from "./language";
|
import { translations } from "./language";
|
||||||
|
|
||||||
import { PageProps } from "@/validations/translations/translation";
|
import { PageProps } from "@/validations/translations/translation";
|
||||||
@@ -24,7 +26,7 @@ const AppendersServicePage: React.FC<PageProps> = ({ lang }: { lang: Language })
|
|||||||
error: errorServices,
|
error: errorServices,
|
||||||
updatePagination: updatePaginationServices,
|
updatePagination: updatePaginationServices,
|
||||||
refetch: refetchServices
|
refetch: refetchServices
|
||||||
} = useApiData<schema.ApplicationData>('/api/services');
|
} = useApiData<schemaServices.ServiceData>('/api/services');
|
||||||
const {
|
const {
|
||||||
data: dataEvents,
|
data: dataEvents,
|
||||||
pagination: paginationEvents,
|
pagination: paginationEvents,
|
||||||
@@ -43,34 +45,36 @@ const AppendersServicePage: React.FC<PageProps> = ({ lang }: { lang: Language })
|
|||||||
} = useApiData<schema.ApplicationData>('/api/appenders');
|
} = useApiData<schema.ApplicationData>('/api/appenders');
|
||||||
|
|
||||||
const [mode, setMode] = useState<FormModeView | FormMode>("list");
|
const [mode, setMode] = useState<FormModeView | FormMode>("list");
|
||||||
const [selectedItemServices, setSelectedItemServices] = useState<schema.ApplicationData | null>(null);
|
|
||||||
const [selectedItemEvents, setSelectedItemEvents] = useState<schema.ApplicationData | null>(null);
|
|
||||||
const [selectedItemAppenders, setSelectedItemAppenders] = useState<schema.ApplicationData | null>(null);
|
|
||||||
const [gridCols, setGridCols] = useState<GridSize>(3);
|
const [gridCols, setGridCols] = useState<GridSize>(3);
|
||||||
|
|
||||||
const [fieldDefinitionsServices, setFieldDefinitionsServices] = useState<schema.FieldDefinitionsType | null>(null);
|
const [selectedItemServices, setSelectedItemServices] = useState<schemaServices.ServiceData | null>(null);
|
||||||
const [validationSchemaServices, setValidationSchemaServices] = useState<z.ZodSchema | null>(null);
|
const [selectedItemEvents, setSelectedItemEvents] = useState<schema.ApplicationData | null>(null);
|
||||||
|
const [selectedItemAppenders, setSelectedItemAppenders] = useState<schema.ApplicationData | null>(null);
|
||||||
|
|
||||||
|
const [fieldDefinitionsServices, setFieldDefinitionsServices] = useState<schemaServices.FieldDefinitionsType | null>(null);
|
||||||
const [fieldDefinitionsEvents, setFieldDefinitionsEvents] = useState<schema.FieldDefinitionsType | null>(null);
|
const [fieldDefinitionsEvents, setFieldDefinitionsEvents] = useState<schema.FieldDefinitionsType | null>(null);
|
||||||
const [validationSchemaEvents, setValidationSchemaEvents] = useState<z.ZodSchema | null>(null);
|
|
||||||
const [fieldDefinitionsAppenders, setFieldDefinitionsAppenders] = useState<schema.FieldDefinitionsType | null>(null);
|
const [fieldDefinitionsAppenders, setFieldDefinitionsAppenders] = useState<schema.FieldDefinitionsType | null>(null);
|
||||||
|
|
||||||
|
const [validationSchemaServices, setValidationSchemaServices] = useState<z.ZodSchema | null>(null);
|
||||||
|
const [validationSchemaEvents, setValidationSchemaEvents] = useState<z.ZodSchema | null>(null);
|
||||||
const [validationSchemaAppenders, setValidationSchemaAppenders] = useState<z.ZodSchema | null>(null);
|
const [validationSchemaAppenders, setValidationSchemaAppenders] = useState<z.ZodSchema | null>(null);
|
||||||
|
|
||||||
const showFieldsServices = ["service_name", "service_code", "related_responsibility"];
|
const showFieldsServices = ["service_name", "service_code", "related_responsibility"];
|
||||||
const showFieldsEvents = ["description", "marketing_layer", "cost"];
|
const showFieldsEvents = ["description", "marketing_layer", "cost"];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFieldDefinitionsServices(schema.viewFieldDefinitions); setValidationSchemaServices(schema.UpdateApplicationSchema);
|
setFieldDefinitionsServices(schemaServices.viewFieldDefinitions); setValidationSchemaServices(schemaServices.ViewServiceSchema);
|
||||||
setFieldDefinitionsEvents(schema.viewFieldDefinitions); setValidationSchemaEvents(schema.UpdateApplicationSchema);
|
setFieldDefinitionsEvents(schema.viewFieldDefinitions); setValidationSchemaEvents(schema.ViewApplicationSchema);
|
||||||
setFieldDefinitionsAppenders(schema.viewFieldDefinitions); setValidationSchemaAppenders(schema.UpdateApplicationSchema);
|
setFieldDefinitionsAppenders(schema.viewFieldDefinitions); setValidationSchemaAppenders(schema.ViewApplicationSchema);
|
||||||
}, [lang]);
|
}, [lang]);
|
||||||
|
|
||||||
const handleQueryChange = (key: string, value: string | null) => {
|
const handleQueryChange = (key: string, value: string | null) => {
|
||||||
const newQuery = { ...paginationServices.query };
|
const newQuery = { ...paginationServices.query };
|
||||||
if (value === null) { delete newQuery[key]; } else if (value.trim() === "") { delete newQuery[key]; } else { newQuery[key] = value; }
|
if (value === null) { delete newQuery[key] } else if (value.trim() === "") { delete newQuery[key] } else { newQuery[key] = value }
|
||||||
updatePaginationServices({ page: 1, query: newQuery });
|
updatePaginationServices({ page: 1, query: newQuery })
|
||||||
};
|
};
|
||||||
const handleServicesCardClick = (item: schema.ApplicationData) => { console.log("Services Card clicked:", item) };
|
const handleServicesCardClick = (item: schemaServices.ServiceData) => { setSelectedItemServices(item); setMode("list"); };
|
||||||
const handleServicesViewClick = (item: schema.ApplicationData) => { setSelectedItemServices(item); setMode("view"); };
|
const handleServicesViewClick = (item: schemaServices.ServiceData) => { setSelectedItemServices(item); setMode("view"); };
|
||||||
|
|
||||||
const handleEventsCardClick = (item: schema.ApplicationData) => { console.log("Events Card clicked:", item) };
|
const handleEventsCardClick = (item: schema.ApplicationData) => { console.log("Events Card clicked:", item) };
|
||||||
const handleEventsViewClick = (item: schema.ApplicationData) => { setSelectedItemEvents(item); setMode("view"); };
|
const handleEventsViewClick = (item: schema.ApplicationData) => { setSelectedItemEvents(item); setMode("view"); };
|
||||||
@@ -85,7 +89,7 @@ const AppendersServicePage: React.FC<PageProps> = ({ lang }: { lang: Language })
|
|||||||
setSelectedItemAppenders(null);
|
setSelectedItemAppenders(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const seriveListProps = {
|
const serviceListProps = {
|
||||||
lang,
|
lang,
|
||||||
loading: loadingServices,
|
loading: loadingServices,
|
||||||
error: errorServices,
|
error: errorServices,
|
||||||
@@ -93,6 +97,7 @@ const AppendersServicePage: React.FC<PageProps> = ({ lang }: { lang: Language })
|
|||||||
pagination: paginationServices,
|
pagination: paginationServices,
|
||||||
showFields: showFieldsServices,
|
showFields: showFieldsServices,
|
||||||
gridCols: gridCols,
|
gridCols: gridCols,
|
||||||
|
titleField: "service_name",
|
||||||
handleQueryChange: handleQueryChange,
|
handleQueryChange: handleQueryChange,
|
||||||
updatePagination: updatePaginationServices,
|
updatePagination: updatePaginationServices,
|
||||||
handleCardClick: handleServicesCardClick,
|
handleCardClick: handleServicesCardClick,
|
||||||
@@ -106,7 +111,8 @@ const AppendersServicePage: React.FC<PageProps> = ({ lang }: { lang: Language })
|
|||||||
data: dataEvents,
|
data: dataEvents,
|
||||||
pagination: paginationEvents,
|
pagination: paginationEvents,
|
||||||
showFields: showFieldsEvents,
|
showFields: showFieldsEvents,
|
||||||
gridCols: gridCols,
|
gridCols: 1,
|
||||||
|
titleField: "description",
|
||||||
handleQueryChange: handleQueryChange,
|
handleQueryChange: handleQueryChange,
|
||||||
updatePagination: updatePaginationEvents,
|
updatePagination: updatePaginationEvents,
|
||||||
handleCardClick: handleEventsCardClick,
|
handleCardClick: handleEventsCardClick,
|
||||||
@@ -120,7 +126,8 @@ const AppendersServicePage: React.FC<PageProps> = ({ lang }: { lang: Language })
|
|||||||
data: dataAppenders,
|
data: dataAppenders,
|
||||||
pagination: paginationAppenders,
|
pagination: paginationAppenders,
|
||||||
showFields: showFieldsEvents,
|
showFields: showFieldsEvents,
|
||||||
gridCols: gridCols,
|
gridCols: 1,
|
||||||
|
titleField: "description",
|
||||||
handleQueryChange: handleQueryChange,
|
handleQueryChange: handleQueryChange,
|
||||||
updatePagination: updatePaginationAppenders,
|
updatePagination: updatePaginationAppenders,
|
||||||
handleCardClick: handleAppendersCardClick,
|
handleCardClick: handleAppendersCardClick,
|
||||||
@@ -184,26 +191,25 @@ const AppendersServicePage: React.FC<PageProps> = ({ lang }: { lang: Language })
|
|||||||
<h1 className="text-2xl font-bold">{translations[lang].mainTitle}</h1>
|
<h1 className="text-2xl font-bold">{translations[lang].mainTitle}</h1>
|
||||||
<div className="flex space-x-4"><GridSelectionComponent gridCols={gridCols} setGridCols={setGridCols} /></div>
|
<div className="flex space-x-4"><GridSelectionComponent gridCols={gridCols} setGridCols={setGridCols} /></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{mode === "list" ? (
|
{mode === "list" ? (
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
{!selectedItemServices ? <div className="w-full h-1/2"><ListComponent {...seriveListProps} /></div> :
|
{!selectedItemServices ? <div className="w-full h-1/2"><ListComponentServices {...serviceListProps} /></div> :
|
||||||
<div className="w-full h-1/2">
|
<div className="w-full h-1/2">
|
||||||
<Button onClick={cancelAllSelections}>{translations[lang].cancel}</Button>
|
<Button onClick={cancelAllSelections}>{translations[lang].cancel}</Button>
|
||||||
<Card className="my-5">
|
<Card className="my-5">
|
||||||
<CardHeader>{translations[lang].serviceSelectedTitle}</CardHeader>
|
<CardHeader>{translations[lang].serviceSelectedTitle}</CardHeader>
|
||||||
<CardContent>{selectedItemServices?.name}{" "}{translations[lang].serviceSelectedContent}</CardContent>
|
<CardContent>{selectedItemServices?.service_name}{" "}{translations[lang].serviceSelectedContent}</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<div className="flex flex-row space-x-4">
|
<div className="flex flex-row space-x-4">
|
||||||
<div className="flex-1"><ListComponent {...eventsListProps} /></div>
|
<div className="flex-1"><ListComponentEvents {...eventsListProps} /></div>
|
||||||
<div className="flex-1"><ListComponent {...appendersListProps} /></div>
|
<div className="flex-1"><ListComponentEvents {...appendersListProps} /></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
{selectedItemServices && <FormDisplay<schema.ApplicationData> {...serviceFormProps} />}
|
{selectedItemServices && <FormDisplay<schemaServices.ServiceData> {...serviceFormProps} />}
|
||||||
{selectedItemEvents && <FormDisplay<schema.ApplicationData> {...eventsFormProps} />}
|
{selectedItemEvents && <FormDisplay<schema.ApplicationData> {...eventsFormProps} />}
|
||||||
{selectedItemAppenders && <FormDisplay<schema.ApplicationData> {...appendersFormProps} />}
|
{selectedItemAppenders && <FormDisplay<schema.ApplicationData> {...appendersFormProps} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -137,9 +137,7 @@ export type CreateApplicationFormData = z.infer<typeof CreateApplicationSchema>;
|
|||||||
export type UpdateApplicationFormData = z.infer<typeof UpdateApplicationSchema>;
|
export type UpdateApplicationFormData = z.infer<typeof UpdateApplicationSchema>;
|
||||||
export type ViewApplicationFormData = z.infer<typeof ViewApplicationSchema>;
|
export type ViewApplicationFormData = z.infer<typeof ViewApplicationSchema>;
|
||||||
|
|
||||||
// Base field definitions grouped by section
|
|
||||||
const baseFieldDefinitions = {
|
const baseFieldDefinitions = {
|
||||||
// Identification fields
|
|
||||||
identificationInfo: {
|
identificationInfo: {
|
||||||
title: "Identification Information",
|
title: "Identification Information",
|
||||||
order: 1,
|
order: 1,
|
||||||
@@ -173,8 +171,6 @@ const baseFieldDefinitions = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Application details
|
|
||||||
applicationDetails: {
|
applicationDetails: {
|
||||||
title: "Application Details",
|
title: "Application Details",
|
||||||
order: 2,
|
order: 2,
|
||||||
@@ -219,8 +215,6 @@ const baseFieldDefinitions = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Status fields
|
|
||||||
statusInfo: {
|
statusInfo: {
|
||||||
title: "Status Information",
|
title: "Status Information",
|
||||||
order: 3,
|
order: 3,
|
||||||
@@ -247,8 +241,6 @@ const baseFieldDefinitions = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// System fields
|
|
||||||
systemInfo: {
|
systemInfo: {
|
||||||
title: "System Information",
|
title: "System Information",
|
||||||
order: 4,
|
order: 4,
|
||||||
@@ -275,10 +267,8 @@ const baseFieldDefinitions = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a flat version of the field definitions for compatibility
|
|
||||||
const flatFieldDefinitions = flattenFieldDefinitions(baseFieldDefinitions);
|
const flatFieldDefinitions = flattenFieldDefinitions(baseFieldDefinitions);
|
||||||
|
|
||||||
// Create mode-specific field definitions using the flattened structure
|
|
||||||
export const createFieldDefinitions = {
|
export const createFieldDefinitions = {
|
||||||
name: {
|
name: {
|
||||||
...flatFieldDefinitions.name,
|
...flatFieldDefinitions.name,
|
||||||
@@ -324,7 +314,6 @@ export const createFieldDefinitions = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update mode-specific field definitions
|
|
||||||
export const updateFieldDefinitions = {
|
export const updateFieldDefinitions = {
|
||||||
uu_id: {
|
uu_id: {
|
||||||
...flatFieldDefinitions.uu_id,
|
...flatFieldDefinitions.uu_id,
|
||||||
@@ -376,7 +365,6 @@ export const updateFieldDefinitions = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// View mode-specific field definitions
|
|
||||||
export const viewFieldDefinitions = {
|
export const viewFieldDefinitions = {
|
||||||
uu_id: {
|
uu_id: {
|
||||||
...flatFieldDefinitions.uu_id,
|
...flatFieldDefinitions.uu_id,
|
||||||
@@ -446,7 +434,6 @@ export const viewFieldDefinitions = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Combined field definitions for all modes
|
|
||||||
export const fieldDefinitions = {
|
export const fieldDefinitions = {
|
||||||
...baseFieldDefinitions,
|
...baseFieldDefinitions,
|
||||||
getDefinitionsByMode: (mode: "create" | "update" | "view") => {
|
getDefinitionsByMode: (mode: "create" | "update" | "view") => {
|
||||||
|
|||||||
@@ -0,0 +1,298 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { flattenFieldDefinitions } from "@/eventRouters/schemas/zodSchemas";
|
||||||
|
|
||||||
|
export interface ServiceData {
|
||||||
|
uu_id: string;
|
||||||
|
module_uu_id: string;
|
||||||
|
service_name: string;
|
||||||
|
service_description?: string;
|
||||||
|
service_code: string;
|
||||||
|
related_responsibility?: string;
|
||||||
|
is_confirmed: boolean;
|
||||||
|
active: boolean;
|
||||||
|
deleted?: boolean;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessages = {
|
||||||
|
en: {
|
||||||
|
moduleUuIdRequired: "Module UUID is required",
|
||||||
|
serviceNameRequired: "Service name is required",
|
||||||
|
serviceCodeRequired: "Service code is required",
|
||||||
|
},
|
||||||
|
tr: {
|
||||||
|
moduleUuIdRequired: "Modül UUID'si gereklidir",
|
||||||
|
serviceNameRequired: "Servis adı gereklidir",
|
||||||
|
serviceCodeRequired: "Servis kodu gereklidir",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const getServiceBaseSchema = (lang: "en" | "tr" = "en") =>
|
||||||
|
z.object({
|
||||||
|
uu_id: z.string().optional(),
|
||||||
|
module_uu_id: z.string().min(1, errorMessages[lang].moduleUuIdRequired),
|
||||||
|
service_name: z.string().min(1, errorMessages[lang].serviceNameRequired),
|
||||||
|
service_description: z.string().optional(),
|
||||||
|
service_code: z.string().min(1, errorMessages[lang].serviceCodeRequired),
|
||||||
|
related_responsibility: z.string().optional(),
|
||||||
|
is_confirmed: z.boolean().default(false),
|
||||||
|
active: z.boolean().default(true),
|
||||||
|
deleted: z.boolean().default(false),
|
||||||
|
created_at: z.string().optional(),
|
||||||
|
updated_at: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ServiceBaseSchema = getServiceBaseSchema("en");
|
||||||
|
|
||||||
|
export const ServiceBaseTranslationTr = {
|
||||||
|
uu_id: "UUID",
|
||||||
|
module_uu_id: "Modül UUID'si",
|
||||||
|
service_name: "Servis Adı",
|
||||||
|
service_description: "Servis Açıklaması",
|
||||||
|
service_code: "Servis Kodu",
|
||||||
|
related_responsibility: "İlgili Sorumluluk",
|
||||||
|
is_confirmed: "Onaylandı",
|
||||||
|
active: "Active",
|
||||||
|
deleted: "Deleted",
|
||||||
|
created_at: "Created At",
|
||||||
|
updated_at: "Updated At",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ServiceBaseTranslationEn = {
|
||||||
|
uu_id: "UUID",
|
||||||
|
module_uu_id: "Module UUID",
|
||||||
|
service_name: "Service Name",
|
||||||
|
service_description: "Service Description",
|
||||||
|
service_code: "Service Code",
|
||||||
|
related_responsibility: "Related Responsibility",
|
||||||
|
is_confirmed: "Confirmed",
|
||||||
|
active: "Active",
|
||||||
|
deleted: "Deleted",
|
||||||
|
created_at: "Created At",
|
||||||
|
updated_at: "Updated At",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ViewServiceSchema = ServiceBaseSchema;
|
||||||
|
const ServiceSchema = ServiceBaseSchema;
|
||||||
|
|
||||||
|
export {
|
||||||
|
ServiceBaseSchema,
|
||||||
|
ServiceSchema,
|
||||||
|
ViewServiceSchema,
|
||||||
|
getServiceBaseSchema,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServiceFormData = z.infer<typeof ServiceSchema>;
|
||||||
|
export type ViewServiceFormData = z.infer<typeof ViewServiceSchema>;
|
||||||
|
|
||||||
|
const baseFieldDefinitions = {
|
||||||
|
identificationInfo: {
|
||||||
|
title: "Service Information",
|
||||||
|
order: 1,
|
||||||
|
fields: {
|
||||||
|
uu_id: {
|
||||||
|
type: "text",
|
||||||
|
label: {
|
||||||
|
tr: ServiceBaseTranslationTr.uu_id,
|
||||||
|
en: ServiceBaseTranslationEn.uu_id,
|
||||||
|
},
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
module_uu_id: {
|
||||||
|
type: "text",
|
||||||
|
label: {
|
||||||
|
tr: ServiceBaseTranslationTr.module_uu_id,
|
||||||
|
en: ServiceBaseTranslationEn.module_uu_id,
|
||||||
|
},
|
||||||
|
readOnly: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
service_name: {
|
||||||
|
type: "text",
|
||||||
|
label: {
|
||||||
|
tr: ServiceBaseTranslationTr.service_name,
|
||||||
|
en: ServiceBaseTranslationEn.service_name,
|
||||||
|
},
|
||||||
|
readOnly: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
service_description: {
|
||||||
|
type: "text",
|
||||||
|
label: {
|
||||||
|
tr: ServiceBaseTranslationTr.service_description,
|
||||||
|
en: ServiceBaseTranslationEn.service_description,
|
||||||
|
},
|
||||||
|
readOnly: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
service_code: {
|
||||||
|
type: "text",
|
||||||
|
label: {
|
||||||
|
tr: ServiceBaseTranslationTr.service_code,
|
||||||
|
en: ServiceBaseTranslationEn.service_code,
|
||||||
|
},
|
||||||
|
readOnly: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
related_responsibility: {
|
||||||
|
type: "text",
|
||||||
|
label: {
|
||||||
|
tr: ServiceBaseTranslationTr.related_responsibility,
|
||||||
|
en: ServiceBaseTranslationEn.related_responsibility,
|
||||||
|
},
|
||||||
|
readOnly: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
statusInfo: {
|
||||||
|
title: "Status Information",
|
||||||
|
order: 3,
|
||||||
|
fields: {
|
||||||
|
active: {
|
||||||
|
type: "checkbox",
|
||||||
|
label: {
|
||||||
|
tr: ServiceBaseTranslationTr.active,
|
||||||
|
en: ServiceBaseTranslationEn.active,
|
||||||
|
},
|
||||||
|
readOnly: false,
|
||||||
|
required: false,
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
deleted: {
|
||||||
|
type: "checkbox",
|
||||||
|
label: {
|
||||||
|
tr: ServiceBaseTranslationTr.deleted,
|
||||||
|
en: ServiceBaseTranslationEn.deleted,
|
||||||
|
},
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
is_confirmed: {
|
||||||
|
type: "checkbox",
|
||||||
|
label: {
|
||||||
|
tr: ServiceBaseTranslationTr.is_confirmed,
|
||||||
|
en: ServiceBaseTranslationEn.is_confirmed,
|
||||||
|
},
|
||||||
|
readOnly: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
systemInfo: {
|
||||||
|
title: "System Information",
|
||||||
|
order: 4,
|
||||||
|
fields: {
|
||||||
|
created_at: {
|
||||||
|
type: "date",
|
||||||
|
label: {
|
||||||
|
tr: ServiceBaseTranslationTr.created_at,
|
||||||
|
en: ServiceBaseTranslationEn.created_at,
|
||||||
|
},
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
type: "date",
|
||||||
|
label: {
|
||||||
|
tr: ServiceBaseTranslationTr.updated_at,
|
||||||
|
en: ServiceBaseTranslationEn.updated_at,
|
||||||
|
},
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const flatFieldDefinitions = flattenFieldDefinitions(baseFieldDefinitions);
|
||||||
|
export const viewFieldDefinitions = {
|
||||||
|
uu_id: {
|
||||||
|
...flatFieldDefinitions.uu_id,
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
module_uu_id: {
|
||||||
|
...flatFieldDefinitions.module_uu_id,
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
service_name: {
|
||||||
|
...flatFieldDefinitions.service_name,
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
service_description: {
|
||||||
|
...flatFieldDefinitions.service_description,
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
service_code: {
|
||||||
|
...flatFieldDefinitions.service_code,
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
related_responsibility: {
|
||||||
|
...flatFieldDefinitions.related_responsibility,
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
...flatFieldDefinitions.active,
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
is_confirmed: {
|
||||||
|
...flatFieldDefinitions.is_confirmed,
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
deleted: {
|
||||||
|
...flatFieldDefinitions.deleted,
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
...flatFieldDefinitions.created_at,
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
...flatFieldDefinitions.updated_at,
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fieldDefinitions = {
|
||||||
|
...baseFieldDefinitions,
|
||||||
|
getDefinitionsByMode: (mode: "view") => {
|
||||||
|
switch (mode) {
|
||||||
|
case "view":
|
||||||
|
return viewFieldDefinitions;
|
||||||
|
default:
|
||||||
|
return baseFieldDefinitions;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fieldsByMode = {
|
||||||
|
view: Object.keys(viewFieldDefinitions),
|
||||||
|
};
|
||||||
|
export type FieldDefinitionsType = typeof viewFieldDefinitions;
|
||||||
@@ -1,17 +1,34 @@
|
|||||||
import { Language } from "@/components/common/schemas";
|
import { Language } from "@/components/common/schemas";
|
||||||
import { GridSize } from "@/components/common/HeaderSelections/GridSelectionComponent";
|
import { GridSize } from "@/components/common/HeaderSelections/GridSelectionComponent";
|
||||||
import * as schema from "./schemaList/schema";
|
import * as schema from "./schemaList/schema";
|
||||||
|
import * as schemaServices from "./schemaList/services";
|
||||||
|
|
||||||
export interface ListComponentProps {
|
export interface ListComponentApplicationProps {
|
||||||
lang: Language;
|
lang: Language;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: any;
|
error: any;
|
||||||
data: schema.ApplicationData[];
|
data: schema.ApplicationData[];
|
||||||
pagination: any;
|
pagination: any;
|
||||||
showFields: string[];
|
showFields: string[];
|
||||||
gridCols: GridSize;
|
gridCols: GridSize | number;
|
||||||
|
titleField: string;
|
||||||
handleQueryChange: (key: string, value: string | null) => void;
|
handleQueryChange: (key: string, value: string | null) => void;
|
||||||
updatePagination: (pagination: any) => void;
|
updatePagination: (pagination: any) => void;
|
||||||
handleCardClick: (item: schema.ApplicationData) => void;
|
handleCardClick: (item: schema.ApplicationData) => void;
|
||||||
handleViewClick: (item: schema.ApplicationData) => void;
|
handleViewClick: (item: schema.ApplicationData) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ListComponentServiceProps {
|
||||||
|
lang: Language;
|
||||||
|
loading: boolean;
|
||||||
|
error: any;
|
||||||
|
data: schemaServices.ServiceData[];
|
||||||
|
pagination: any;
|
||||||
|
showFields: string[];
|
||||||
|
gridCols: GridSize | number;
|
||||||
|
titleField: string;
|
||||||
|
handleQueryChange: (key: string, value: string | null) => void;
|
||||||
|
updatePagination: (pagination: any) => void;
|
||||||
|
handleCardClick: (item: schemaServices.ServiceData) => void;
|
||||||
|
handleViewClick: (item: schemaServices.ServiceData) => void;
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export interface ApplicationData {
|
|||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation error messages by language
|
|
||||||
const errorMessages = {
|
const errorMessages = {
|
||||||
en: {
|
en: {
|
||||||
nameRequired: "Name is required",
|
nameRequired: "Name is required",
|
||||||
@@ -31,7 +30,6 @@ const errorMessages = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to get schema with language-specific validation messages
|
|
||||||
const getApplicationBaseSchema = (lang: "en" | "tr" = "en") =>
|
const getApplicationBaseSchema = (lang: "en" | "tr" = "en") =>
|
||||||
z.object({
|
z.object({
|
||||||
// Identification fields
|
// Identification fields
|
||||||
@@ -58,7 +56,6 @@ const getApplicationBaseSchema = (lang: "en" | "tr" = "en") =>
|
|||||||
updated_at: z.string().optional(),
|
updated_at: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// For backward compatibility
|
|
||||||
const ApplicationBaseSchema = getApplicationBaseSchema("en");
|
const ApplicationBaseSchema = getApplicationBaseSchema("en");
|
||||||
|
|
||||||
export const ApplicationBaseTranslationTr = {
|
export const ApplicationBaseTranslationTr = {
|
||||||
@@ -89,7 +86,6 @@ export const ApplicationBaseTranslationEn = {
|
|||||||
updated_at: "Updated At",
|
updated_at: "Updated At",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create schema for creating new applications with language support
|
|
||||||
const getCreateApplicationSchema = (lang: "en" | "tr" = "en") =>
|
const getCreateApplicationSchema = (lang: "en" | "tr" = "en") =>
|
||||||
getApplicationBaseSchema(lang).omit({
|
getApplicationBaseSchema(lang).omit({
|
||||||
uu_id: true,
|
uu_id: true,
|
||||||
@@ -98,7 +94,6 @@ const getCreateApplicationSchema = (lang: "en" | "tr" = "en") =>
|
|||||||
deleted: true,
|
deleted: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update schema for updating existing applications with language support
|
|
||||||
const getUpdateApplicationSchema = (lang: "en" | "tr" = "en") =>
|
const getUpdateApplicationSchema = (lang: "en" | "tr" = "en") =>
|
||||||
getApplicationBaseSchema(lang)
|
getApplicationBaseSchema(lang)
|
||||||
.omit({
|
.omit({
|
||||||
@@ -110,17 +105,11 @@ const getUpdateApplicationSchema = (lang: "en" | "tr" = "en") =>
|
|||||||
uu_id: true,
|
uu_id: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// For backward compatibility
|
|
||||||
const CreateApplicationSchema = getCreateApplicationSchema("en");
|
const CreateApplicationSchema = getCreateApplicationSchema("en");
|
||||||
const UpdateApplicationSchema = getUpdateApplicationSchema("en");
|
const UpdateApplicationSchema = getUpdateApplicationSchema("en");
|
||||||
|
|
||||||
// Schema for viewing an application (all fields)
|
|
||||||
const ViewApplicationSchema = ApplicationBaseSchema;
|
const ViewApplicationSchema = ApplicationBaseSchema;
|
||||||
|
|
||||||
// Default schema (used for validation)
|
|
||||||
const ApplicationSchema = ApplicationBaseSchema;
|
const ApplicationSchema = ApplicationBaseSchema;
|
||||||
|
|
||||||
// Export all schemas and schema generators
|
|
||||||
export {
|
export {
|
||||||
ApplicationBaseSchema,
|
ApplicationBaseSchema,
|
||||||
ApplicationSchema,
|
ApplicationSchema,
|
||||||
@@ -137,7 +126,6 @@ export type CreateApplicationFormData = z.infer<typeof CreateApplicationSchema>;
|
|||||||
export type UpdateApplicationFormData = z.infer<typeof UpdateApplicationSchema>;
|
export type UpdateApplicationFormData = z.infer<typeof UpdateApplicationSchema>;
|
||||||
export type ViewApplicationFormData = z.infer<typeof ViewApplicationSchema>;
|
export type ViewApplicationFormData = z.infer<typeof ViewApplicationSchema>;
|
||||||
|
|
||||||
// Base field definitions grouped by section
|
|
||||||
const baseFieldDefinitions = {
|
const baseFieldDefinitions = {
|
||||||
// Identification fields
|
// Identification fields
|
||||||
identificationInfo: {
|
identificationInfo: {
|
||||||
@@ -274,11 +262,8 @@ const baseFieldDefinitions = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a flat version of the field definitions for compatibility
|
|
||||||
const flatFieldDefinitions = flattenFieldDefinitions(baseFieldDefinitions);
|
const flatFieldDefinitions = flattenFieldDefinitions(baseFieldDefinitions);
|
||||||
|
|
||||||
// Create mode-specific field definitions using the flattened structure
|
|
||||||
export const createFieldDefinitions = {
|
export const createFieldDefinitions = {
|
||||||
name: {
|
name: {
|
||||||
...flatFieldDefinitions.name,
|
...flatFieldDefinitions.name,
|
||||||
@@ -323,8 +308,6 @@ export const createFieldDefinitions = {
|
|||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update mode-specific field definitions
|
|
||||||
export const updateFieldDefinitions = {
|
export const updateFieldDefinitions = {
|
||||||
uu_id: {
|
uu_id: {
|
||||||
...flatFieldDefinitions.uu_id,
|
...flatFieldDefinitions.uu_id,
|
||||||
@@ -375,8 +358,6 @@ export const updateFieldDefinitions = {
|
|||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// View mode-specific field definitions
|
|
||||||
export const viewFieldDefinitions = {
|
export const viewFieldDefinitions = {
|
||||||
uu_id: {
|
uu_id: {
|
||||||
...flatFieldDefinitions.uu_id,
|
...flatFieldDefinitions.uu_id,
|
||||||
@@ -445,8 +426,6 @@ export const viewFieldDefinitions = {
|
|||||||
defaultValue: "",
|
defaultValue: "",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Combined field definitions for all modes
|
|
||||||
export const fieldDefinitions = {
|
export const fieldDefinitions = {
|
||||||
...baseFieldDefinitions,
|
...baseFieldDefinitions,
|
||||||
getDefinitionsByMode: (mode: "create" | "update" | "view") => {
|
getDefinitionsByMode: (mode: "create" | "update" | "view") => {
|
||||||
@@ -462,8 +441,6 @@ export const fieldDefinitions = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fields to show based on mode - dynamically generated from field definitions
|
|
||||||
export const fieldsByMode = {
|
export const fieldsByMode = {
|
||||||
create: Object.keys(createFieldDefinitions),
|
create: Object.keys(createFieldDefinitions),
|
||||||
update: Object.keys(updateFieldDefinitions),
|
update: Object.keys(updateFieldDefinitions),
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// Define field definition type
|
|
||||||
interface FieldDefinition {
|
interface FieldDefinition {
|
||||||
type: string;
|
type: string;
|
||||||
group: string;
|
group: string;
|
||||||
@@ -9,7 +8,6 @@ interface FieldDefinition {
|
|||||||
defaultValue?: any;
|
defaultValue?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to flatten grouped field definitions into a flat structure
|
|
||||||
const flattenFieldDefinitions = (
|
const flattenFieldDefinitions = (
|
||||||
groupedDefs: any
|
groupedDefs: any
|
||||||
): Record<string, FieldDefinition> => {
|
): Record<string, FieldDefinition> => {
|
||||||
@@ -27,7 +25,6 @@ const flattenFieldDefinitions = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user