diff --git a/WebServices/client-frontend/setup-shadcn.sh b/WebServices/client-frontend/setup-shadcn.sh index 52dec08..bd6dc81 100755 --- a/WebServices/client-frontend/setup-shadcn.sh +++ b/WebServices/client-frontend/setup-shadcn.sh @@ -34,6 +34,7 @@ npx shadcn@latest add calendar -y npx shadcn@latest add date-picker -y npx shadcn@latest add skeleton -y npx shadcn@latest add table -y +npx shadcn@latest add textarea -y # Update any dependencies with legacy peer deps echo "🔄 Updating dependencies..." diff --git a/WebServices/client-frontend/src/app/(DashboardLayout)/individual/page.tsx b/WebServices/client-frontend/src/app/(DashboardLayout)/individual/page.tsx index 0f4ed5b..2c99949 100644 --- a/WebServices/client-frontend/src/app/(DashboardLayout)/individual/page.tsx +++ b/WebServices/client-frontend/src/app/(DashboardLayout)/individual/page.tsx @@ -1,54 +1,28 @@ "use server"; import React from "react"; -import ClientMenu from "@/components/menu/menu"; -import { retrievePageList, retrievePagebyUrl } from "@/apicalls/cookies/token"; -import { retrievePageByUrlAndPageId } from "@/components/navigator/retriever"; -import { searchPlaceholder } from "@/app/commons/pageDefaults"; - -const pageInfo = { - tr: "Birey Sayfası", - en: "Individual Page", -}; +import DashboardLayout from "@/components/layouts/DashboardLayout"; +import { useDashboardPage } from "@/components/common/hooks/useDashboardPage"; export default async function Dashboard({ searchParams, }: { searchParams: Promise<{ [key: string]: string | undefined }>; }) { - const activePage = "/individual"; - const siteUrlsList = (await retrievePageList()) || []; - const pageToDirect = await retrievePagebyUrl(activePage); - const PageComponent = retrievePageByUrlAndPageId(pageToDirect, activePage); - const searchParamsInstance = await searchParams; - const lang = (searchParamsInstance?.lang as "en" | "tr") || "en"; + // Use the enhanced dashboard hook to get all necessary data + const { + activePage, + searchParamsInstance, + lang, + PageComponent, + siteUrlsList + } = await useDashboardPage({ + pageUrl: "/individual", + searchParams + }); return ( - <> -
- {/* Sidebar */} - - - {/* Main Content Area */} -
- {/* Sticky Header */} -
-

{pageInfo[lang]}

