diff --git a/ApiServices/InitialService/init_app_defaults.py b/ApiServices/InitialService/init_app_defaults.py index 8f3094b..65fa709 100644 --- a/ApiServices/InitialService/init_app_defaults.py +++ b/ApiServices/InitialService/init_app_defaults.py @@ -525,22 +525,23 @@ def create_application_defaults(db_session): sup_manager_employee.password_token = PasswordModule.generate_refresher_token() with mongo_handler.collection(collection_name) as mongo_engine: existing_record = mongo_engine.find_one({"user_uu_id": str(sup_manager_employee.uu_id)}) - if not existing_record: + print('insert sup existing record',existing_record) mongo_engine.insert_one( document={ "user_uu_id": str(sup_manager_employee.uu_id), - "other_domains_list": [main_domain], + "other_domains_list": [main_domain, "management.com.tr"], "main_domain": main_domain, "modified_at": arrow.now().timestamp(), } ) else: + print('update sup existing record',existing_record) # Optionally update the existing record if needed mongo_engine.update_one( {"user_uu_id": str(sup_manager_employee.uu_id)}, {"$set": { - "other_domains_list": [main_domain], + "other_domains_list": [main_domain, "management.com.tr"], "main_domain": main_domain, "modified_at": arrow.now().timestamp(), }} diff --git a/ApiServices/InitialService/init_occ_defaults.py b/ApiServices/InitialService/init_occ_defaults.py index 23ddfb8..fd27063 100644 --- a/ApiServices/InitialService/init_occ_defaults.py +++ b/ApiServices/InitialService/init_occ_defaults.py @@ -239,34 +239,67 @@ def create_occupant_defaults(db_session): user_tenant.password_token = PasswordModule.generate_refresher_token() with mongo_handler.collection(collection_name) as mongo_engine: - mongo_engine.insert_one( - document={ - "user_uu_id": str(user_build_manager.uu_id), - "other_domains_list": [main_domain], - "main_domain": main_domain, - "modified_at": arrow.now().timestamp(), - } - ) + existing_record = mongo_engine.find_one({"user_uu_id": str(user_build_manager.uu_id)}) + if not existing_record: + mongo_engine.insert_one( + document={ + "user_uu_id": str(user_build_manager.uu_id), + "other_domains_list": [main_domain], + "main_domain": main_domain, + "modified_at": arrow.now().timestamp(), + } + ) + else: + mongo_engine.update_one( + {"user_uu_id": str(user_build_manager.uu_id)}, + {"$set": { + "other_domains_list": [main_domain], + "main_domain": main_domain, + "modified_at": arrow.now().timestamp(), + }} + ) with mongo_handler.collection(collection_name) as mongo_engine: - mongo_engine.insert_one( - document={ - "user_uu_id": str(user_owner.uu_id), - "other_domains_list": [main_domain], - "main_domain": main_domain, - "modified_at": arrow.now().timestamp(), - } - ) + existing_record = mongo_engine.find_one({"user_uu_id": str(user_owner.uu_id)}) + if not existing_record: + mongo_engine.insert_one( + document={ + "user_uu_id": str(user_owner.uu_id), + "other_domains_list": [main_domain], + "main_domain": main_domain, + "modified_at": arrow.now().timestamp(), + } + ) + else: + mongo_engine.update_one( + {"user_uu_id": str(user_owner.uu_id)}, + {"$set": { + "other_domains_list": [main_domain], + "main_domain": main_domain, + "modified_at": arrow.now().timestamp(), + }} + ) with mongo_handler.collection(collection_name) as mongo_engine: - mongo_engine.insert_one( - document={ - "user_uu_id": str(user_tenant.uu_id), - "other_domains_list": [main_domain], - "main_domain": main_domain, - "modified_at": arrow.now().timestamp(), - } - ) + existing_record = mongo_engine.find_one({"user_uu_id": str(user_tenant.uu_id)}) + if not existing_record: + mongo_engine.insert_one( + document={ + "user_uu_id": str(user_tenant.uu_id), + "other_domains_list": [main_domain], + "main_domain": main_domain, + "modified_at": arrow.now().timestamp(), + } + ) + else: + mongo_engine.update_one( + {"user_uu_id": str(user_tenant.uu_id)}, + {"$set": { + "other_domains_list": [main_domain], + "main_domain": main_domain, + "modified_at": arrow.now().timestamp(), + }} + ) created_build_living_space_prs = BuildLivingSpace.find_or_create( build_id=created_build.id, diff --git a/WebServices/client-frontend/setup-shadcn.sh b/WebServices/client-frontend/setup-shadcn.sh index b3c8d15..52dec08 100755 --- a/WebServices/client-frontend/setup-shadcn.sh +++ b/WebServices/client-frontend/setup-shadcn.sh @@ -18,20 +18,22 @@ npm config set legacy-peer-deps true # Install base components echo "🧩 Installing base shadcn/ui components..." -npx shadcn@latest add button --yes -npx shadcn@latest add form --yes -npx shadcn@latest add input --yes -npx shadcn@latest add label --yes -npx shadcn@latest add select --yes -npx shadcn@latest add checkbox --yes -npx shadcn@latest add card --yes -npx shadcn@latest add dialog --yes -npx shadcn@latest add popover --yes -npx shadcn@latest add sonner --yes -npx shadcn@latest add table --yes -npx shadcn@latest add pagination --yes -npx shadcn@latest add calendar --yes -npx shadcn@latest add date-picker --yes +npx shadcn@latest add button -y +npx shadcn@latest add form -y +npx shadcn@latest add input -y +npx shadcn@latest add label -y +npx shadcn@latest add select -y +npx shadcn@latest add checkbox -y +npx shadcn@latest add card -y +npx shadcn@latest add dialog -y +npx shadcn@latest add popover -y +npx shadcn@latest add sonner -y +npx shadcn@latest add table -y +npx shadcn@latest add pagination -y +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 # Update any dependencies with legacy peer deps echo "🔄 Updating dependencies..." diff --git a/WebServices/management-frontend/setup-shadcn.sh b/WebServices/management-frontend/setup-shadcn.sh index 0ba99ad..7897184 100755 --- a/WebServices/management-frontend/setup-shadcn.sh +++ b/WebServices/management-frontend/setup-shadcn.sh @@ -18,20 +18,22 @@ npm config set legacy-peer-deps true # Install base components echo "🧩 Installing base shadcn/ui components..." -npx shadcn@latest add button --yes -npx shadcn@latest add form --yes -npx shadcn@latest add input --yes -npx shadcn@latest add label --yes -npx shadcn@latest add select --yes -npx shadcn@latest add checkbox --yes -npx shadcn@latest add card --yes -npx shadcn@latest add dialog --yes -npx shadcn@latest add popover --yes -npx shadcn@latest add sonner --yes -npx shadcn@latest add table --yes -npx shadcn@latest add pagination --yes -npx shadcn@latest add calendar --yes -npx shadcn@latest add date-picker --yes +npx shadcn@latest add button -y +npx shadcn@latest add form -y +npx shadcn@latest add input -y +npx shadcn@latest add label -y +npx shadcn@latest add select -y +npx shadcn@latest add checkbox -y +npx shadcn@latest add card -y +npx shadcn@latest add dialog -y +npx shadcn@latest add popover -y +npx shadcn@latest add sonner -y +npx shadcn@latest add table -y +npx shadcn@latest add pagination -y +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 # Update any dependencies with legacy peer deps echo "🔄 Updating dependencies..." diff --git a/WebServices/management-frontend/src/app/api/applications/route.ts b/WebServices/management-frontend/src/app/api/applications/route.ts new file mode 100644 index 0000000..3a06901 --- /dev/null +++ b/WebServices/management-frontend/src/app/api/applications/route.ts @@ -0,0 +1,86 @@ +import { NextRequest, NextResponse } from "next/server"; +import { listApplications } from "@/apicalls/application/application"; + +export async function GET(request: NextRequest) { + try { + // Get query parameters + const searchParams = request.nextUrl.searchParams; + + // Extract pagination parameters + const page = parseInt(searchParams.get("page") || "1"); + const size = parseInt(searchParams.get("size") || "10"); + + // Extract sorting parameters + const orderField = searchParams.getAll("orderField") || ["name"]; + const orderType = searchParams.getAll("orderType") || ["asc"]; + + // Extract query filters + const query: Record = {}; + for (const [key, value] of searchParams.entries()) { + if (!["page", "size", "orderField", "orderType"].includes(key)) { + query[key] = value; + } + } + + // Call the actual API function + const response = await listApplications({ + page, + size, + orderField, + orderType, + query, + }); + + // Return the response + return NextResponse.json({ + data: response.data || [], + pagination: response.pagination || { + page, + size, + totalCount: 0, + totalItems: 0, + totalPages: 0, + pageCount: 0, + orderField, + orderType, + query, + next: false, + back: false, + }, + }); + } catch (error) { + console.error("API error:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 } + ); + } +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + + // Here you would call your actual API function to create a new application + // For example: const result = await createApplication(body); + + // For now, we'll return a mock response + return NextResponse.json( + { + success: true, + data: { + id: Math.floor(Math.random() * 1000), + ...body, + createdAt: new Date().toISOString(), + }, + }, + { status: 201 } + ); + } catch (error) { + console.error("API error:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 } + ); + } +} diff --git a/WebServices/management-frontend/src/app/api/data/route.ts b/WebServices/management-frontend/src/app/api/data/route.ts new file mode 100644 index 0000000..a9a46b9 --- /dev/null +++ b/WebServices/management-frontend/src/app/api/data/route.ts @@ -0,0 +1,146 @@ +import { NextRequest, NextResponse } from "next/server"; + +// Generic API handler that can be extended for different data types +export async function GET(request: NextRequest) { + try { + // Get query parameters + const searchParams = request.nextUrl.searchParams; + + // Extract pagination parameters + const page = parseInt(searchParams.get("page") || "1"); + const size = parseInt(searchParams.get("size") || "10"); + + // Extract sorting parameters + const orderField = searchParams.getAll("orderField") || ["name"]; + const orderType = searchParams.getAll("orderType") || ["asc"]; + + // Extract query filters + const query: Record = {}; + for (const [key, value] of searchParams.entries()) { + if (!["page", "size", "orderField", "orderType"].includes(key)) { + query[key] = value; + } + } + + // This is where you would call your actual data service + // For example: const result = await dataService.getData(page, size, orderField, orderType, query); + + // Define the data type for our mock items + interface MockItem { + id: number; + name: string; + description: string; + createdAt: string; + application_code: string; + site_url: string; + application_type: string; + [key: string]: string | number; // Index signature to allow string indexing + } + + // Generate mock data with application-specific fields + const allMockData: MockItem[] = Array.from({ length: 100 }, (_, i) => ({ + id: i + 1, + name: `Application ${i + 1}`, + description: `Description for application ${i + 1}`, + createdAt: new Date().toISOString(), + application_code: `APP-${1000 + i}`, + site_url: `/app-${i + 1}`, + application_type: i % 3 === 0 ? 'Web' : i % 3 === 1 ? 'Mobile' : 'Desktop', + })); + + // Apply filtering based on query parameters + let filteredData = [...allMockData]; + + // Apply simple filtering for demonstration + if (Object.keys(query).length > 0) { + filteredData = filteredData.filter(item => { + return Object.entries(query).every(([key, value]) => { + // Handle special operators like __ilike + if (key.includes('__ilike')) { + const actualKey = key.split('__')[0]; + const searchValue = String(value).replace(/%/g, ''); + return String(item[actualKey]).toLowerCase().includes(searchValue.toLowerCase()); + } + + // Handle exact match + return String(item[key]).toLowerCase() === String(value).toLowerCase(); + }); + }); + } + + // Apply sorting + if (orderField.length > 0 && orderType.length > 0) { + const field = orderField[0]; + const direction = orderType[0]; + + filteredData.sort((a, b) => { + if (direction === 'asc') { + return String(a[field]).localeCompare(String(b[field])); + } else { + return String(b[field]).localeCompare(String(a[field])); + } + }); + } + + // Calculate pagination metadata + const totalCount = filteredData.length; + const totalPages = Math.ceil(totalCount / size); + + // Apply pagination + const startIndex = (page - 1) * size; + const endIndex = startIndex + size; + const paginatedData = filteredData.slice(startIndex, endIndex); + + return NextResponse.json({ + data: paginatedData, + pagination: { + page, + size, + totalCount, + totalItems: totalCount, + totalPages, + pageCount: paginatedData.length, + orderField, + orderType, + query, + next: page < totalPages, + back: page > 1, + }, + }); + } catch (error) { + console.error("API error:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 } + ); + } +} + +// POST handler for creating new items +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + + // This is where you would call your actual data service to create a new item + // For example: const result = await dataService.createItem(body); + + // For now, we'll return a mock response + return NextResponse.json( + { + success: true, + data: { + id: Math.floor(Math.random() * 1000), + ...body, + createdAt: new Date().toISOString(), + }, + }, + { status: 201 } + ); + } catch (error) { + console.error("API error:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 } + ); + } +} diff --git a/WebServices/management-frontend/src/app/card-example/page.tsx b/WebServices/management-frontend/src/app/card-example/page.tsx new file mode 100644 index 0000000..ed1988a --- /dev/null +++ b/WebServices/management-frontend/src/app/card-example/page.tsx @@ -0,0 +1,313 @@ +"use client"; +import React, { useState } from "react"; +import { CardDisplay } from "@/components/commons/CardDisplay"; +import { SearchComponent } from "@/components/commons/SearchComponent"; +import { ActionButtonsComponent } from "@/components/commons/ActionButtonsComponent"; +import { PaginationToolsComponent } from "@/components/commons/PaginationToolsComponent"; +import { useApiData } from "@/components/commons/hooks/useApiData"; +import { User, Building } from "lucide-react"; + +// Example translations +const translations = { + en: { + create: "Create", + update: "Update", + delete: "Delete", + view: "View", + search: "Search", + typeSelection: "Type Selection", + filterSelection: "Filter Selection", + siteUrl: "Site URL", + employee: "Employee", + occupant: "Occupant", + showing: "Showing", + of: "of", + items: "items", + total: "Total", + filtered: "Filtered", + previous: "Previous", + next: "Next", + page: "Page", + itemsPerPage: "Items per page", + noData: "No data found", + // Field labels + id: "ID", + name: "Name", + description: "Description", + createdAt: "Created At", + type: "Type", + status: "Status", + application_code: "Code", + site_url: "URL", + application_type: "Type" + }, + tr: { + create: "Oluştur", + update: "Güncelle", + delete: "Sil", + view: "Görüntüle", + search: "Ara", + typeSelection: "Tür Seçimi", + filterSelection: "Filtre Seçimi", + siteUrl: "Site URL", + employee: "Çalışan", + occupant: "Sakin", + showing: "Gösteriliyor", + of: "of", + items: "öğeler", + total: "Toplam", + filtered: "Filtreli", + previous: "Önceki", + next: "Sonraki", + page: "Sayfa", + itemsPerPage: "Sayfa başına öğeler", + noData: "Veri bulunamadı", + // Field labels + id: "ID", + name: "Ad", + description: "Açıklama", + createdAt: "Oluşturulma Tarihi", + type: "Tür", + status: "Durum", + application_code: "Kod", + site_url: "URL", + application_type: "Tür" + } +}; + +// Define the data type +interface ApplicationData { + id: number; + name: string; + description: string; + createdAt: string; + application_code: string; + site_url: string; + application_type: string; +} + +// Form component for create/update +const FormComponent: React.FC<{ + initialData?: ApplicationData; + mode: "create" | "update"; + refetch?: () => void; + setMode: React.Dispatch>; + setSelectedItem: React.Dispatch>; + onCancel: () => void; + lang: string; +}> = ({ initialData, mode, refetch, setMode, onCancel, lang }) => { + // In a real application, you would implement form fields and submission logic here + return ( +
+

+ {mode === "create" ? translations[lang as "en" | "tr"].create : translations[lang as "en" | "tr"].update} +

+

This is a placeholder for the {mode} form.

+
+ + +
+
+ ); +}; + +export default function CardExamplePage() { + const [lang, setLang] = useState<"en" | "tr">("en"); + const [mode, setMode] = useState<"list" | "create" | "update">("list"); + const [selectedItem, setSelectedItem] = useState(null); + const [gridCols, setGridCols] = useState<1 | 2 | 3 | 4 | 5 | 6>(3); + + // Use the API data hook + const { + data, + pagination, + loading, + error, + updatePagination, + refetch + } = useApiData("/api/data"); + + // Fields to display in the cards + const showFields = ["application_code", "site_url", "application_type"]; + + // Search options + const searchOptions = { + typeOptions: [ + { + value: "employee", + label: translations[lang].employee, + icon: + }, + { + value: "occupant", + label: translations[lang].occupant, + icon: + } + ], + urlOptions: [ + "/dashboard", + "/individual", + "/user", + "/settings", + "/reports", + ], + additionalFields: [ + { + name: "status", + label: translations[lang as "en" | "tr"].status, + type: "select", + options: [ + { value: "active", label: "Active" }, + { value: "inactive", label: "Inactive" }, + { value: "pending", label: "Pending" } + ] + } + ] + }; + + // Handle search + const handleSearch = (query: Record) => { + updatePagination({ + page: 1, // Reset to first page on new search + query: query, + }); + }; + + // Handle card actions + const handleCardClick = (item: ApplicationData) => { + console.log("Card clicked:", item); + }; + + const handleViewClick = (item: ApplicationData) => { + console.log("View clicked:", item); + // Example: Open a modal to view details + }; + + const handleUpdateClick = (item: ApplicationData) => { + console.log("Update clicked:", item); + setSelectedItem(item); + setMode("update"); + }; + + // Handle create button click + const handleCreateClick = () => { + setSelectedItem(null); + setMode("create"); + }; + + // Handle cancel + const handleCancel = () => { + setMode("list"); + setSelectedItem(null); + }; + + return ( +
+
+

Card Example Page

+
+
+ Grid Size: + +
+
+ +
+
+
+ + {mode === "list" ? ( + <> + {/* Search Component */} + + + {/* Action Buttons Component */} + + + {/* Card Display Component */} +
+ +
+ + {/* Pagination Tools Component */} +
+ +
+ + ) : ( + + )} +
+ ); +} diff --git a/WebServices/management-frontend/src/app/example/page.tsx b/WebServices/management-frontend/src/app/example/page.tsx new file mode 100644 index 0000000..5e74981 --- /dev/null +++ b/WebServices/management-frontend/src/app/example/page.tsx @@ -0,0 +1,208 @@ +"use client"; +import React, { useState } from "react"; +import { CardDisplay, useApiData } from "@/components/commons"; +import { User, Building } from "lucide-react"; + +// Example translations +const translations = { + en: { + create: "Create", + update: "Update", + delete: "Delete", + view: "View", + search: "Search", + typeSelection: "Type Selection", + filterSelection: "Filter Selection", + siteUrl: "Site URL", + employee: "Employee", + occupant: "Occupant", + showing: "Showing", + of: "of", + items: "items", + total: "Total", + filtered: "Filtered", + previous: "Previous", + next: "Next", + page: "Page", + itemsPerPage: "Items per page", + noData: "No data found", + // Field labels + id: "ID", + name: "Name", + description: "Description", + createdAt: "Created At", + type: "Type", + status: "Status" + }, + tr: { + create: "Oluştur", + update: "Güncelle", + delete: "Sil", + view: "Görüntüle", + search: "Ara", + typeSelection: "Tür Seçimi", + filterSelection: "Filtre Seçimi", + siteUrl: "Site URL", + employee: "Çalışan", + occupant: "Sakin", + showing: "Gösteriliyor", + of: "of", + items: "öğeler", + total: "Toplam", + filtered: "Filtreli", + previous: "Önceki", + next: "Sonraki", + page: "Sayfa", + itemsPerPage: "Sayfa başına öğeler", + noData: "Veri bulunamadı", + // Field labels + id: "ID", + name: "Ad", + description: "Açıklama", + createdAt: "Oluşturulma Tarihi", + type: "Tür", + status: "Durum" + } +}; + +// Define the data type +interface ExampleData { + id: number; + name: string; + description: string; + createdAt: string; + type?: string; + status?: string; +} + +// Form component for create/update +const FormComponent: React.FC<{ + initialData?: ExampleData; + mode: "create" | "update"; + refetch?: () => void; + setMode: React.Dispatch>; + setSelectedItem: React.Dispatch>; + onCancel: () => void; + lang: string; +}> = ({ initialData, mode, refetch, setMode, onCancel, lang }) => { + // In a real application, you would implement form fields and submission logic here + return ( +
+