-
- -
-
-
-
- -
-
-
- + + + ); } diff --git a/WebServices/client-frontend/src/app/(DashboardLayout)/layout.tsx b/WebServices/client-frontend/src/app/(DashboardLayout)/layout.tsx index 521102d..ec6fb15 100644 --- a/WebServices/client-frontend/src/app/(DashboardLayout)/layout.tsx +++ b/WebServices/client-frontend/src/app/(DashboardLayout)/layout.tsx @@ -18,7 +18,7 @@ export default async function DashLayout({ } return (
-
{children}
+
{children}
); } diff --git a/WebServices/client-frontend/src/components/common/ActionButtonsDisplay/CreateButton.tsx b/WebServices/client-frontend/src/components/common/ActionButtonsDisplay/CreateButton.tsx new file mode 100644 index 0000000..2716947 --- /dev/null +++ b/WebServices/client-frontend/src/components/common/ActionButtonsDisplay/CreateButton.tsx @@ -0,0 +1,25 @@ +"use client"; +import React from "react"; +import { Button } from "@/components/ui/button"; +import { Plus } from "lucide-react"; + +interface CreateButtonProps { + onClick: () => void; + translations: Record; + lang: string; +} + +export const CreateButton: React.FC = ({ + onClick, + translations, + lang, +}) => { + const t = translations[lang] || {}; + + return ( + + ); +}; diff --git a/WebServices/client-frontend/src/components/common/ActionButtonsDisplay/CustomButtonComponent.tsx b/WebServices/client-frontend/src/components/common/ActionButtonsDisplay/CustomButtonComponent.tsx new file mode 100644 index 0000000..2d86b8b --- /dev/null +++ b/WebServices/client-frontend/src/components/common/ActionButtonsDisplay/CustomButtonComponent.tsx @@ -0,0 +1,31 @@ +"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 = ({ + button, + isSelected, + onClick, +}) => { + return ( + + ); +}; diff --git a/WebServices/client-frontend/src/components/common/ActionButtonsDisplay/index.ts b/WebServices/client-frontend/src/components/common/ActionButtonsDisplay/index.ts new file mode 100644 index 0000000..5b1e2de --- /dev/null +++ b/WebServices/client-frontend/src/components/common/ActionButtonsDisplay/index.ts @@ -0,0 +1,3 @@ +export * from './CreateButton'; +export * from './CustomButtonComponent'; +export * from './types'; diff --git a/WebServices/client-frontend/src/components/common/ActionButtonsDisplay/types.ts b/WebServices/client-frontend/src/components/common/ActionButtonsDisplay/types.ts new file mode 100644 index 0000000..270f86a --- /dev/null +++ b/WebServices/client-frontend/src/components/common/ActionButtonsDisplay/types.ts @@ -0,0 +1,17 @@ +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; + lang: string; + customButtons?: CustomButton[]; + defaultSelectedButtonId?: string; +} diff --git a/WebServices/client-frontend/src/components/common/CardDisplay/CardDisplay.tsx b/WebServices/client-frontend/src/components/common/CardDisplay/CardDisplay.tsx new file mode 100644 index 0000000..8b4b3ce --- /dev/null +++ b/WebServices/client-frontend/src/components/common/CardDisplay/CardDisplay.tsx @@ -0,0 +1,73 @@ +"use client"; +import React from "react"; +import { CardItem } from "./CardItem"; +import { CardSkeleton } from "./CardSkeleton"; +import { getFieldValue, getGridClasses } from "./utils"; +import { CardDisplayProps } from "./schema"; + +// Interface moved to schema.ts + +export function CardDisplay({ + showFields, + data, + lang, + translations, + error, + loading, + titleField = "name", + onCardClick, + renderCustomField, + gridCols = 4, + showViewIcon = false, + showUpdateIcon = false, + onViewClick, + onUpdateClick, +}: CardDisplayProps) { + if (error) { + return ( +
+ {error.message || "An error occurred while fetching data."} +
+ ); + } + + return ( +
+ {loading ? ( + // Loading skeletons + Array.from({ length: 10 }).map((_, index) => ( + + )) + ) : data.length === 0 ? ( +
+ {(translations[lang] || {}).noData || "No data found"} +
+ ) : ( + data.map((item, index) => ( + + )) + )} +
+ ); +} diff --git a/WebServices/client-frontend/src/components/common/CardDisplay/CardItem.tsx b/WebServices/client-frontend/src/components/common/CardDisplay/CardItem.tsx new file mode 100644 index 0000000..83bd719 --- /dev/null +++ b/WebServices/client-frontend/src/components/common/CardDisplay/CardItem.tsx @@ -0,0 +1,130 @@ +"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({ + item, + index, + showFields, + titleField, + lang, + translations, + onCardClick, + renderCustomField, + showViewIcon, + showUpdateIcon, + onViewClick, + onUpdateClick, + getFieldValue, +}: CardItemProps) { + return ( +
+ onCardClick(item) : undefined} + > + +

+ {getFieldValue(item, titleField)} +