+ {mode === "create" ? translations[lang as "en" | "tr"].create : translations[lang as "en" | "tr"].update} +

+

This is a placeholder for the {mode} form.

+
+ + +
+
+ ); +}; + +export default function ExamplePage() { + const [lang, setLang] = useState<"en" | "tr">("en"); + + // Use the API data hook + const { + data, + pagination, + loading, + error, + updatePagination, + refetch + } = useApiData("/api/data"); + + // Fields to display in the table + const showFields = ["id", "name", "description", "createdAt"]; + + // Search options + const searchOptions = { + typeOptions: [ + { + value: "employee", + label: translations[lang as "en" | "tr"].employee, + icon: + }, + { + value: "occupant", + label: translations[lang as "en" | "tr"].occupant, + icon: + } + ], + urlOptions: [ + "/dashboard", + "/individual", + "/user", + "/settings", + "/reports", + ], + additionalFields: [ + { + name: "status", + label: translations[lang as "en" | "tr"].status, + type: "select", + options: [ + { value: "active", label: "Active" }, + { value: "inactive", label: "Inactive" }, + { value: "pending", label: "Pending" } + ] + } + ] + }; + + // Custom cell renderer example + const renderCustomCell = (item: ExampleData, field: string) => { + if (field === "createdAt") { + return new Date(item.createdAt).toLocaleDateString(); + } + return item[field as keyof ExampleData]; + }; + + return ( +
+
+

Example Page

+
+ +
+
+ + + showFields={showFields} + data={data} + lang={lang} + translations={translations} + pagination={pagination} + updatePagination={updatePagination} + error={error} + loading={loading} + refetch={refetch} + searchOptions={searchOptions} + renderCustomCell={renderCustomCell} + FormComponent={FormComponent} + /> +
+ ); +} diff --git a/WebServices/management-frontend/src/components/Pages/application/SearchComponent.tsx b/WebServices/management-frontend/src/components/Pages/application/SearchComponent.tsx index 14840b1..cbd64ce 100644 --- a/WebServices/management-frontend/src/components/Pages/application/SearchComponent.tsx +++ b/WebServices/management-frontend/src/components/Pages/application/SearchComponent.tsx @@ -27,7 +27,7 @@ export const SearchComponent: React.FC = ({ // Handle selection button click const handleTypeSelect = (type: "employee" | "occupant") => { setSelectedType(type); - + // Include type in search query handleSearch(searchQuery, selectedUrl, type); }; @@ -35,13 +35,15 @@ export const SearchComponent: React.FC = ({ // Handle search with all parameters const handleSearch = (query: string, url: string, type: "employee" | "occupant") => { const searchParams: Record = {}; - + if (url) { searchParams.site_url = url; } - searchParams.name = query + if (query) { + searchParams.name = query + } searchParams.application_for = type === "employee" ? "EMP" : "OCC"; - + // Call onSearch with the search parameters // The parent component will handle resetting pagination onSearch(searchParams); @@ -102,12 +104,12 @@ export const SearchComponent: React.FC = ({ className="pl-8 w-full h-10" /> - + ))} + + )} + + {/* Filters on the right (w-1/2) */} +
0 ? 'md:w-1/2 md:pl-4' : ''} flex flex-col space-y-4`}> +
+ + {t.filterSelection || "Filter Selection"} +
+ + {/* Search input */} +
+ +
+ setSearchQuery(e.target.value)} + onKeyUp={(e) => { + if (e.key === 'Enter') { + handleSearch(searchQuery, selectedUrl, selectedType, additionalValues); + } + }} + className="pl-8 w-full h-10" + /> + + +
+
+ + {/* Site URL dropdown */} + {urlOptions.length > 0 && ( +
+ +
+ +
+
+ )} + + {/* Additional fields */} + {additionalFields.map((field) => ( +
+ + {field.type === "text" ? ( + handleAdditionalFieldChange(field.name, e.target.value)} + className="w-full h-10" + /> + ) : ( + + )} +
+ ))} +
+ + + + ); +}; diff --git a/WebServices/management-frontend/src/components/commons/hooks/useApiData.ts b/WebServices/management-frontend/src/components/commons/hooks/useApiData.ts new file mode 100644 index 0000000..085fbb0 --- /dev/null +++ b/WebServices/management-frontend/src/components/commons/hooks/useApiData.ts @@ -0,0 +1,75 @@ +import { useDataFetching, RequestParams, ApiResponse } from "./useDataFetching"; + +/** + * 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( + endpoint: string, + initialParams: Partial = {} +) { + // Define the fetch function that will be passed to useDataFetching + const fetchFromApi = async (params: RequestParams): Promise> => { + try { + // Construct query parameters + const queryParams = new URLSearchParams(); + + // Add pagination parameters + queryParams.append("page", params.page.toString()); + queryParams.append("size", params.size.toString()); + + // Add sorting parameters + if (params.orderField && params.orderField.length > 0) { + params.orderField.forEach((field, index) => { + queryParams.append("orderField", field); + if (params.orderType && params.orderType[index]) { + queryParams.append("orderType", params.orderType[index]); + } + }); + } + + // Add query filters + if (params.query && Object.keys(params.query).length > 0) { + Object.entries(params.query).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== "") { + queryParams.append(key, value.toString()); + } + }); + } + + // Make the API request + const response = await fetch(`${endpoint}?${queryParams.toString()}`); + + 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(fetchFromApi, initialParams); +} diff --git a/WebServices/management-frontend/src/components/commons/hooks/useDataFetching.ts b/WebServices/management-frontend/src/components/commons/hooks/useDataFetching.ts new file mode 100644 index 0000000..4f733d6 --- /dev/null +++ b/WebServices/management-frontend/src/components/commons/hooks/useDataFetching.ts @@ -0,0 +1,193 @@ +import { useState, useEffect, useCallback, useRef } from "react"; + +export interface RequestParams { + page: number; + size: number; + orderField: string[]; + orderType: string[]; + query: Record; +} + +export interface ResponseMetadata { + totalCount: number; + totalItems: number; + totalPages: number; + pageCount: number; + allCount?: number; + next: boolean; + back: boolean; +} + +export interface PagePagination extends RequestParams, ResponseMetadata {} + +export interface ApiResponse { + 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( + fetchFunction: (params: RequestParams) => Promise>, + initialParams: Partial = {} +) { + const [data, setData] = useState([]); + + // Request parameters - these are controlled by the user + const [requestParams, setRequestParams] = useState({ + 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({ + totalCount: 0, + totalItems: 0, + totalPages: 0, + pageCount: 0, + next: true, + back: false, + }); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(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); + + 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) => { + // Transform query parameters to use __ilike with %value% format + if (updates.query) { + const transformedQuery: Record = {}; + + 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, + }; +} diff --git a/WebServices/management-frontend/src/components/commons/index.ts b/WebServices/management-frontend/src/components/commons/index.ts new file mode 100644 index 0000000..3d2e32a --- /dev/null +++ b/WebServices/management-frontend/src/components/commons/index.ts @@ -0,0 +1,9 @@ +// Export all components from the commons directory +export { CardDisplay } from './CardDisplay'; +export { SearchComponent } from './SearchComponent'; +export { ActionButtonsComponent } from './ActionButtonsComponent'; +export { PaginationToolsComponent } from './PaginationToolsComponent'; + +// Export hooks +export { useDataFetching, type RequestParams, type ResponseMetadata, type PagePagination, type ApiResponse } from './hooks/useDataFetching'; +export { useApiData } from './hooks/useApiData'; diff --git a/WebServices/management-frontend/src/components/componentGroups/CardDisplay/ReadMe.md b/WebServices/management-frontend/src/components/componentGroups/CardDisplay/ReadMe.md new file mode 100644 index 0000000..ecf713e --- /dev/null +++ b/WebServices/management-frontend/src/components/componentGroups/CardDisplay/ReadMe.md @@ -0,0 +1,86 @@ +Card Display which includes + +/api/... +async POST somefunction() => /api/... + +/page.tsx +I want create a nextjs api that fecth data instead having below code in schema +```tsx +export const fetchApplicationData = async ({ + page = 1, + size = 10, + orderFields = ["name"], + orderTypes = ["asc"], + query = {}, +}: { + page?: number; + size?: number; + orderFields?: string[]; + orderTypes?: string[]; + query?: Record; +}) => { + // Call the actual API function + try { + const response = await listApplications({ + page, + size, + orderField: orderFields, + orderType: orderTypes, + query, + }); + + return { + data: response.data, + pagination: response.pagination, + }; + } catch (error) { + console.error("Error fetching application data:", error); + return { + data: [], + pagination: { + page, + size, + totalCount: 0, + totalItems: 0, + totalPages: 0, + pageCount: 0, + orderField: orderFields || [], + orderType: orderTypes || [], + query: {}, + } as PagePagination, + }; + } +}; +``` + + + +I want all these components and default return of my external api which is +interface ApiResponse { + data: any[]; + pagination: PagePagination; +} +@/components/schemas +@/components/commons/CardDisplay +@/components/commons/PaginationToolsComponent +@/components/commons/...ImportableComponents // other importable components + +```tsx +const {data, pagination, loading, error, updatePagination, refetch} = fecthDataFromApi(); +const showFields = ["uu_id", "Field1", "Field2"]; +const [mode, setMode] = useState<"list" | "create" | "update">("list"); + +// Importable components + + + +``` diff --git a/WebServices/management-frontend/src/components/componentGroups/CardDisplay/Screenshot from 2025-04-29 19-36-45.png b/WebServices/management-frontend/src/components/componentGroups/CardDisplay/Screenshot from 2025-04-29 19-36-45.png new file mode 100644 index 0000000..309f151 Binary files /dev/null and b/WebServices/management-frontend/src/components/componentGroups/CardDisplay/Screenshot from 2025-04-29 19-36-45.png differ diff --git a/WebServices/management-frontend/src/components/ui/skeleton.tsx b/WebServices/management-frontend/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..32ea0ef --- /dev/null +++ b/WebServices/management-frontend/src/components/ui/skeleton.tsx @@ -0,0 +1,13 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/WebServices/management-frontend/src/components/ui/table.tsx b/WebServices/management-frontend/src/components/ui/table.tsx new file mode 100644 index 0000000..51b74dd --- /dev/null +++ b/WebServices/management-frontend/src/components/ui/table.tsx @@ -0,0 +1,116 @@ +"use client" + +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Table({ className, ...props }: React.ComponentProps<"table">) { + return ( +
+ + + ) +} + +function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { + return ( + + ) +} + +function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { + return ( + + ) +} + +function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { + return ( + tr]:last:border-b-0", + className + )} + {...props} + /> + ) +} + +function TableRow({ className, ...props }: React.ComponentProps<"tr">) { + return ( + + ) +} + +function TableHead({ className, ...props }: React.ComponentProps<"th">) { + return ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCell({ className, ...props }: React.ComponentProps<"td">) { + return ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCaption({ + className, + ...props +}: React.ComponentProps<"caption">) { + return ( +
+ ) +} + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}