+ +
+ +
+ {showFields.map((field) => ( + + ))} +
+
+
+
+ ); +} + +// Interface moved to schema.ts + +function CardActions({ + item, + showViewIcon, + showUpdateIcon, + onViewClick, + onUpdateClick, +}: CardActionsProps) { + if (!showViewIcon && !showUpdateIcon) return null; + + return ( +
+ {showViewIcon && ( + + )} + {showUpdateIcon && ( + + )} +
+ ); +} + +// Interface moved to schema.ts + +function CardField({ + item, + field, + lang, + translations, + renderCustomField, + getFieldValue, +}: CardFieldProps) { + return ( +
+ + {translations[field]?.[lang] || field}: + + + {renderCustomField + ? renderCustomField(item, field) + : getFieldValue(item, field)} + +
+ ); +} diff --git a/WebServices/client-frontend/src/components/common/CardDisplay/CardSkeleton.tsx b/WebServices/client-frontend/src/components/common/CardDisplay/CardSkeleton.tsx new file mode 100644 index 0000000..73c0620 --- /dev/null +++ b/WebServices/client-frontend/src/components/common/CardDisplay/CardSkeleton.tsx @@ -0,0 +1,46 @@ +"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 ( +
+ + + +
+ {showViewIcon && ( + + )} + {showUpdateIcon && ( + + )} +
+
+ +
+ {showFields.map((field, fieldIndex) => ( +
+ + +
+ ))} +
+
+
+
+ ); +} diff --git a/WebServices/client-frontend/src/components/common/CardDisplay/index.tsx b/WebServices/client-frontend/src/components/common/CardDisplay/index.tsx new file mode 100644 index 0000000..b3dba2c --- /dev/null +++ b/WebServices/client-frontend/src/components/common/CardDisplay/index.tsx @@ -0,0 +1 @@ +export { CardDisplay } from './CardDisplay'; diff --git a/WebServices/client-frontend/src/components/common/CardDisplay/schema.ts b/WebServices/client-frontend/src/components/common/CardDisplay/schema.ts new file mode 100644 index 0000000..b1637b0 --- /dev/null +++ b/WebServices/client-frontend/src/components/common/CardDisplay/schema.ts @@ -0,0 +1,117 @@ +/** + * CardDisplay component interfaces + */ + +/** + * Main props for the CardDisplay component + */ +export interface CardDisplayProps { + /** 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; + /** 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 { + /** 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; + /** 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 { + /** 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 { + /** 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; + /** 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; +} diff --git a/WebServices/client-frontend/src/components/common/CardDisplay/utils.ts b/WebServices/client-frontend/src/components/common/CardDisplay/utils.ts new file mode 100644 index 0000000..4e6e97f --- /dev/null +++ b/WebServices/client-frontend/src/components/common/CardDisplay/utils.ts @@ -0,0 +1,46 @@ +/** + * 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, 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 = { + 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]}`; +} diff --git a/WebServices/client-frontend/src/components/common/FormDisplay/CreateComponent.tsx b/WebServices/client-frontend/src/components/common/FormDisplay/CreateComponent.tsx new file mode 100644 index 0000000..77d3756 --- /dev/null +++ b/WebServices/client-frontend/src/components/common/FormDisplay/CreateComponent.tsx @@ -0,0 +1,279 @@ +"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 } 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"; + +// Import field definitions type +interface FieldDefinition { + type: string; + group: string; + label: string; + options?: string[]; + readOnly?: boolean; + required?: boolean; + defaultValue?: any; + name?: string; +} + +export function CreateComponent({ + refetch, + setMode, + setSelectedItem, + onCancel, + lang, + translations, + formProps = {}, +}: CreateComponentProps) { + 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>({}); + + // Process field definitions to group them + useEffect(() => { + if (Object.keys(fieldDefinitions).length > 0) { + const groups: Record = {}; + + // 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 = {}; + 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, + } = useForm({ + defaultValues, + resolver: validationSchema ? zodResolver(validationSchema) : undefined, + }); + + const formValues = watch(); + + // Handle form submission + const onSubmit = async (data: Record) => { + try { + console.log("Form data to save:", data); + + // Here you would make an API call to save the data + // For example: await createApplication(data); + + // Mock API call success + 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) => { + // First check if there's a translation for the exact group key + if (t[groupName]) { + return t[groupName]; + } + + // Try to format the group name in a more readable way if no translation exists + // Convert camelCase or snake_case to Title Case with spaces + const formattedName = groupName + // Insert space before capital letters and uppercase the first letter + .replace(/([A-Z])/g, ' $1') + // Replace underscores with spaces + .replace(/_/g, ' ') + // Capitalize first letter + .replace(/^./, (str) => str.toUpperCase()) + // Capitalize each word + .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 ( +
+ + + {errorMessage && ( +

{errorMessage}

+ )} +
+ ); + + case "textarea": + return ( +
+ +