components stablized
This commit is contained in:
@@ -1022,6 +1022,7 @@
|
|||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
|
||||||
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
|
"integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-compose-refs": "1.1.2"
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ npx shadcn@latest add calendar -y
|
|||||||
npx shadcn@latest add date-picker -y
|
npx shadcn@latest add date-picker -y
|
||||||
npx shadcn@latest add skeleton -y
|
npx shadcn@latest add skeleton -y
|
||||||
npx shadcn@latest add table -y
|
npx shadcn@latest add table -y
|
||||||
|
npx shadcn@latest add textarea -y
|
||||||
|
|
||||||
# Update any dependencies with legacy peer deps
|
# Update any dependencies with legacy peer deps
|
||||||
echo "🔄 Updating dependencies..."
|
echo "🔄 Updating dependencies..."
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Header from "@/components/header/Header";
|
import Header from "@/components/header/Header";
|
||||||
import ClientMenu from "@/components/menu/menu";
|
import ClientMenu from "@/components/menu/menu";
|
||||||
import { retrievePageByUrl } from "@/components/Pages/pageRetriever";
|
import { retrievePageByUrl } from "@/eventRouters/pageRetriever";
|
||||||
|
|
||||||
async function DashboardPage({
|
async function DashboardPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import Header from "@/components/header/Header";
|
import Header from "@/components/header/Header";
|
||||||
import ClientMenu from "@/components/menu/menu";
|
import ClientMenu from "@/components/menu/menu";
|
||||||
import { retrievePageByUrl } from "@/components/Pages/pageRetriever";
|
import { retrievePageByUrl } from "@/eventRouters/pageRetriever";
|
||||||
|
|
||||||
async function PageDashboard({
|
async function PageDashboard({
|
||||||
searchParams,
|
searchParams,
|
||||||
|
|||||||
@@ -1,81 +1,76 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { listApplications } from "@/apicalls/application/application";
|
import { listApplications } from "@/apicalls/application/application";
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
// Get query parameters
|
const requestBody = await request.json();
|
||||||
const searchParams = request.nextUrl.searchParams;
|
|
||||||
|
|
||||||
// Extract pagination parameters
|
// Check if this is a list request or a create request
|
||||||
const page = parseInt(searchParams.get("page") || "1");
|
// If action is 'list' or if pagination parameters are present, treat as a list request
|
||||||
const size = parseInt(searchParams.get("size") || "10");
|
if (requestBody.action === 'list' || requestBody.page !== undefined) {
|
||||||
|
// Extract pagination parameters with defaults
|
||||||
// Extract sorting parameters
|
const page = requestBody.page || 1;
|
||||||
const orderField = searchParams.getAll("orderField") || ["name"];
|
const size = requestBody.size || 10;
|
||||||
const orderType = searchParams.getAll("orderType") || ["asc"];
|
|
||||||
|
// Extract sorting parameters with defaults
|
||||||
// Extract query filters
|
const orderField = requestBody.orderField || ["name"];
|
||||||
const query: Record<string, any> = {};
|
const orderType = requestBody.orderType || ["asc"];
|
||||||
for (const [key, value] of searchParams.entries()) {
|
|
||||||
if (!["page", "size", "orderField", "orderType"].includes(key)) {
|
// Extract query filters
|
||||||
query[key] = value;
|
const query = requestBody.query || {};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the actual API function
|
// Call the actual API function for listing
|
||||||
const response = await listApplications({
|
const response = await listApplications({
|
||||||
page,
|
|
||||||
size,
|
|
||||||
orderField,
|
|
||||||
orderType,
|
|
||||||
query,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return the response
|
|
||||||
return NextResponse.json({
|
|
||||||
data: response.data || [],
|
|
||||||
pagination: response.pagination || {
|
|
||||||
page,
|
page,
|
||||||
size,
|
size,
|
||||||
totalCount: 0,
|
|
||||||
totalItems: 0,
|
|
||||||
totalPages: 0,
|
|
||||||
pageCount: 0,
|
|
||||||
orderField,
|
orderField,
|
||||||
orderType,
|
orderType,
|
||||||
query,
|
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) {
|
// Return the list response
|
||||||
try {
|
return NextResponse.json({
|
||||||
const body = await request.json();
|
data: response.data || [],
|
||||||
|
pagination: response.pagination || {
|
||||||
// Here you would call your actual API function to create a new application
|
page,
|
||||||
// For example: const result = await createApplication(body);
|
size,
|
||||||
|
totalCount: 0,
|
||||||
// For now, we'll return a mock response
|
totalItems: 0,
|
||||||
return NextResponse.json(
|
totalPages: 0,
|
||||||
{
|
pageCount: 0,
|
||||||
success: true,
|
orderField,
|
||||||
data: {
|
orderType,
|
||||||
id: Math.floor(Math.random() * 1000),
|
query,
|
||||||
...body,
|
next: false,
|
||||||
createdAt: new Date().toISOString(),
|
back: false,
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
{ status: 201 }
|
}
|
||||||
);
|
// If action is 'create' or no action is specified (assuming it's a create request)
|
||||||
|
else if (requestBody.action === 'create' || !requestBody.action) {
|
||||||
|
// Here you would call your actual API function to create a new application
|
||||||
|
// For example: const result = await createApplication(requestBody.data);
|
||||||
|
|
||||||
|
// For now, we'll return a mock response
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
id: Math.floor(Math.random() * 1000),
|
||||||
|
...requestBody.data,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ status: 201 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// If the action is not recognized
|
||||||
|
else {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Invalid action specified" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("API error:", error);
|
console.error("API error:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { CardDisplay } from "@/components/commons/CardDisplay";
|
import { CardDisplay } from "@/components/common/CardDisplay";
|
||||||
import { SearchComponent } from "@/components/commons/SearchComponent";
|
import { TextQueryModifier, SelectQueryModifier, TypeQueryModifier } from "@/components/common/QueryModifiers";
|
||||||
import { ActionButtonsComponent } from "@/components/commons/ActionButtonsComponent";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { PaginationToolsComponent } from "@/components/commons/PaginationToolsComponent";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useApiData } from "@/components/commons/hooks/useApiData";
|
import { Filter } from "lucide-react";
|
||||||
|
import { CreateButton } from "@/components/common/ActionButtonsDisplay/CreateButton";
|
||||||
|
import { PaginationToolsComponent } from "@/components/common/PaginationModifiers/PaginationToolsComponent";
|
||||||
|
import { useApiData } from "@/components/common/hooks/useApiData";
|
||||||
import { User, Building } from "lucide-react";
|
import { User, Building } from "lucide-react";
|
||||||
|
import { GridSelectionComponent, GridSize } from "@/components/common/HeaderSelections/GridSelectionComponent";
|
||||||
|
import { LanguageSelectionComponent, Language } from "@/components/common/HeaderSelections/LanguageSelectionComponent";
|
||||||
|
import { FormDisplay } from "@/components/common/FormDisplay/FormDisplay";
|
||||||
|
import type { FormMode } from "@/components/common/FormDisplay/types";
|
||||||
|
|
||||||
// Example translations
|
// Example translations
|
||||||
const translations = {
|
const translations = {
|
||||||
@@ -18,6 +25,7 @@ const translations = {
|
|||||||
typeSelection: "Type Selection",
|
typeSelection: "Type Selection",
|
||||||
filterSelection: "Filter Selection",
|
filterSelection: "Filter Selection",
|
||||||
siteUrl: "Site URL",
|
siteUrl: "Site URL",
|
||||||
|
resetAll: "Reset All",
|
||||||
employee: "Employee",
|
employee: "Employee",
|
||||||
occupant: "Occupant",
|
occupant: "Occupant",
|
||||||
showing: "Showing",
|
showing: "Showing",
|
||||||
@@ -50,6 +58,7 @@ const translations = {
|
|||||||
typeSelection: "Tür Seçimi",
|
typeSelection: "Tür Seçimi",
|
||||||
filterSelection: "Filtre Seçimi",
|
filterSelection: "Filtre Seçimi",
|
||||||
siteUrl: "Site URL",
|
siteUrl: "Site URL",
|
||||||
|
resetAll: "Tümünü Sıfırla",
|
||||||
employee: "Çalışan",
|
employee: "Çalışan",
|
||||||
occupant: "Sakin",
|
occupant: "Sakin",
|
||||||
showing: "Gösteriliyor",
|
showing: "Gösteriliyor",
|
||||||
@@ -86,64 +95,27 @@ interface ApplicationData {
|
|||||||
application_type: string;
|
application_type: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Form component for create/update
|
// We're now using the modular FormDisplay component instead of an inline FormComponent
|
||||||
const FormComponent: React.FC<{
|
|
||||||
initialData?: ApplicationData;
|
|
||||||
mode: "create" | "update";
|
|
||||||
refetch?: () => void;
|
|
||||||
setMode: React.Dispatch<React.SetStateAction<"list" | "create" | "update">>;
|
|
||||||
setSelectedItem: React.Dispatch<React.SetStateAction<ApplicationData | null>>;
|
|
||||||
onCancel: () => void;
|
|
||||||
lang: string;
|
|
||||||
}> = ({ initialData, mode, refetch, setMode, onCancel, lang }) => {
|
|
||||||
// In a real application, you would implement form fields and submission logic here
|
|
||||||
return (
|
|
||||||
<div className="p-4 border rounded-lg">
|
|
||||||
<h2 className="text-xl font-bold mb-4">
|
|
||||||
{mode === "create" ? translations[lang as "en" | "tr"].create : translations[lang as "en" | "tr"].update}
|
|
||||||
</h2>
|
|
||||||
<p>This is a placeholder for the {mode} form.</p>
|
|
||||||
<div className="mt-4 flex space-x-2">
|
|
||||||
<button
|
|
||||||
className="px-4 py-2 bg-gray-200 rounded"
|
|
||||||
onClick={onCancel}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="px-4 py-2 bg-blue-500 text-white rounded"
|
|
||||||
onClick={() => {
|
|
||||||
// In a real app, you would submit the form data here
|
|
||||||
if (refetch) refetch();
|
|
||||||
setMode("list");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function CardExamplePage() {
|
export default function CardExamplePage() {
|
||||||
const [lang, setLang] = useState<"en" | "tr">("en");
|
const [lang, setLang] = useState<Language>("en");
|
||||||
const [mode, setMode] = useState<"list" | "create" | "update">("list");
|
const [mode, setMode] = useState<FormMode>("list");
|
||||||
const [selectedItem, setSelectedItem] = useState<ApplicationData | null>(null);
|
const [selectedItem, setSelectedItem] = useState<ApplicationData | null>(null);
|
||||||
const [gridCols, setGridCols] = useState<1 | 2 | 3 | 4 | 5 | 6>(3);
|
const [gridCols, setGridCols] = useState<GridSize>(3);
|
||||||
|
|
||||||
// Use the API data hook
|
// Use the API data hook
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
pagination,
|
pagination,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
updatePagination,
|
updatePagination,
|
||||||
refetch
|
refetch
|
||||||
} = useApiData<ApplicationData>("/api/data");
|
} = useApiData<ApplicationData>("/api/data");
|
||||||
|
|
||||||
// Fields to display in the cards
|
// Fields to display in the cards
|
||||||
const showFields = ["application_code", "site_url", "application_type"];
|
const showFields = ["application_code", "site_url", "application_type"];
|
||||||
|
|
||||||
// Search options
|
// Search options
|
||||||
const searchOptions = {
|
const searchOptions = {
|
||||||
typeOptions: [
|
typeOptions: [
|
||||||
@@ -169,7 +141,7 @@ export default function CardExamplePage() {
|
|||||||
{
|
{
|
||||||
name: "status",
|
name: "status",
|
||||||
label: translations[lang as "en" | "tr"].status,
|
label: translations[lang as "en" | "tr"].status,
|
||||||
type: "select",
|
type: "select" as const,
|
||||||
options: [
|
options: [
|
||||||
{ value: "active", label: "Active" },
|
{ value: "active", label: "Active" },
|
||||||
{ value: "inactive", label: "Inactive" },
|
{ value: "inactive", label: "Inactive" },
|
||||||
@@ -179,11 +151,32 @@ export default function CardExamplePage() {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle search
|
// Handle query changes
|
||||||
const handleSearch = (query: Record<string, string>) => {
|
const handleQueryChange = (key: string, value: string | null) => {
|
||||||
|
const newQuery = { ...pagination.query };
|
||||||
|
|
||||||
|
if (value === null) {
|
||||||
|
// Remove the key if value is null
|
||||||
|
delete newQuery[key];
|
||||||
|
} else if (value.trim() === "") {
|
||||||
|
// Remove the key if value is empty string
|
||||||
|
delete newQuery[key];
|
||||||
|
} else {
|
||||||
|
// Add/update the key with the value
|
||||||
|
newQuery[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
updatePagination({
|
updatePagination({
|
||||||
page: 1, // Reset to first page on new search
|
page: 1, // Reset to first page on new search
|
||||||
query: query,
|
query: newQuery,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset all filters
|
||||||
|
const handleResetAllFilters = () => {
|
||||||
|
updatePagination({
|
||||||
|
page: 1,
|
||||||
|
query: {},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -191,14 +184,13 @@ export default function CardExamplePage() {
|
|||||||
const handleCardClick = (item: ApplicationData) => {
|
const handleCardClick = (item: ApplicationData) => {
|
||||||
console.log("Card clicked:", item);
|
console.log("Card clicked:", item);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleViewClick = (item: ApplicationData) => {
|
const handleViewClick = (item: ApplicationData) => {
|
||||||
console.log("View clicked:", item);
|
setSelectedItem(item);
|
||||||
// Example: Open a modal to view details
|
setMode("view");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateClick = (item: ApplicationData) => {
|
const handleUpdateClick = (item: ApplicationData) => {
|
||||||
console.log("Update clicked:", item);
|
|
||||||
setSelectedItem(item);
|
setSelectedItem(item);
|
||||||
setMode("update");
|
setMode("update");
|
||||||
};
|
};
|
||||||
@@ -206,66 +198,135 @@ export default function CardExamplePage() {
|
|||||||
// Handle create button click
|
// Handle create button click
|
||||||
const handleCreateClick = () => {
|
const handleCreateClick = () => {
|
||||||
setSelectedItem(null);
|
setSelectedItem(null);
|
||||||
setMode("create");
|
setMode("create" as FormMode);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle cancel
|
// Handle cancel
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setMode("list");
|
setMode("list" as FormMode);
|
||||||
setSelectedItem(null);
|
setSelectedItem(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-4">
|
<div className="container mx-auto p-4">
|
||||||
<div className="mb-4 flex justify-between items-center">
|
<div className="mb-4 flex justify-between items-center">
|
||||||
<h1 className="text-2xl font-bold">Card Example Page</h1>
|
<h1 className="text-2xl font-bold">Card Example Page</h1>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="flex items-center">
|
<GridSelectionComponent
|
||||||
<span className="mr-2 text-sm">Grid Size:</span>
|
gridCols={gridCols}
|
||||||
<select
|
setGridCols={setGridCols}
|
||||||
value={gridCols}
|
translations={translations}
|
||||||
onChange={(e) => setGridCols(Number(e.target.value) as 1 | 2 | 3 | 4 | 5 | 6)}
|
lang={lang}
|
||||||
className="p-2 border rounded"
|
/>
|
||||||
>
|
|
||||||
<option value="1">1 Column</option>
|
|
||||||
<option value="2">2 Columns</option>
|
|
||||||
<option value="3">3 Columns</option>
|
|
||||||
<option value="4">4 Columns</option>
|
|
||||||
<option value="5">5 Columns</option>
|
|
||||||
<option value="6">6 Columns</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<select
|
<LanguageSelectionComponent
|
||||||
value={lang}
|
lang={lang}
|
||||||
onChange={(e) => setLang(e.target.value as "en" | "tr")}
|
setLang={setLang}
|
||||||
className="p-2 border rounded"
|
translations={translations}
|
||||||
>
|
/>
|
||||||
<option value="en">English</option>
|
|
||||||
<option value="tr">Turkish</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{mode === "list" ? (
|
{mode === "list" ? (
|
||||||
<>
|
<>
|
||||||
{/* Search Component */}
|
{/* Search Filters */}
|
||||||
<SearchComponent
|
<Card>
|
||||||
onSearch={handleSearch}
|
<CardContent className="pt-6">
|
||||||
translations={translations}
|
<div className="flex flex-col md:flex-row">
|
||||||
lang={lang}
|
{/* Type selection - vertical on the left */}
|
||||||
typeOptions={searchOptions.typeOptions}
|
<div className="w-full md:w-1/2 md:pr-4">
|
||||||
urlOptions={searchOptions.urlOptions}
|
<TypeQueryModifier
|
||||||
additionalFields={searchOptions.additionalFields}
|
fieldKey="type"
|
||||||
/>
|
value={pagination.query["type__ilike"] ? pagination.query["type__ilike"].replace(/%/g, "") : ""}
|
||||||
|
options={searchOptions.typeOptions}
|
||||||
|
onQueryChange={handleQueryChange}
|
||||||
|
translations={translations}
|
||||||
|
lang={lang}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Action Buttons Component */}
|
{/* Filters on the right */}
|
||||||
<ActionButtonsComponent
|
<div className={`w-full ${searchOptions.typeOptions.length > 0 ? 'md:w-1/2 md:pl-4' : ''} flex flex-col space-y-4`}>
|
||||||
onCreateClick={handleCreateClick}
|
<div className="font-medium text-sm mb-2 flex justify-between items-center">
|
||||||
translations={translations}
|
<div className="flex items-center">
|
||||||
lang={lang}
|
<Filter className="mr-2 h-4 w-4" />
|
||||||
/>
|
{translations[lang].filterSelection || "Filter Selection"}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleResetAllFilters}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
{translations[lang].resetAll || "Reset All"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search input */}
|
||||||
|
<TextQueryModifier
|
||||||
|
fieldKey="name"
|
||||||
|
value={pagination.query["name__ilike"] ? pagination.query["name__ilike"].replace(/%/g, "") : ""}
|
||||||
|
label={translations[lang].search || "Search"}
|
||||||
|
onQueryChange={handleQueryChange}
|
||||||
|
translations={translations}
|
||||||
|
lang={lang}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Site URL dropdown */}
|
||||||
|
{searchOptions.urlOptions.length > 0 && (
|
||||||
|
<SelectQueryModifier
|
||||||
|
fieldKey="site_url"
|
||||||
|
value={pagination.query["site_url__ilike"] ? pagination.query["site_url__ilike"].replace(/%/g, "") : ""}
|
||||||
|
label={translations[lang].siteUrl || "Site URL"}
|
||||||
|
options={searchOptions.urlOptions.map(url => ({ value: url, label: url }))}
|
||||||
|
onQueryChange={handleQueryChange}
|
||||||
|
translations={translations}
|
||||||
|
lang={lang}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Additional fields */}
|
||||||
|
{searchOptions.additionalFields.map(field => (
|
||||||
|
<SelectQueryModifier
|
||||||
|
key={field.name}
|
||||||
|
fieldKey={field.name}
|
||||||
|
value={pagination.query[`${field.name}__ilike`] ? pagination.query[`${field.name}__ilike`].replace(/%/g, "") : ""}
|
||||||
|
label={field.label}
|
||||||
|
options={field.options || []}
|
||||||
|
onQueryChange={handleQueryChange}
|
||||||
|
translations={translations}
|
||||||
|
lang={lang}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Create Button */}
|
||||||
|
<Card className="my-4">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<CreateButton
|
||||||
|
onClick={handleCreateClick}
|
||||||
|
translations={translations}
|
||||||
|
lang={lang}
|
||||||
|
/>
|
||||||
|
</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 */}
|
{/* Card Display Component */}
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
@@ -285,20 +346,9 @@ export default function CardExamplePage() {
|
|||||||
onUpdateClick={handleUpdateClick}
|
onUpdateClick={handleUpdateClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination Tools Component */}
|
|
||||||
<div className="mt-4">
|
|
||||||
<PaginationToolsComponent
|
|
||||||
pagination={pagination}
|
|
||||||
updatePagination={updatePagination}
|
|
||||||
loading={loading}
|
|
||||||
lang={lang}
|
|
||||||
translations={translations}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<FormComponent
|
<FormDisplay<ApplicationData>
|
||||||
initialData={selectedItem || undefined}
|
initialData={selectedItem || undefined}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
refetch={refetch}
|
refetch={refetch}
|
||||||
@@ -306,6 +356,7 @@ export default function CardExamplePage() {
|
|||||||
setSelectedItem={setSelectedItem}
|
setSelectedItem={setSelectedItem}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { CardDisplay, useApiData } from "@/components/commons";
|
import { CardDisplay, useApiData } from "@/components/common";
|
||||||
import { User, Building } from "lucide-react";
|
import { User, Building } from "lucide-react";
|
||||||
|
|
||||||
// Example translations
|
// Example translations
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Plus } from "lucide-react";
|
|
||||||
import { LanguageTranslation } from "@/components/validations/translations/translation";
|
|
||||||
|
|
||||||
interface ActionButtonsComponentProps {
|
|
||||||
onCreateClick: () => void;
|
|
||||||
translations: Record<string, LanguageTranslation>;
|
|
||||||
lang: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ActionButtonsComponent: React.FC<ActionButtonsComponentProps> = ({
|
|
||||||
onCreateClick,
|
|
||||||
translations,
|
|
||||||
lang,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="flex justify-end my-4">
|
|
||||||
<Button onClick={onCreateClick}>
|
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
|
||||||
Create New Application
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import { LanguageTranslation } from "@/components/validations/translations/translation";
|
|
||||||
import { ApplicationData } from "./types";
|
|
||||||
|
|
||||||
interface DataDisplayComponentProps {
|
|
||||||
data: ApplicationData[];
|
|
||||||
loading: boolean;
|
|
||||||
error: Error | null;
|
|
||||||
onUpdateClick: (item: ApplicationData) => void;
|
|
||||||
translations: Record<string, LanguageTranslation>;
|
|
||||||
lang: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DataDisplayComponent: React.FC<DataDisplayComponentProps> = ({
|
|
||||||
data,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
onUpdateClick,
|
|
||||||
translations,
|
|
||||||
lang,
|
|
||||||
}) => {
|
|
||||||
if (loading) {
|
|
||||||
return (
|
|
||||||
<div className="w-full text-center py-8">Loading applications...</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<div className="w-full text-center py-8 text-red-500">
|
|
||||||
Error loading applications: {error.message}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.length === 0) {
|
|
||||||
return <div className="w-full text-center py-8">No applications found</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-wrap -mx-2">
|
|
||||||
{data.map((app, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="w-full sm:w-1/5 p-1"
|
|
||||||
onClick={() => onUpdateClick(app)}
|
|
||||||
>
|
|
||||||
<Card className="h-full hover:bg-accent/50 cursor-pointer transition-colors">
|
|
||||||
<CardContent className="p-3">
|
|
||||||
<div className="font-medium text-sm mb-1">{app.name}</div>
|
|
||||||
<div className="space-y-0.5 text-xs">
|
|
||||||
<div className="flex">
|
|
||||||
<span className="text-muted-foreground w-10">
|
|
||||||
{translations.code &&
|
|
||||||
translations.code[lang as keyof LanguageTranslation]}
|
|
||||||
:
|
|
||||||
</span>
|
|
||||||
<span className="truncate">{app.application_code}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="text-muted-foreground w-10">
|
|
||||||
{translations.url &&
|
|
||||||
translations.url[lang as keyof LanguageTranslation]}
|
|
||||||
:
|
|
||||||
</span>
|
|
||||||
<span className="truncate">{app.site_url}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex">
|
|
||||||
<span className="text-muted-foreground w-10">
|
|
||||||
{translations.type &&
|
|
||||||
translations.type[lang as keyof LanguageTranslation]}
|
|
||||||
:
|
|
||||||
</span>
|
|
||||||
<span className="truncate">{app.application_type}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,359 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Save, ArrowLeft } from "lucide-react";
|
|
||||||
import { ApplicationData } from "./types";
|
|
||||||
import {
|
|
||||||
ApplicationFormData,
|
|
||||||
ApplicationSchema,
|
|
||||||
CreateApplicationSchema,
|
|
||||||
UpdateApplicationSchema,
|
|
||||||
fieldDefinitions,
|
|
||||||
fieldsByMode,
|
|
||||||
FieldDefinition,
|
|
||||||
} from "./schema";
|
|
||||||
import { getTranslation, LanguageKey } from "./language";
|
|
||||||
|
|
||||||
interface FormComponentProps {
|
|
||||||
initialData?: ApplicationData;
|
|
||||||
mode: "create" | "update";
|
|
||||||
refetch: () => void;
|
|
||||||
setMode: (mode: "list" | "create" | "update") => void;
|
|
||||||
setSelectedItem: (item: ApplicationData | null) => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
lang: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ValidationErrors {
|
|
||||||
[key: string]: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FormComponent: React.FC<FormComponentProps> = ({
|
|
||||||
initialData,
|
|
||||||
mode,
|
|
||||||
refetch,
|
|
||||||
setMode,
|
|
||||||
setSelectedItem,
|
|
||||||
onCancel,
|
|
||||||
lang,
|
|
||||||
}) => {
|
|
||||||
const t = getTranslation(lang as LanguageKey);
|
|
||||||
|
|
||||||
// Convert initialData to ApplicationFormData with proper defaults
|
|
||||||
const getInitialFormData = (): ApplicationFormData => {
|
|
||||||
if (initialData) {
|
|
||||||
return {
|
|
||||||
...initialData,
|
|
||||||
// Ensure required fields have defaults
|
|
||||||
active: initialData.active ?? true,
|
|
||||||
deleted: initialData.deleted ?? false,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
name: "",
|
|
||||||
application_code: "",
|
|
||||||
site_url: "",
|
|
||||||
application_type: "",
|
|
||||||
application_for: "EMP",
|
|
||||||
description: "",
|
|
||||||
active: true,
|
|
||||||
deleted: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState<ApplicationFormData>(
|
|
||||||
getInitialFormData()
|
|
||||||
);
|
|
||||||
|
|
||||||
const [validationErrors, setValidationErrors] = useState<ValidationErrors>(
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
|
|
||||||
// Get field definitions for current mode
|
|
||||||
const currentFields = fieldsByMode[mode];
|
|
||||||
const fieldDefs = fieldDefinitions.getDefinitionsByMode(mode);
|
|
||||||
|
|
||||||
// Handle form input changes
|
|
||||||
const handleInputChange = (name: string, value: any) => {
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
[name]: value,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear validation error for this field when it changes
|
|
||||||
if (validationErrors[name]) {
|
|
||||||
const newErrors = { ...validationErrors };
|
|
||||||
delete newErrors[name];
|
|
||||||
setValidationErrors(newErrors);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Validate form data based on mode
|
|
||||||
const validateForm = (): boolean => {
|
|
||||||
try {
|
|
||||||
if (mode === "create") {
|
|
||||||
CreateApplicationSchema.parse(formData);
|
|
||||||
} else if (mode === "update") {
|
|
||||||
UpdateApplicationSchema.parse(formData);
|
|
||||||
} else {
|
|
||||||
ApplicationSchema.parse(formData);
|
|
||||||
}
|
|
||||||
setValidationErrors({});
|
|
||||||
return true;
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.errors) {
|
|
||||||
const errors: ValidationErrors = {};
|
|
||||||
error.errors.forEach((err: any) => {
|
|
||||||
const field = err.path[0];
|
|
||||||
if (!errors[field]) {
|
|
||||||
errors[field] = [];
|
|
||||||
}
|
|
||||||
errors[field].push(err.message);
|
|
||||||
});
|
|
||||||
setValidationErrors(errors);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle form submission
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (isSubmitting) return;
|
|
||||||
|
|
||||||
setIsSubmitting(true);
|
|
||||||
|
|
||||||
// Validate form data
|
|
||||||
if (!validateForm()) {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// TODO: Implement API call to save the data
|
|
||||||
// For now, just go back to list mode after a delay to simulate API call
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
||||||
refetch();
|
|
||||||
setMode("list");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error saving application:", error);
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle cancel button click
|
|
||||||
const handleCancel = () => {
|
|
||||||
setSelectedItem(null);
|
|
||||||
setMode("list");
|
|
||||||
onCancel();
|
|
||||||
};
|
|
||||||
|
|
||||||
const title =
|
|
||||||
mode === "create"
|
|
||||||
? "Create Application"
|
|
||||||
: mode === "update"
|
|
||||||
? "Update Application"
|
|
||||||
: "View Application";
|
|
||||||
|
|
||||||
// Render a field based on its definition
|
|
||||||
const renderField = (fieldName: string) => {
|
|
||||||
const fieldDef = fieldDefs[
|
|
||||||
fieldName as keyof typeof fieldDefs
|
|
||||||
] as FieldDefinition;
|
|
||||||
if (!fieldDef) return null;
|
|
||||||
|
|
||||||
const hasError = !!validationErrors[fieldName];
|
|
||||||
const errorMessages = validationErrors[fieldName] || [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={fieldName} className="mb-4">
|
|
||||||
<Label
|
|
||||||
className={`block text-sm font-medium mb-1 ${
|
|
||||||
hasError ? "text-red-500" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{fieldDef.label}{" "}
|
|
||||||
{fieldDef.required && <span className="text-red-500">*</span>}
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
{fieldDef.type === "text" && !fieldDef.readOnly && (
|
|
||||||
<>
|
|
||||||
<Input
|
|
||||||
name={fieldName}
|
|
||||||
value={
|
|
||||||
(formData[fieldName as keyof ApplicationFormData] as string) ||
|
|
||||||
""
|
|
||||||
}
|
|
||||||
onChange={(e) => handleInputChange(fieldName, e.target.value)}
|
|
||||||
className={hasError ? "border-red-500" : ""}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{fieldDef.type === "text" && fieldDef.readOnly && (
|
|
||||||
<h1>
|
|
||||||
{(formData[fieldName as keyof ApplicationFormData] as string) || ""}
|
|
||||||
</h1>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{fieldDef.type === "textarea" && !fieldDef.readOnly && (
|
|
||||||
<textarea
|
|
||||||
name={fieldName}
|
|
||||||
className={`w-full min-h-[100px] rounded-md border ${
|
|
||||||
hasError ? "border-red-500" : "border-input"
|
|
||||||
} bg-transparent px-3 py-2 text-sm shadow-sm`}
|
|
||||||
value={
|
|
||||||
(formData[fieldName as keyof ApplicationFormData] as string) || ""
|
|
||||||
}
|
|
||||||
onChange={(e) => handleInputChange(fieldName, e.target.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{fieldDef.type === "textarea" && fieldDef.readOnly && (
|
|
||||||
<h1>
|
|
||||||
{(formData[fieldName as keyof ApplicationFormData] as string) || ""}
|
|
||||||
</h1>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{fieldDef.type === "select" && (
|
|
||||||
<Select
|
|
||||||
value={
|
|
||||||
(formData[fieldName as keyof ApplicationFormData] as string) || ""
|
|
||||||
}
|
|
||||||
onValueChange={(value) => handleInputChange(fieldName, value)}
|
|
||||||
disabled={fieldDef.readOnly}
|
|
||||||
>
|
|
||||||
<SelectTrigger className={hasError ? "border-red-500" : ""}>
|
|
||||||
<SelectValue
|
|
||||||
placeholder={`Select ${fieldDef.label.toLowerCase()}`}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{(fieldDef.options || []).map((option: string) => (
|
|
||||||
<SelectItem key={option} value={option}>
|
|
||||||
{option}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{fieldDef.type === "checkbox" && (
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id={fieldName}
|
|
||||||
checked={!!formData[fieldName as keyof ApplicationFormData]}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
handleInputChange(fieldName, !!checked)
|
|
||||||
}
|
|
||||||
disabled={fieldDef.readOnly}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor={fieldName}
|
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
||||||
>
|
|
||||||
{fieldDef.label}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{fieldDef.type === "date" && !fieldDef.readOnly && (
|
|
||||||
<Input
|
|
||||||
type="datetime-local"
|
|
||||||
name={fieldName}
|
|
||||||
value={
|
|
||||||
(formData[fieldName as keyof ApplicationFormData] as string) || ""
|
|
||||||
}
|
|
||||||
onChange={(e) => handleInputChange(fieldName, e.target.value)}
|
|
||||||
className={hasError ? "border-red-500" : ""}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{fieldDef.type === "date" && fieldDef.readOnly && (
|
|
||||||
<h1>
|
|
||||||
{(formData[fieldName as keyof ApplicationFormData] as string) || ""}
|
|
||||||
</h1>
|
|
||||||
)}
|
|
||||||
{hasError &&
|
|
||||||
errorMessages.map((error, index) => (
|
|
||||||
<p key={index} className="text-xs text-red-500 mt-1">
|
|
||||||
{error}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Group fields by their group property
|
|
||||||
const groupedFields: Record<string, string[]> = {};
|
|
||||||
currentFields.forEach((field) => {
|
|
||||||
const fieldDef = fieldDefs[field as keyof typeof fieldDefs];
|
|
||||||
if (fieldDef) {
|
|
||||||
const group = fieldDef.group || "other";
|
|
||||||
if (!groupedFields[group]) {
|
|
||||||
groupedFields[group] = [];
|
|
||||||
}
|
|
||||||
groupedFields[group].push(field);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
|
||||||
<CardTitle>{title}</CardTitle>
|
|
||||||
<Button variant="outline" size="sm" onClick={handleCancel}>
|
|
||||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
||||||
{t.previous}
|
|
||||||
</Button>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{/* Render fields by group */}
|
|
||||||
{Object.entries(groupedFields).map(([group, fields]) => (
|
|
||||||
<div key={group} className="mb-6">
|
|
||||||
<h3 className="text-lg font-medium mb-4 capitalize">
|
|
||||||
{group
|
|
||||||
.replace(/([A-Z])/g, " $1")
|
|
||||||
.replace(/Info$/, " Information")}
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{fields.map((field) => renderField(field))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{mode === "create" ||
|
|
||||||
(mode === "update" && (
|
|
||||||
<div className="flex justify-end mt-6">
|
|
||||||
<Button
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
className="min-w-[120px]"
|
|
||||||
>
|
|
||||||
<Save className="mr-2 h-4 w-4" />
|
|
||||||
{isSubmitting ? "Saving..." : "Save"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{
|
|
||||||
<div className="mt-6">
|
|
||||||
<h3 className="text-lg font-medium mb-4">Raw Data</h3>
|
|
||||||
<pre className="bg-muted p-4 rounded-md overflow-auto">
|
|
||||||
{JSON.stringify(formData, null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { PagePagination } from "./hooks";
|
|
||||||
import { getTranslation, LanguageKey } from "./language";
|
|
||||||
|
|
||||||
interface PaginationToolsComponentProps {
|
|
||||||
pagination: PagePagination;
|
|
||||||
updatePagination: (updates: Partial<PagePagination>) => void;
|
|
||||||
loading: boolean;
|
|
||||||
lang: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PaginationToolsComponent: React.FC<
|
|
||||||
PaginationToolsComponentProps
|
|
||||||
> = ({ pagination, updatePagination, loading, lang }) => {
|
|
||||||
const t = getTranslation(lang as LanguageKey);
|
|
||||||
|
|
||||||
const handlePageChange = (newPage: number) => {
|
|
||||||
if (newPage >= 1 && newPage <= pagination.totalPages) {
|
|
||||||
updatePagination({ page: newPage });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className="flex flex-wrap justify-between items-center mt-6 gap-4">
|
|
||||||
{/* Pagination stats - left side */}
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
<div>
|
|
||||||
{t.showing}{" "}
|
|
||||||
{/* Show the range based on filtered count when available */}
|
|
||||||
{(pagination.totalCount || pagination.allCount || 0) > 0
|
|
||||||
? (pagination.page - 1) * pagination.size + 1
|
|
||||||
: 0}{" "}
|
|
||||||
-{" "}
|
|
||||||
{Math.min(
|
|
||||||
pagination.page * pagination.size,
|
|
||||||
pagination.totalCount || pagination.allCount || 0
|
|
||||||
)}{" "}
|
|
||||||
{t.of} {pagination.totalCount || pagination.allCount || 0} {t.items}
|
|
||||||
</div>
|
|
||||||
{pagination.totalCount &&
|
|
||||||
pagination.totalCount !== (pagination.allCount || 0) && (
|
|
||||||
<div>
|
|
||||||
{t.total}: {pagination.allCount || 0} {t.items} ({t.filtered}:{" "}
|
|
||||||
{pagination.totalCount} {t.items})
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Navigation buttons - center */}
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
|
|
||||||
|
|
||||||
{
|
|
||||||
pagination.back ? (<><Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handlePageChange(pagination.page - 1)}
|
|
||||||
|
|
||||||
>
|
|
||||||
{t.previous}
|
|
||||||
</Button></>) : (<><Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
{t.previous}
|
|
||||||
</Button></>)
|
|
||||||
}
|
|
||||||
|
|
||||||
{/* Page number buttons */}
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
{Array.from(
|
|
||||||
{
|
|
||||||
length: Math.min(
|
|
||||||
5,
|
|
||||||
Math.max(
|
|
||||||
1,
|
|
||||||
Math.ceil(
|
|
||||||
(pagination.totalCount &&
|
|
||||||
pagination.totalCount !== pagination.allCount
|
|
||||||
? pagination.totalCount
|
|
||||||
: pagination.allCount || 0) / pagination.size
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
},
|
|
||||||
(_, i) => {
|
|
||||||
// Show pages around current page
|
|
||||||
let pageNum;
|
|
||||||
const calculatedTotalPages = Math.max(
|
|
||||||
1,
|
|
||||||
Math.ceil(
|
|
||||||
(pagination.totalCount &&
|
|
||||||
pagination.totalCount !== pagination.allCount
|
|
||||||
? pagination.totalCount
|
|
||||||
: pagination.allCount || 0) / pagination.size
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (calculatedTotalPages <= 5) {
|
|
||||||
pageNum = i + 1;
|
|
||||||
} else if (pagination.page <= 3) {
|
|
||||||
pageNum = i + 1;
|
|
||||||
} else if (pagination.page >= calculatedTotalPages - 2) {
|
|
||||||
pageNum = calculatedTotalPages - 4 + i;
|
|
||||||
} else {
|
|
||||||
pageNum = pagination.page - 2 + i;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={pageNum}
|
|
||||||
variant={pagination.page === pageNum ? "default" : "outline"}
|
|
||||||
size="sm"
|
|
||||||
className="w-9 h-9 p-0"
|
|
||||||
onClick={() => handlePageChange(pageNum)}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{pageNum}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
pagination.page < pagination.totalPages ? (<><Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handlePageChange(pagination.page + 1)}
|
|
||||||
>
|
|
||||||
{t.next}
|
|
||||||
</Button></>) : (<><Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
{t.next}
|
|
||||||
</Button></>)
|
|
||||||
}
|
|
||||||
|
|
||||||
{/* Page text display */}
|
|
||||||
<span className="px-4 py-1 text-sm text-muted-foreground">
|
|
||||||
{t.page} {pagination.page} {t.of}{" "}
|
|
||||||
{Math.max(
|
|
||||||
1,
|
|
||||||
Math.ceil(
|
|
||||||
(pagination.totalCount &&
|
|
||||||
pagination.totalCount !== pagination.allCount
|
|
||||||
? pagination.totalCount
|
|
||||||
: pagination.allCount || 0) / pagination.size
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Items per page selector - right side */}
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<span className="text-sm text-muted-foreground">{t.itemsPerPage}</span>
|
|
||||||
<Select
|
|
||||||
value={pagination.size.toString()}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
updatePagination({
|
|
||||||
size: Number(value),
|
|
||||||
page: 1, // Reset to first page when changing page size
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-16">
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="5">5</SelectItem>
|
|
||||||
<SelectItem value="10">10</SelectItem>
|
|
||||||
<SelectItem value="20">20</SelectItem>
|
|
||||||
<SelectItem value="50">50</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { User, Building, Filter, Search, Link } from "lucide-react";
|
|
||||||
import { LanguageTranslation } from "@/components/validations/translations/translation";
|
|
||||||
|
|
||||||
interface SearchComponentProps {
|
|
||||||
onSearch: (query: Record<string, string>) => void;
|
|
||||||
translations: Record<string, LanguageTranslation>;
|
|
||||||
lang: string;
|
|
||||||
urlOptions: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SearchComponent: React.FC<SearchComponentProps> = ({
|
|
||||||
onSearch,
|
|
||||||
translations,
|
|
||||||
lang,
|
|
||||||
urlOptions,
|
|
||||||
}) => {
|
|
||||||
const [selectedType, setSelectedType] = useState<"employee" | "occupant">("employee");
|
|
||||||
const [selectedUrl, setSelectedUrl] = useState<string>("");
|
|
||||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
|
||||||
|
|
||||||
// Handle selection button click
|
|
||||||
const handleTypeSelect = (type: "employee" | "occupant") => {
|
|
||||||
setSelectedType(type);
|
|
||||||
|
|
||||||
// Include type in search query
|
|
||||||
handleSearch(searchQuery, selectedUrl, type);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle search with all parameters
|
|
||||||
const handleSearch = (query: string, url: string, type: "employee" | "occupant") => {
|
|
||||||
const searchParams: Record<string, string> = {};
|
|
||||||
|
|
||||||
if (url) {
|
|
||||||
searchParams.site_url = url;
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardContent className="pt-6">
|
|
||||||
<div className="flex flex-row">
|
|
||||||
{/* User type selection - vertical on the left (w-1/2) */}
|
|
||||||
<div className="w-1/2 flex flex-col space-y-4 pr-4">
|
|
||||||
<div className="font-medium text-sm mb-2 flex items-center">
|
|
||||||
<User className="mr-2 h-4 w-4" />
|
|
||||||
{translations.typeSelection && translations.typeSelection[lang as keyof LanguageTranslation]}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant={selectedType === "employee" ? "default" : "outline"}
|
|
||||||
size="lg"
|
|
||||||
onClick={() => handleTypeSelect("employee")}
|
|
||||||
className="w-full h-14 mb-2"
|
|
||||||
>
|
|
||||||
<User className="mr-2 h-4 w-4" />
|
|
||||||
{translations.employee && translations.employee[lang as keyof LanguageTranslation]}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={selectedType === "occupant" ? "default" : "outline"}
|
|
||||||
size="lg"
|
|
||||||
onClick={() => handleTypeSelect("occupant")}
|
|
||||||
className="w-full h-14"
|
|
||||||
>
|
|
||||||
<Building className="mr-2 h-4 w-4" />
|
|
||||||
{translations.occupant && translations.occupant[lang as keyof LanguageTranslation]}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters on the right (w-1/2) */}
|
|
||||||
<div className="w-1/2 flex flex-col space-y-4 pl-4">
|
|
||||||
<div className="font-medium text-sm mb-2 flex items-center">
|
|
||||||
<Filter className="mr-2 h-4 w-4" />
|
|
||||||
{translations.filterSelection && translations.filterSelection[lang as keyof LanguageTranslation]}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Search input */}
|
|
||||||
<div className="w-full">
|
|
||||||
<label className="block text-xs font-medium mb-1">
|
|
||||||
{translations.search && translations.search[lang as keyof LanguageTranslation]}
|
|
||||||
</label>
|
|
||||||
<div className="relative w-full flex">
|
|
||||||
<Input
|
|
||||||
placeholder={`${translations.search && translations.search[lang as keyof LanguageTranslation]}...`}
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
onKeyUp={(e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
handleSearch(searchQuery, selectedUrl, selectedType);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="pl-8 w-full h-10"
|
|
||||||
/>
|
|
||||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
className="ml-2"
|
|
||||||
onClick={() => {
|
|
||||||
handleSearch(searchQuery, selectedUrl, selectedType);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Search className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Site URL dropdown */}
|
|
||||||
<div className="w-full">
|
|
||||||
<label className="block text-xs font-medium mb-1">
|
|
||||||
{translations.siteUrl && translations.siteUrl[lang as keyof LanguageTranslation]}
|
|
||||||
</label>
|
|
||||||
<div className="w-full">
|
|
||||||
<Select
|
|
||||||
value={selectedUrl}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
setSelectedUrl(value);
|
|
||||||
handleSearch(searchQuery, value, selectedType);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full h-10">
|
|
||||||
<SelectValue
|
|
||||||
placeholder={`${translations.siteUrl && translations.siteUrl[lang as keyof LanguageTranslation]}...`}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{urlOptions.map((url) => (
|
|
||||||
<SelectItem key={url} value={url}>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Link className="mr-2 h-3 w-3" />
|
|
||||||
{url}
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
|
||||||
import { listApplications } from "@/apicalls/application/application";
|
|
||||||
import { ApplicationData } from "./types";
|
|
||||||
|
|
||||||
export interface RequestParams {
|
|
||||||
page: number;
|
|
||||||
size: number;
|
|
||||||
orderField: string[];
|
|
||||||
orderType: string[];
|
|
||||||
query: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ResponseMetadata {
|
|
||||||
totalCount: number;
|
|
||||||
totalItems: number;
|
|
||||||
totalPages: number;
|
|
||||||
pageCount: number;
|
|
||||||
allCount?: number;
|
|
||||||
next: boolean;
|
|
||||||
back: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PagePagination extends RequestParams, ResponseMetadata {}
|
|
||||||
|
|
||||||
// Custom hook for pagination and data fetching
|
|
||||||
export function useApplicationData() {
|
|
||||||
const [data, setData] = useState<ApplicationData[]>([]);
|
|
||||||
|
|
||||||
// Request parameters - these are controlled by the user
|
|
||||||
const [requestParams, setRequestParams] = useState<RequestParams>({
|
|
||||||
page: 1,
|
|
||||||
size: 10,
|
|
||||||
orderField: ["name"],
|
|
||||||
orderType: ["asc"],
|
|
||||||
query: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Response metadata - these come from the API
|
|
||||||
const [responseMetadata, setResponseMetadata] = useState<ResponseMetadata>({
|
|
||||||
totalCount: 0,
|
|
||||||
totalItems: 0,
|
|
||||||
totalPages: 0,
|
|
||||||
pageCount: 0,
|
|
||||||
next: true,
|
|
||||||
back: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState<Error | null>(null);
|
|
||||||
|
|
||||||
const fetchApplicationsFromApi = useCallback(async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const result = await listApplications({
|
|
||||||
page: requestParams.page,
|
|
||||||
size: requestParams.size,
|
|
||||||
orderField: requestParams.orderField,
|
|
||||||
orderType: requestParams.orderType,
|
|
||||||
query: requestParams.query,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result && result.data) {
|
|
||||||
setData(result.data);
|
|
||||||
|
|
||||||
// Update response metadata from API response
|
|
||||||
if (result.pagination) {
|
|
||||||
setResponseMetadata({
|
|
||||||
totalCount: result.pagination.totalCount || 0,
|
|
||||||
totalItems: result.pagination.totalCount || 0,
|
|
||||||
totalPages: result.pagination.totalPages || 1,
|
|
||||||
pageCount: result.pagination.pageCount || 0,
|
|
||||||
allCount: result.pagination.allCount || 0,
|
|
||||||
next: result.pagination.next || true,
|
|
||||||
back: result.pagination.back || false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setError(null);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err : new Error("Unknown error"));
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
requestParams.page,
|
|
||||||
requestParams.size,
|
|
||||||
requestParams.orderField,
|
|
||||||
requestParams.orderType,
|
|
||||||
requestParams.query,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
fetchApplicationsFromApi();
|
|
||||||
}, 300); // Debounce
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [fetchApplicationsFromApi]);
|
|
||||||
|
|
||||||
const updatePagination = useCallback((updates: Partial<RequestParams>) => {
|
|
||||||
// Transform query parameters to use __ilike with %value% format
|
|
||||||
if (updates.query) {
|
|
||||||
const transformedQuery: Record<string, any> = {};
|
|
||||||
|
|
||||||
Object.entries(updates.query).forEach(([key, value]) => {
|
|
||||||
// Only transform string values that aren't already using a special operator
|
|
||||||
if (
|
|
||||||
typeof value === "string" &&
|
|
||||||
!key.includes("__") &&
|
|
||||||
value.trim() !== ""
|
|
||||||
) {
|
|
||||||
transformedQuery[`${key}__ilike`] = `%${value}%`;
|
|
||||||
} else {
|
|
||||||
transformedQuery[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
updates.query = transformedQuery;
|
|
||||||
|
|
||||||
// Always reset to page 1 when search query changes
|
|
||||||
if (!updates.hasOwnProperty("page")) {
|
|
||||||
updates.page = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset response metadata when search changes to avoid stale pagination data
|
|
||||||
setResponseMetadata({
|
|
||||||
totalCount: 0,
|
|
||||||
totalItems: 0,
|
|
||||||
totalPages: 0,
|
|
||||||
pageCount: 0,
|
|
||||||
allCount: 0,
|
|
||||||
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,
|
|
||||||
}));
|
|
||||||
fetchApplicationsFromApi();
|
|
||||||
}, [fetchApplicationsFromApi]);
|
|
||||||
|
|
||||||
// Combine request params and response metadata
|
|
||||||
const pagination: PagePagination = {
|
|
||||||
...requestParams,
|
|
||||||
...responseMetadata,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
pagination,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
updatePagination,
|
|
||||||
refetch,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
export type LanguageKey = "en" | "tr";
|
|
||||||
|
|
||||||
export interface TranslationSet {
|
|
||||||
showing: string;
|
|
||||||
of: string;
|
|
||||||
items: string;
|
|
||||||
total: string;
|
|
||||||
filtered: string;
|
|
||||||
page: string;
|
|
||||||
previous: string;
|
|
||||||
next: string;
|
|
||||||
itemsPerPage: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const translations: Record<LanguageKey, TranslationSet> = {
|
|
||||||
en: {
|
|
||||||
showing: "Showing",
|
|
||||||
of: "of",
|
|
||||||
items: "items",
|
|
||||||
total: "Total",
|
|
||||||
filtered: "Filtered",
|
|
||||||
page: "Page",
|
|
||||||
previous: "Previous",
|
|
||||||
next: "Next",
|
|
||||||
itemsPerPage: "Items per page",
|
|
||||||
},
|
|
||||||
tr: {
|
|
||||||
showing: "Gösteriliyor",
|
|
||||||
of: "/",
|
|
||||||
items: "öğe",
|
|
||||||
total: "Toplam",
|
|
||||||
filtered: "Filtrelenmiş",
|
|
||||||
page: "Sayfa",
|
|
||||||
previous: "Önceki",
|
|
||||||
next: "Sonraki",
|
|
||||||
itemsPerPage: "Sayfa başına öğe",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getTranslation(lang: LanguageKey): TranslationSet {
|
|
||||||
return translations[lang];
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { PageProps } from "@/components/validations/translations/translation";
|
|
||||||
import { useApplicationData } from "./hooks";
|
|
||||||
import { ApplicationData, translations } from "./types";
|
|
||||||
import { SearchComponent } from "./SearchComponent";
|
|
||||||
import { ActionButtonsComponent } from "./ActionButtonsComponent";
|
|
||||||
import { SortingComponent } from "./SortingComponent";
|
|
||||||
import { PaginationToolsComponent } from "./PaginationToolsComponent";
|
|
||||||
import { DataDisplayComponent } from "./DataDisplayComponent";
|
|
||||||
import { FormComponent } from "./FormComponent";
|
|
||||||
|
|
||||||
const ApplicationPage: React.FC<PageProps> = ({ lang = "en" }) => {
|
|
||||||
// Use the custom hook for paginated data
|
|
||||||
const { data, pagination, loading, error, updatePagination, refetch } =
|
|
||||||
useApplicationData();
|
|
||||||
|
|
||||||
// State for managing view/edit modes
|
|
||||||
const [mode, setMode] = useState<"list" | "create" | "update">("list");
|
|
||||||
const [selectedItem, setSelectedItem] = useState<ApplicationData | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
// // State for sorting
|
|
||||||
// const [sortField, setSortField] = useState<string | null>(null);
|
|
||||||
// const [sortDirection, setSortDirection] = useState<"asc" | "desc" | null>(
|
|
||||||
// null
|
|
||||||
// );
|
|
||||||
|
|
||||||
// Available options for dropdowns
|
|
||||||
const urlOptions = [
|
|
||||||
"/dashboard",
|
|
||||||
"/individual",
|
|
||||||
"/user",
|
|
||||||
"/settings",
|
|
||||||
"/reports",
|
|
||||||
];
|
|
||||||
|
|
||||||
const handleUpdateClick = (item: ApplicationData) => {
|
|
||||||
setSelectedItem(item);
|
|
||||||
setMode("update");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to handle the create button click
|
|
||||||
const handleCreateClick = () => {
|
|
||||||
setSelectedItem(null);
|
|
||||||
setMode("create");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle search from the SearchComponent
|
|
||||||
const handleSearch = (query: Record<string, string>) => {
|
|
||||||
updatePagination({
|
|
||||||
page: 1, // Reset to first page on new search
|
|
||||||
query: query,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// // Handle sorting
|
|
||||||
// const handleSort = (field: string) => {
|
|
||||||
// let direction: "asc" | "desc" | null = "asc";
|
|
||||||
|
|
||||||
// if (sortField === field) {
|
|
||||||
// // Toggle direction if same field is clicked
|
|
||||||
// if (sortDirection === "asc") {
|
|
||||||
// direction = "desc";
|
|
||||||
// } else if (sortDirection === "desc") {
|
|
||||||
// // Clear sorting if already desc
|
|
||||||
// field = "";
|
|
||||||
// direction = null;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// setSortField(field || null);
|
|
||||||
// setSortDirection(direction);
|
|
||||||
|
|
||||||
// updatePagination({
|
|
||||||
// orderField: field ? [field] : [],
|
|
||||||
// orderType: direction ? [direction] : [],
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto px-4 py-6">
|
|
||||||
{/* List Mode - Show search, actions, and data display */}
|
|
||||||
{mode === "list" && (
|
|
||||||
<>
|
|
||||||
{/* Search Component */}
|
|
||||||
<SearchComponent
|
|
||||||
onSearch={handleSearch}
|
|
||||||
translations={translations}
|
|
||||||
lang={lang}
|
|
||||||
urlOptions={urlOptions}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Action Buttons Component */}
|
|
||||||
<ActionButtonsComponent
|
|
||||||
onCreateClick={handleCreateClick}
|
|
||||||
translations={translations}
|
|
||||||
lang={lang}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Sorting Component
|
|
||||||
<SortingComponent
|
|
||||||
sortField={sortField}
|
|
||||||
sortDirection={sortDirection}
|
|
||||||
onSort={handleSort}
|
|
||||||
translations={translations}
|
|
||||||
lang={lang}
|
|
||||||
/> */}
|
|
||||||
|
|
||||||
{/* Data Display Component */}
|
|
||||||
<div className="mt-6">
|
|
||||||
<DataDisplayComponent
|
|
||||||
data={data}
|
|
||||||
loading={loading}
|
|
||||||
error={error}
|
|
||||||
onUpdateClick={handleUpdateClick}
|
|
||||||
translations={translations}
|
|
||||||
lang={lang}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Pagination Tools Component */}
|
|
||||||
<div className="mt-4">
|
|
||||||
<PaginationToolsComponent
|
|
||||||
pagination={pagination}
|
|
||||||
updatePagination={updatePagination}
|
|
||||||
loading={loading}
|
|
||||||
lang={lang}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Form Mode - Show create/update/view form */}
|
|
||||||
{mode !== "list" && (
|
|
||||||
<FormComponent
|
|
||||||
initialData={selectedItem || undefined}
|
|
||||||
mode={mode}
|
|
||||||
refetch={refetch}
|
|
||||||
setMode={setMode}
|
|
||||||
setSelectedItem={setSelectedItem}
|
|
||||||
onCancel={() => setMode("list")}
|
|
||||||
lang={lang}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ApplicationPage;
|
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
import { listApplications } from "@/apicalls/application/application";
|
|
||||||
import { PagePagination } from "./hooks";
|
|
||||||
|
|
||||||
// Base schema with all possible fields
|
|
||||||
const ApplicationBaseSchema = z.object({
|
|
||||||
// Identification fields
|
|
||||||
uu_id: z.number().optional(),
|
|
||||||
name: z.string().min(1, "Name is required"),
|
|
||||||
application_code: z.string().min(1, "Application code is required"),
|
|
||||||
|
|
||||||
// Application details
|
|
||||||
site_url: z.string().min(1, "Site URL is required"),
|
|
||||||
application_type: z.string().min(1, "Application type is required"),
|
|
||||||
application_for: z.string().optional(),
|
|
||||||
description: z.string().optional(),
|
|
||||||
|
|
||||||
// Status fields
|
|
||||||
active: z.boolean().default(true),
|
|
||||||
deleted: z.boolean().default(false),
|
|
||||||
|
|
||||||
// System fields
|
|
||||||
created_at: z.string().optional(),
|
|
||||||
updated_at: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Schema for creating a new application
|
|
||||||
export const CreateApplicationSchema = ApplicationBaseSchema.omit({
|
|
||||||
uu_id: true,
|
|
||||||
created_at: true,
|
|
||||||
updated_at: true,
|
|
||||||
deleted: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Schema for updating an existing application
|
|
||||||
export const UpdateApplicationSchema = ApplicationBaseSchema.omit({
|
|
||||||
created_at: true,
|
|
||||||
updated_at: true,
|
|
||||||
deleted: true
|
|
||||||
}).required({
|
|
||||||
uu_id: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Schema for viewing an application (all fields)
|
|
||||||
export const ViewApplicationSchema = ApplicationBaseSchema;
|
|
||||||
|
|
||||||
// Default schema (used for validation)
|
|
||||||
export const ApplicationSchema = ApplicationBaseSchema;
|
|
||||||
|
|
||||||
export type ApplicationFormData = z.infer<typeof ApplicationSchema>;
|
|
||||||
export type CreateApplicationFormData = z.infer<typeof CreateApplicationSchema>;
|
|
||||||
export type UpdateApplicationFormData = z.infer<typeof UpdateApplicationSchema>;
|
|
||||||
export type ViewApplicationFormData = z.infer<typeof ViewApplicationSchema>;
|
|
||||||
|
|
||||||
// Define field definition type
|
|
||||||
export interface FieldDefinition {
|
|
||||||
type: string;
|
|
||||||
group: string;
|
|
||||||
label: string;
|
|
||||||
options?: string[];
|
|
||||||
readOnly?: boolean;
|
|
||||||
required?: boolean;
|
|
||||||
defaultValue?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base field definitions with common properties
|
|
||||||
const baseFieldDefinitions: Record<string, FieldDefinition> = {
|
|
||||||
// Identification fields
|
|
||||||
uu_id: { type: "text", group: "identificationInfo", label: "UUID", readOnly: true, required: false },
|
|
||||||
name: { type: "text", group: "identificationInfo", label: "Name", readOnly: false, required: true },
|
|
||||||
application_code: { type: "text", group: "identificationInfo", label: "Application Code", readOnly: false, required: true },
|
|
||||||
|
|
||||||
// Application details
|
|
||||||
site_url: { type: "text", group: "applicationDetails", label: "Site URL", readOnly: false, required: true },
|
|
||||||
application_type: { type: "select", group: "applicationDetails", label: "Application Type", options: ["info", "Dash", "Admin"], readOnly: false, required: true },
|
|
||||||
application_for: { type: "select", group: "applicationDetails", label: "Application For", options: ["EMP", "OCC"], readOnly: false, required: false },
|
|
||||||
description: { type: "textarea", group: "applicationDetails", label: "Description", readOnly: false, required: false },
|
|
||||||
|
|
||||||
// Status fields
|
|
||||||
active: { type: "checkbox", group: "statusInfo", label: "Active", readOnly: false, required: false, defaultValue: true },
|
|
||||||
deleted: { type: "checkbox", group: "statusInfo", label: "Deleted", readOnly: true, required: false, defaultValue: false },
|
|
||||||
|
|
||||||
// System fields
|
|
||||||
created_at: { type: "date", group: "systemInfo", label: "Created At", readOnly: true, required: false },
|
|
||||||
updated_at: { type: "date", group: "systemInfo", label: "Updated At", readOnly: true, required: false },
|
|
||||||
};
|
|
||||||
|
|
||||||
// Field definitions for create mode
|
|
||||||
export const createFieldDefinitions = {
|
|
||||||
name: { ...baseFieldDefinitions.name, readOnly: false, required: true, defaultValue: "" },
|
|
||||||
application_code: { ...baseFieldDefinitions.application_code, readOnly: false, required: true, defaultValue: "" },
|
|
||||||
site_url: { ...baseFieldDefinitions.site_url, readOnly: false, required: true, defaultValue: "" },
|
|
||||||
application_type: { ...baseFieldDefinitions.application_type, readOnly: false, required: true, defaultValue: "" },
|
|
||||||
application_for: { ...baseFieldDefinitions.application_for, readOnly: false, required: false, defaultValue: "EMP" },
|
|
||||||
description: { ...baseFieldDefinitions.description, readOnly: false, required: false, defaultValue: "" },
|
|
||||||
active: { ...baseFieldDefinitions.active, readOnly: false, required: false, defaultValue: true },
|
|
||||||
};
|
|
||||||
|
|
||||||
// Field definitions for update mode
|
|
||||||
export const updateFieldDefinitions = {
|
|
||||||
uu_id: { ...baseFieldDefinitions.uu_id, readOnly: true, required: false, defaultValue: "" },
|
|
||||||
name: { ...baseFieldDefinitions.name, readOnly: false, required: true, defaultValue: "" },
|
|
||||||
application_code: { ...baseFieldDefinitions.application_code, readOnly: false, required: true, defaultValue: "" },
|
|
||||||
site_url: { ...baseFieldDefinitions.site_url, readOnly: false, required: true, defaultValue: "" },
|
|
||||||
application_type: { ...baseFieldDefinitions.application_type, readOnly: false, required: true, defaultValue: "" },
|
|
||||||
application_for: { ...baseFieldDefinitions.application_for, readOnly: false, required: false, defaultValue: "" },
|
|
||||||
description: { ...baseFieldDefinitions.description, readOnly: false, required: false, defaultValue: "" },
|
|
||||||
active: { ...baseFieldDefinitions.active, readOnly: false, required: false, defaultValue: true },
|
|
||||||
};
|
|
||||||
|
|
||||||
// Field definitions for view mode
|
|
||||||
export const viewFieldDefinitions = {
|
|
||||||
uu_id: { ...baseFieldDefinitions.uu_id, readOnly: true, required: false, defaultValue: 0 },
|
|
||||||
name: { ...baseFieldDefinitions.name, readOnly: true, required: false, defaultValue: "" },
|
|
||||||
application_code: { ...baseFieldDefinitions.application_code, readOnly: true, required: false, defaultValue: "" },
|
|
||||||
site_url: { ...baseFieldDefinitions.site_url, readOnly: true, required: false, defaultValue: "" },
|
|
||||||
application_type: { ...baseFieldDefinitions.application_type, readOnly: true, required: false, defaultValue: "" },
|
|
||||||
application_for: { ...baseFieldDefinitions.application_for, readOnly: true, required: false, defaultValue: "" },
|
|
||||||
description: { ...baseFieldDefinitions.description, readOnly: true, required: false, defaultValue: "" },
|
|
||||||
active: { ...baseFieldDefinitions.active, readOnly: true, required: false, defaultValue: true },
|
|
||||||
deleted: { ...baseFieldDefinitions.deleted, readOnly: true, required: false, defaultValue: false },
|
|
||||||
created_at: { ...baseFieldDefinitions.created_at, readOnly: true, required: false, defaultValue: "" },
|
|
||||||
updated_at: { ...baseFieldDefinitions.updated_at, readOnly: true, required: false, defaultValue: "" },
|
|
||||||
};
|
|
||||||
|
|
||||||
// Combined field definitions for all modes
|
|
||||||
export const fieldDefinitions = {
|
|
||||||
...baseFieldDefinitions,
|
|
||||||
getDefinitionsByMode: (mode: "create" | "update" | "view") => {
|
|
||||||
switch (mode) {
|
|
||||||
case "create":
|
|
||||||
return createFieldDefinitions;
|
|
||||||
case "update":
|
|
||||||
return updateFieldDefinitions;
|
|
||||||
case "view":
|
|
||||||
return viewFieldDefinitions;
|
|
||||||
default:
|
|
||||||
return baseFieldDefinitions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fields to show based on mode - dynamically generated from field definitions
|
|
||||||
export const fieldsByMode = {
|
|
||||||
create: Object.keys(createFieldDefinitions),
|
|
||||||
update: Object.keys(updateFieldDefinitions),
|
|
||||||
view: Object.keys(viewFieldDefinitions),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchApplicationData = async ({
|
|
||||||
page = 1,
|
|
||||||
size = 10,
|
|
||||||
orderFields = ["name"],
|
|
||||||
orderTypes = ["asc"],
|
|
||||||
query = {},
|
|
||||||
}: {
|
|
||||||
page?: number;
|
|
||||||
size?: number;
|
|
||||||
orderFields?: string[];
|
|
||||||
orderTypes?: string[];
|
|
||||||
query?: Record<string, any>;
|
|
||||||
}) => {
|
|
||||||
// Call the actual API function
|
|
||||||
try {
|
|
||||||
const response = await listApplications({
|
|
||||||
page,
|
|
||||||
size,
|
|
||||||
orderField: orderFields,
|
|
||||||
orderType: orderTypes,
|
|
||||||
query,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: response.data,
|
|
||||||
pagination: response.pagination,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching application data:", error);
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
pagination: {
|
|
||||||
page,
|
|
||||||
size,
|
|
||||||
totalCount: 0,
|
|
||||||
totalItems: 0,
|
|
||||||
totalPages: 0,
|
|
||||||
pageCount: 0,
|
|
||||||
orderField: orderFields || [],
|
|
||||||
orderType: orderTypes || [],
|
|
||||||
query: {},
|
|
||||||
} as PagePagination,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { LanguageTranslation } from "@/components/validations/translations/translation";
|
|
||||||
|
|
||||||
export interface ApplicationData {
|
|
||||||
id?: number;
|
|
||||||
name: string;
|
|
||||||
application_code: string;
|
|
||||||
site_url: string;
|
|
||||||
application_type: string;
|
|
||||||
application_for?: string;
|
|
||||||
description?: string;
|
|
||||||
active: boolean;
|
|
||||||
deleted?: boolean;
|
|
||||||
created_at?: string;
|
|
||||||
updated_at?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const translations: Record<string, LanguageTranslation> = {
|
|
||||||
typeSelection: {
|
|
||||||
en: "Type Selection",
|
|
||||||
tr: "Tür Seçimi",
|
|
||||||
},
|
|
||||||
filterSelection: {
|
|
||||||
en: "Filter Selection",
|
|
||||||
tr: "Filtre Seçimi",
|
|
||||||
},
|
|
||||||
employee: {
|
|
||||||
en: "Employee",
|
|
||||||
tr: "Çalışan",
|
|
||||||
},
|
|
||||||
occupant: {
|
|
||||||
en: "Occupant",
|
|
||||||
tr: "Sakin",
|
|
||||||
},
|
|
||||||
search: {
|
|
||||||
en: "Search",
|
|
||||||
tr: "Ara",
|
|
||||||
},
|
|
||||||
siteUrl: {
|
|
||||||
en: "Site URL",
|
|
||||||
tr: "Site URL",
|
|
||||||
},
|
|
||||||
applicationType: {
|
|
||||||
en: "Application Type",
|
|
||||||
tr: "Uygulama Türü",
|
|
||||||
},
|
|
||||||
availableApplications: {
|
|
||||||
en: "Available Applications",
|
|
||||||
tr: "Mevcut Uygulamalar",
|
|
||||||
},
|
|
||||||
code: {
|
|
||||||
en: "Code",
|
|
||||||
tr: "Kod",
|
|
||||||
},
|
|
||||||
url: {
|
|
||||||
en: "URL",
|
|
||||||
tr: "URL",
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
en: "Type",
|
|
||||||
tr: "Tür",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import EventAppendPage from "@/components/Pages/appenderEvent/page";
|
|
||||||
import AppendersServicePage from "./appendersService/page";
|
|
||||||
import ApplicationPage from "./application/page";
|
|
||||||
import EmployeePage from "./employee/page";
|
|
||||||
import OcuppantPage from "./ocuppant/page";
|
|
||||||
import DashboardPage from "./dashboard/page";
|
|
||||||
|
|
||||||
export const menuPages = {
|
|
||||||
"/dashboard": DashboardPage,
|
|
||||||
"/append/event": EventAppendPage,
|
|
||||||
"/append/service": AppendersServicePage,
|
|
||||||
"/application": ApplicationPage,
|
|
||||||
"/employee": EmployeePage,
|
|
||||||
"/ocuppant": OcuppantPage,
|
|
||||||
};
|
|
||||||
@@ -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<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './CreateButton';
|
||||||
|
export * from './CustomButtonComponent';
|
||||||
|
export * from './types';
|
||||||
@@ -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<string, any>;
|
||||||
|
lang: string;
|
||||||
|
customButtons?: CustomButton[];
|
||||||
|
defaultSelectedButtonId?: string;
|
||||||
|
}
|
||||||
@@ -9,8 +9,6 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Eye, Edit } from "lucide-react";
|
import { Eye, Edit } from "lucide-react";
|
||||||
import { CardItemProps, CardActionsProps, CardFieldProps } from "./schema";
|
import { CardItemProps, CardActionsProps, CardFieldProps } from "./schema";
|
||||||
|
|
||||||
// Interface moved to schema.ts
|
|
||||||
|
|
||||||
export function CardItem<T>({
|
export function CardItem<T>({
|
||||||
item,
|
item,
|
||||||
index,
|
index,
|
||||||
@@ -28,7 +26,7 @@ export function CardItem<T>({
|
|||||||
}: 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}
|
||||||
>
|
>
|
||||||
@@ -36,7 +34,7 @@ export function CardItem<T>({
|
|||||||
<h3 className="text-lg font-semibold">
|
<h3 className="text-lg font-semibold">
|
||||||
{getFieldValue(item, titleField)}
|
{getFieldValue(item, titleField)}
|
||||||
</h3>
|
</h3>
|
||||||
<CardActions
|
<CardActions
|
||||||
item={item}
|
item={item}
|
||||||
showViewIcon={showViewIcon}
|
showViewIcon={showViewIcon}
|
||||||
showUpdateIcon={showUpdateIcon}
|
showUpdateIcon={showUpdateIcon}
|
||||||
@@ -78,10 +76,10 @@ function CardActions<T>({
|
|||||||
return (
|
return (
|
||||||
<div className="flex space-x-1">
|
<div className="flex space-x-1">
|
||||||
{showViewIcon && (
|
{showViewIcon && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (onViewClick) onViewClick(item);
|
if (onViewClick) onViewClick(item);
|
||||||
@@ -91,10 +89,10 @@ function CardActions<T>({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{showUpdateIcon && (
|
{showUpdateIcon && (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (onUpdateClick) onUpdateClick(item);
|
if (onUpdateClick) onUpdateClick(item);
|
||||||
@@ -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<T>({
|
||||||
|
refetch,
|
||||||
|
setMode,
|
||||||
|
setSelectedItem,
|
||||||
|
onCancel,
|
||||||
|
lang,
|
||||||
|
translations,
|
||||||
|
formProps = {},
|
||||||
|
}: 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,
|
||||||
|
} = useForm({
|
||||||
|
defaultValues,
|
||||||
|
resolver: validationSchema ? zodResolver(validationSchema) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const formValues = watch();
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
const onSubmit = async (data: Record<string, any>) => {
|
||||||
|
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 (
|
||||||
|
<div className="space-y-2" key={fieldName}>
|
||||||
|
<Label htmlFor={fieldName}>
|
||||||
|
{t[fieldName] || field.label}
|
||||||
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={fieldName}
|
||||||
|
{...register(fieldName)}
|
||||||
|
placeholder={t[fieldName] || field.label}
|
||||||
|
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}
|
||||||
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id={fieldName}
|
||||||
|
{...register(fieldName)}
|
||||||
|
placeholder={t[fieldName] || field.label}
|
||||||
|
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}
|
||||||
|
{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} />
|
||||||
|
</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}
|
||||||
|
{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(() => console.log("Form data to save:"))}>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-2 pt-6">
|
||||||
|
<Button type="button" variant="outline" onClick={onCancel}>
|
||||||
|
{t.cancel || "Cancel"}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">
|
||||||
|
{t.save || "Save"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
"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 = {},
|
||||||
|
}: FormDisplayProps<T>) {
|
||||||
|
const [enhancedFormProps, setEnhancedFormProps] = useState(formProps);
|
||||||
|
|
||||||
|
// Dynamically import schema definitions if provided in formProps
|
||||||
|
useEffect(() => {
|
||||||
|
const loadSchemaDefinitions = 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
|
||||||
|
let validationSchema;
|
||||||
|
if (mode === "create" && schemaModule.CreateApplicationSchema) {
|
||||||
|
validationSchema = schemaModule.CreateApplicationSchema;
|
||||||
|
} else if (mode === "update" && schemaModule.UpdateApplicationSchema) {
|
||||||
|
validationSchema = schemaModule.UpdateApplicationSchema;
|
||||||
|
} 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
|
||||||
|
setEnhancedFormProps({
|
||||||
|
...formProps,
|
||||||
|
fieldDefinitions: fieldDefs || {},
|
||||||
|
validationSchema,
|
||||||
|
fieldsByMode: schemaModule.fieldsByMode || {},
|
||||||
|
groupedFieldDefinitions: groupedFieldDefs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading schema definitions:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadSchemaDefinitions();
|
||||||
|
}, [formProps, mode]);
|
||||||
|
|
||||||
|
// Render the appropriate component based on the mode
|
||||||
|
switch (mode) {
|
||||||
|
case "create":
|
||||||
|
return (
|
||||||
|
<CreateComponent<T>
|
||||||
|
refetch={refetch}
|
||||||
|
setMode={setMode}
|
||||||
|
setSelectedItem={setSelectedItem}
|
||||||
|
onCancel={onCancel}
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
formProps={enhancedFormProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "update":
|
||||||
|
return initialData ? (
|
||||||
|
<UpdateComponent<T>
|
||||||
|
initialData={initialData}
|
||||||
|
refetch={refetch}
|
||||||
|
setMode={setMode}
|
||||||
|
setSelectedItem={setSelectedItem}
|
||||||
|
onCancel={onCancel}
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
formProps={enhancedFormProps}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
case "view":
|
||||||
|
return initialData ? (
|
||||||
|
<ViewComponent<T>
|
||||||
|
initialData={initialData}
|
||||||
|
refetch={refetch}
|
||||||
|
setMode={setMode}
|
||||||
|
setSelectedItem={setSelectedItem}
|
||||||
|
onCancel={onCancel}
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
formProps={formProps}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
"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 { UpdateComponentProps } 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; // Add name property for TypeScript compatibility
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UpdateComponent<T>({
|
||||||
|
initialData,
|
||||||
|
refetch,
|
||||||
|
setMode,
|
||||||
|
setSelectedItem,
|
||||||
|
onCancel,
|
||||||
|
lang,
|
||||||
|
translations,
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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 and initialData
|
||||||
|
const defaultValues: Record<string, any> = {};
|
||||||
|
Object.entries(fieldDefinitions).forEach(([key, def]) => {
|
||||||
|
const fieldDef = def as FieldDefinition;
|
||||||
|
defaultValues[key] = fieldDef.defaultValue !== undefined ? fieldDef.defaultValue : "";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merge initialData with default values
|
||||||
|
if (initialData) {
|
||||||
|
Object.assign(defaultValues, initialData as Record<string, any>);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup form with validation schema if available
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
setValue,
|
||||||
|
watch,
|
||||||
|
reset,
|
||||||
|
} = useForm({
|
||||||
|
defaultValues,
|
||||||
|
resolver: validationSchema ? zodResolver(validationSchema) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset form when initialData changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialData) {
|
||||||
|
reset({ ...initialData as Record<string, any> });
|
||||||
|
}
|
||||||
|
}, [initialData, reset]);
|
||||||
|
|
||||||
|
const formValues = watch();
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
const onSubmit = async (data: any) => {
|
||||||
|
try {
|
||||||
|
console.log("Form data to update:", data);
|
||||||
|
|
||||||
|
// Here you would make an API call to update the data
|
||||||
|
// For example: await updateApplication(data);
|
||||||
|
|
||||||
|
// Mock API call success
|
||||||
|
if (refetch) refetch();
|
||||||
|
setMode("list");
|
||||||
|
setSelectedItem(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating 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 (
|
||||||
|
<div className="space-y-2" key={fieldName}>
|
||||||
|
<Label htmlFor={fieldName}>
|
||||||
|
{t[fieldName] || field.label}
|
||||||
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={fieldName}
|
||||||
|
{...register(fieldName)}
|
||||||
|
placeholder={t[fieldName] || field.label}
|
||||||
|
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}
|
||||||
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id={fieldName}
|
||||||
|
{...register(fieldName)}
|
||||||
|
placeholder={t[fieldName] || field.label}
|
||||||
|
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}
|
||||||
|
{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} />
|
||||||
|
</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}
|
||||||
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
|
</Label>
|
||||||
|
{errorMessage && (
|
||||||
|
<p className="text-sm text-red-500">{errorMessage}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
return (
|
||||||
|
<div className="space-y-2" key={fieldName}>
|
||||||
|
<Label htmlFor={fieldName}>
|
||||||
|
{t[fieldName] || field.label}
|
||||||
|
{field.required && <span className="text-red-500 ml-1">*</span>}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id={fieldName}
|
||||||
|
type="date"
|
||||||
|
{...register(fieldName)}
|
||||||
|
disabled={field.readOnly}
|
||||||
|
/>
|
||||||
|
{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"}
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-2 pt-6">
|
||||||
|
<Button type="button" variant="outline" onClick={onCancel}>
|
||||||
|
{t.cancel || "Cancel"}
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">
|
||||||
|
{t.save || "Save"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
"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 } from "./types";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// Import field definitions type
|
||||||
|
export interface FieldDefinition {
|
||||||
|
type: string;
|
||||||
|
group: string;
|
||||||
|
label: string;
|
||||||
|
options?: string[];
|
||||||
|
readOnly?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
defaultValue?: any;
|
||||||
|
name?: string; // Add name property for TypeScript compatibility
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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={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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// Export the main components
|
||||||
|
export { FormDisplay } from "./FormDisplay";
|
||||||
|
|
||||||
|
// Export types
|
||||||
|
export type { FormMode } from "./types";
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// 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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateComponentProps<T> extends BaseFormProps<T> {}
|
||||||
|
|
||||||
|
export interface UpdateComponentProps<T> extends BaseFormProps<T> {
|
||||||
|
initialData: T; // Required for update
|
||||||
|
}
|
||||||
|
|
||||||
|
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>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export type Language = "en" | "tr";
|
||||||
|
|
||||||
|
interface LanguageSelectionComponentProps {
|
||||||
|
lang: Language;
|
||||||
|
setLang: (lang: Language) => void;
|
||||||
|
translations?: Record<string, any>;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
"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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
"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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export * from './PaginationToolsComponent';
|
||||||
|
export * from './PaginationStats';
|
||||||
|
export * from './PageNavigation';
|
||||||
|
export * from './PageSizeSelector';
|
||||||
|
export * from './types';
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
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>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './types';
|
||||||
|
export * from './TextQueryModifier';
|
||||||
|
export * from './SelectQueryModifier';
|
||||||
|
export * from './TypeQueryModifier';
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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>;
|
||||||
128
WebServices/management-frontend/src/components/common/ReadMe.md
Normal file
128
WebServices/management-frontend/src/components/common/ReadMe.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
@@ -2,15 +2,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ArrowDown, ArrowUp } from "lucide-react";
|
import { ArrowDown, ArrowUp } from "lucide-react";
|
||||||
import { LanguageTranslation } from "@/components/validations/translations/translation";
|
import { SortingComponentProps, SortField } from "./types";
|
||||||
|
|
||||||
interface SortingComponentProps {
|
|
||||||
sortField: string | null;
|
|
||||||
sortDirection: "asc" | "desc" | null;
|
|
||||||
onSort: (field: string) => void;
|
|
||||||
translations: Record<string, LanguageTranslation>;
|
|
||||||
lang: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SortingComponent: React.FC<SortingComponentProps> = ({
|
export const SortingComponent: React.FC<SortingComponentProps> = ({
|
||||||
sortField,
|
sortField,
|
||||||
@@ -18,19 +10,19 @@ export const SortingComponent: React.FC<SortingComponentProps> = ({
|
|||||||
onSort,
|
onSort,
|
||||||
translations,
|
translations,
|
||||||
lang,
|
lang,
|
||||||
}) => {
|
sortFields = [
|
||||||
// Available sort fields
|
|
||||||
const sortFields = [
|
|
||||||
{ key: "name", label: "Name" },
|
{ key: "name", label: "Name" },
|
||||||
{ key: "application_code", label: "Code" },
|
{ key: "code", label: "Code" },
|
||||||
{ key: "application_type", label: "Type" },
|
{ key: "type", label: "Type" },
|
||||||
{ key: "created_at", label: "Created" },
|
{ key: "created_at", label: "Created" },
|
||||||
];
|
],
|
||||||
|
}) => {
|
||||||
|
const t = translations?.[lang] || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-2 my-4">
|
<div className="flex flex-wrap gap-2 my-4">
|
||||||
<div className="text-sm font-medium mr-2 flex items-center">
|
<div className="text-sm font-medium mr-2 flex items-center">
|
||||||
Sort by:
|
{t.sortBy || "Sort by:"}
|
||||||
</div>
|
</div>
|
||||||
{sortFields.map((field) => (
|
{sortFields.map((field) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -40,7 +32,7 @@ export const SortingComponent: React.FC<SortingComponentProps> = ({
|
|||||||
onClick={() => onSort(field.key)}
|
onClick={() => onSort(field.key)}
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
>
|
>
|
||||||
{field.label}
|
{t[field.key] || field.label}
|
||||||
{sortField === field.key && (
|
{sortField === field.key && (
|
||||||
<>
|
<>
|
||||||
{sortDirection === "asc" ? (
|
{sortDirection === "asc" ? (
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './SortingComponent';
|
||||||
|
export * from './types';
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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[];
|
||||||
|
}
|
||||||
@@ -13,34 +13,24 @@ export function useApiData<T>(
|
|||||||
// Define the fetch function that will be passed to useDataFetching
|
// Define the fetch function that will be passed to useDataFetching
|
||||||
const fetchFromApi = async (params: RequestParams): Promise<ApiResponse<T>> => {
|
const fetchFromApi = async (params: RequestParams): Promise<ApiResponse<T>> => {
|
||||||
try {
|
try {
|
||||||
// Construct query parameters
|
// Prepare the request body with action and all params
|
||||||
const queryParams = new URLSearchParams();
|
const requestBody = {
|
||||||
|
action: 'list',
|
||||||
|
page: params.page,
|
||||||
|
size: params.size,
|
||||||
|
orderField: params.orderField,
|
||||||
|
orderType: params.orderType,
|
||||||
|
query: params.query
|
||||||
|
};
|
||||||
|
|
||||||
// Add pagination parameters
|
// Make the API request using POST
|
||||||
queryParams.append("page", params.page.toString());
|
const response = await fetch(endpoint, {
|
||||||
queryParams.append("size", params.size.toString());
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
// Add sorting parameters
|
'Content-Type': 'application/json',
|
||||||
if (params.orderField && params.orderField.length > 0) {
|
},
|
||||||
params.orderField.forEach((field, index) => {
|
body: JSON.stringify(requestBody),
|
||||||
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) {
|
if (!response.ok) {
|
||||||
throw new Error(`API request failed with status ${response.status}`);
|
throw new Error(`API request failed with status ${response.status}`);
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// 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";
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
// 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>;
|
||||||
|
}
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Plus } from "lucide-react";
|
|
||||||
|
|
||||||
interface ActionButtonsComponentProps {
|
|
||||||
onCreateClick: () => void;
|
|
||||||
translations: Record<string, any>;
|
|
||||||
lang: string;
|
|
||||||
customButtons?: {
|
|
||||||
label: string;
|
|
||||||
onClick: () => void;
|
|
||||||
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link";
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ActionButtonsComponent: React.FC<ActionButtonsComponentProps> = ({
|
|
||||||
onCreateClick,
|
|
||||||
translations,
|
|
||||||
lang,
|
|
||||||
customButtons = [],
|
|
||||||
}) => {
|
|
||||||
const t = translations[lang] || {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex justify-between items-center my-4">
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button onClick={onCreateClick} className="flex items-center">
|
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
|
||||||
{t.create || "Create"}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Render custom buttons */}
|
|
||||||
{customButtons.map((button, index) => (
|
|
||||||
<Button
|
|
||||||
key={index}
|
|
||||||
onClick={button.onClick}
|
|
||||||
variant={button.variant || "default"}
|
|
||||||
className="flex items-center"
|
|
||||||
>
|
|
||||||
{button.icon && <span className="mr-2">{button.icon}</span>}
|
|
||||||
{button.label}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
interface FormDisplayProps<T> {
|
|
||||||
initialData?: T;
|
|
||||||
mode: "list" | "create" | "update";
|
|
||||||
refetch?: () => void;
|
|
||||||
setMode: React.Dispatch<React.SetStateAction<"list" | "create" | "update">>;
|
|
||||||
setSelectedItem: React.Dispatch<React.SetStateAction<T | null>>;
|
|
||||||
onCancel: () => void;
|
|
||||||
lang: string;
|
|
||||||
FormComponent: React.FC<any>;
|
|
||||||
formProps?: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FormDisplay<T>({
|
|
||||||
initialData,
|
|
||||||
mode,
|
|
||||||
refetch,
|
|
||||||
setMode,
|
|
||||||
setSelectedItem,
|
|
||||||
onCancel,
|
|
||||||
lang,
|
|
||||||
FormComponent,
|
|
||||||
formProps = {},
|
|
||||||
}: FormDisplayProps<T>) {
|
|
||||||
return (
|
|
||||||
<FormComponent
|
|
||||||
initialData={initialData}
|
|
||||||
mode={mode}
|
|
||||||
refetch={refetch}
|
|
||||||
setMode={setMode}
|
|
||||||
setSelectedItem={setSelectedItem}
|
|
||||||
onCancel={onCancel}
|
|
||||||
lang={lang}
|
|
||||||
{...formProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { PagePagination } from "./hooks/useDataFetching";
|
|
||||||
|
|
||||||
interface PaginationToolsComponentProps {
|
|
||||||
pagination: PagePagination;
|
|
||||||
updatePagination: (updates: Partial<PagePagination>) => void;
|
|
||||||
loading: boolean;
|
|
||||||
lang: string;
|
|
||||||
translations: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PaginationToolsComponent: React.FC<PaginationToolsComponentProps> = ({
|
|
||||||
pagination,
|
|
||||||
updatePagination,
|
|
||||||
loading,
|
|
||||||
lang,
|
|
||||||
translations
|
|
||||||
}) => {
|
|
||||||
const t = translations[lang] || {};
|
|
||||||
|
|
||||||
const handlePageChange = (newPage: number) => {
|
|
||||||
if (newPage >= 1 && newPage <= pagination.totalPages) {
|
|
||||||
updatePagination({ page: newPage });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-wrap justify-between items-center mt-6 gap-4">
|
|
||||||
{/* Pagination stats - left side */}
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
<div>
|
|
||||||
{t.showing || "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>
|
|
||||||
|
|
||||||
{/* Navigation buttons - center */}
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
{
|
|
||||||
pagination.back ? (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handlePageChange(pagination.page - 1)}
|
|
||||||
>
|
|
||||||
{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">
|
|
||||||
{Array.from(
|
|
||||||
{
|
|
||||||
length: Math.min(
|
|
||||||
5,
|
|
||||||
Math.max(
|
|
||||||
1,
|
|
||||||
Math.ceil(
|
|
||||||
(pagination.totalCount &&
|
|
||||||
pagination.totalCount !== pagination.allCount
|
|
||||||
? pagination.totalCount
|
|
||||||
: pagination.allCount || 0) / pagination.size
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
},
|
|
||||||
(_, i) => {
|
|
||||||
// Show pages around current page
|
|
||||||
let pageNum;
|
|
||||||
const calculatedTotalPages = Math.max(
|
|
||||||
1,
|
|
||||||
Math.ceil(
|
|
||||||
(pagination.totalCount &&
|
|
||||||
pagination.totalCount !== pagination.allCount
|
|
||||||
? pagination.totalCount
|
|
||||||
: pagination.allCount || 0) / pagination.size
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (calculatedTotalPages <= 5) {
|
|
||||||
pageNum = i + 1;
|
|
||||||
} else if (pagination.page <= 3) {
|
|
||||||
pageNum = i + 1;
|
|
||||||
} else if (pagination.page >= calculatedTotalPages - 2) {
|
|
||||||
pageNum = calculatedTotalPages - 4 + i;
|
|
||||||
} else {
|
|
||||||
pageNum = pagination.page - 2 + i;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={pageNum}
|
|
||||||
variant={pagination.page === pageNum ? "default" : "outline"}
|
|
||||||
size="sm"
|
|
||||||
className="w-9 h-9 p-0"
|
|
||||||
onClick={() => handlePageChange(pageNum)}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{pageNum}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
pagination.page < pagination.totalPages ? (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handlePageChange(pagination.page + 1)}
|
|
||||||
>
|
|
||||||
{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"}{" "}
|
|
||||||
{Math.max(
|
|
||||||
1,
|
|
||||||
Math.ceil(
|
|
||||||
(pagination.totalCount &&
|
|
||||||
pagination.totalCount !== pagination.allCount
|
|
||||||
? pagination.totalCount
|
|
||||||
: pagination.allCount || 0) / pagination.size
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Items per page selector - right side */}
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<span className="text-sm text-muted-foreground">{t.itemsPerPage || "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>
|
|
||||||
<SelectItem value="5">5</SelectItem>
|
|
||||||
<SelectItem value="10">10</SelectItem>
|
|
||||||
<SelectItem value="20">20</SelectItem>
|
|
||||||
<SelectItem value="50">50</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React, { useState } from "react";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { User, Building, Filter, Search, Link } from "lucide-react";
|
|
||||||
|
|
||||||
interface SearchComponentProps {
|
|
||||||
onSearch: (query: Record<string, string>) => void;
|
|
||||||
translations: Record<string, any>;
|
|
||||||
lang: string;
|
|
||||||
typeOptions?: {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
}[];
|
|
||||||
urlOptions?: string[];
|
|
||||||
additionalFields?: {
|
|
||||||
name: string;
|
|
||||||
label: string;
|
|
||||||
type: "text" | "select";
|
|
||||||
options?: { value: string; label: string }[];
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SearchComponent: React.FC<SearchComponentProps> = ({
|
|
||||||
onSearch,
|
|
||||||
translations,
|
|
||||||
lang,
|
|
||||||
typeOptions = [],
|
|
||||||
urlOptions = [],
|
|
||||||
additionalFields = [],
|
|
||||||
}) => {
|
|
||||||
const [selectedType, setSelectedType] = useState<string>(typeOptions.length > 0 ? typeOptions[0].value : "");
|
|
||||||
const [selectedUrl, setSelectedUrl] = useState<string>("");
|
|
||||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
|
||||||
const [additionalValues, setAdditionalValues] = useState<Record<string, string>>({});
|
|
||||||
|
|
||||||
// Handle selection button click
|
|
||||||
const handleTypeSelect = (type: string) => {
|
|
||||||
setSelectedType(type);
|
|
||||||
|
|
||||||
// Include type in search query
|
|
||||||
handleSearch(searchQuery, selectedUrl, type, additionalValues);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle search with all parameters
|
|
||||||
const handleSearch = (
|
|
||||||
query: string,
|
|
||||||
url: string,
|
|
||||||
type: string,
|
|
||||||
additionalValues: Record<string, string>
|
|
||||||
) => {
|
|
||||||
const searchParams: Record<string, string> = {};
|
|
||||||
|
|
||||||
if (url) {
|
|
||||||
searchParams.site_url = url;
|
|
||||||
}
|
|
||||||
if (query) {
|
|
||||||
searchParams.name = query;
|
|
||||||
}
|
|
||||||
if (type) {
|
|
||||||
searchParams.type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add additional field values
|
|
||||||
Object.entries(additionalValues).forEach(([key, value]) => {
|
|
||||||
if (value) {
|
|
||||||
searchParams[key] = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Call onSearch with the search parameters
|
|
||||||
onSearch(searchParams);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAdditionalFieldChange = (fieldName: string, value: string) => {
|
|
||||||
setAdditionalValues((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[fieldName]: value,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const t = translations[lang] || {};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardContent className="pt-6">
|
|
||||||
<div className="flex flex-col md:flex-row">
|
|
||||||
{/* Type selection - vertical on the left (w-1/2) */}
|
|
||||||
{typeOptions.length > 0 && (
|
|
||||||
<div className="w-full md:w-1/2 flex flex-col space-y-4 md:pr-4 mb-4 md:mb-0">
|
|
||||||
<div className="font-medium text-sm mb-2 flex items-center">
|
|
||||||
<User className="mr-2 h-4 w-4" />
|
|
||||||
{t.typeSelection || "Type Selection"}
|
|
||||||
</div>
|
|
||||||
{typeOptions.map((option) => (
|
|
||||||
<Button
|
|
||||||
key={option.value}
|
|
||||||
variant={selectedType === option.value ? "default" : "outline"}
|
|
||||||
size="lg"
|
|
||||||
onClick={() => handleTypeSelect(option.value)}
|
|
||||||
className="w-full h-14 mb-2"
|
|
||||||
>
|
|
||||||
{option.icon || <User className="mr-2 h-4 w-4" />}
|
|
||||||
{option.label}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Filters on the right (w-1/2) */}
|
|
||||||
<div className={`w-full ${typeOptions.length > 0 ? 'md:w-1/2 md:pl-4' : ''} flex flex-col space-y-4`}>
|
|
||||||
<div className="font-medium text-sm mb-2 flex items-center">
|
|
||||||
<Filter className="mr-2 h-4 w-4" />
|
|
||||||
{t.filterSelection || "Filter Selection"}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Search input */}
|
|
||||||
<div className="w-full">
|
|
||||||
<label className="block text-xs font-medium mb-1">
|
|
||||||
{t.search || "Search"}
|
|
||||||
</label>
|
|
||||||
<div className="relative w-full flex">
|
|
||||||
<Input
|
|
||||||
placeholder={`${t.search || "Search"}...`}
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
onKeyUp={(e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
handleSearch(searchQuery, selectedUrl, selectedType, additionalValues);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="pl-8 w-full h-10"
|
|
||||||
/>
|
|
||||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
className="ml-2"
|
|
||||||
onClick={() => {
|
|
||||||
handleSearch(searchQuery, selectedUrl, selectedType, additionalValues);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Search className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Site URL dropdown */}
|
|
||||||
{urlOptions.length > 0 && (
|
|
||||||
<div className="w-full">
|
|
||||||
<label className="block text-xs font-medium mb-1">
|
|
||||||
{t.siteUrl || "Site URL"}
|
|
||||||
</label>
|
|
||||||
<div className="w-full">
|
|
||||||
<Select
|
|
||||||
value={selectedUrl}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
setSelectedUrl(value);
|
|
||||||
handleSearch(searchQuery, value, selectedType, additionalValues);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full h-10">
|
|
||||||
<SelectValue
|
|
||||||
placeholder={`${t.siteUrl || "Site URL"}...`}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{urlOptions.map((url) => (
|
|
||||||
<SelectItem key={url} value={url}>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Link className="mr-2 h-3 w-3" />
|
|
||||||
{url}
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Additional fields */}
|
|
||||||
{additionalFields.map((field) => (
|
|
||||||
<div key={field.name} className="w-full">
|
|
||||||
<label className="block text-xs font-medium mb-1">
|
|
||||||
{field.label}
|
|
||||||
</label>
|
|
||||||
{field.type === "text" ? (
|
|
||||||
<Input
|
|
||||||
value={additionalValues[field.name] || ""}
|
|
||||||
onChange={(e) => handleAdditionalFieldChange(field.name, e.target.value)}
|
|
||||||
className="w-full h-10"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Select
|
|
||||||
value={additionalValues[field.name] || ""}
|
|
||||||
onValueChange={(value) => handleAdditionalFieldChange(field.name, value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full h-10">
|
|
||||||
<SelectValue placeholder={field.label} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{field.options?.map((option) => (
|
|
||||||
<SelectItem key={option.value} value={option.value}>
|
|
||||||
{option.label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
// 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';
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
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<string, any>;
|
|
||||||
}) => {
|
|
||||||
// Call the actual API function
|
|
||||||
try {
|
|
||||||
const response = await listApplications({
|
|
||||||
page,
|
|
||||||
size,
|
|
||||||
orderField: orderFields,
|
|
||||||
orderType: orderTypes,
|
|
||||||
query,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: response.data,
|
|
||||||
pagination: response.pagination,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching application data:", error);
|
|
||||||
return {
|
|
||||||
data: [],
|
|
||||||
pagination: {
|
|
||||||
page,
|
|
||||||
size,
|
|
||||||
totalCount: 0,
|
|
||||||
totalItems: 0,
|
|
||||||
totalPages: 0,
|
|
||||||
pageCount: 0,
|
|
||||||
orderField: orderFields || [],
|
|
||||||
orderType: orderTypes || [],
|
|
||||||
query: {},
|
|
||||||
} as PagePagination,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
<ImportableComponents(Like Search, Select, Sort...)>
|
|
||||||
|
|
||||||
<CardDisplay
|
|
||||||
showFields={showFields}
|
|
||||||
data={data}
|
|
||||||
lang={lang}
|
|
||||||
translations={translations}
|
|
||||||
pagination={pagination}
|
|
||||||
updatePagination={updatePagination}
|
|
||||||
error={error}
|
|
||||||
loading={loading}
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
@@ -8,7 +8,7 @@ import ProfileLoadingState from "./ProfileLoadingState";
|
|||||||
import {
|
import {
|
||||||
ClientMenuProps,
|
ClientMenuProps,
|
||||||
UserSelection,
|
UserSelection,
|
||||||
} from "@/components/validations/menu/menu";
|
} from "@/validations/menu/menu";
|
||||||
import NavigationMenu from "./NavigationMenu";
|
import NavigationMenu from "./NavigationMenu";
|
||||||
|
|
||||||
// Language definitions for dashboard title
|
// Language definitions for dashboard title
|
||||||
|
|||||||
66
WebServices/management-frontend/src/components/ui/alert.tsx
Normal file
66
WebServices/management-frontend/src/components/ui/alert.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-card text-card-foreground",
|
||||||
|
destructive:
|
||||||
|
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Alert({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert"
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-title"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-description"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription }
|
||||||
46
WebServices/management-frontend/src/components/ui/badge.tsx
Normal file
46
WebServices/management-frontend/src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Badge({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span"> &
|
||||||
|
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||||
|
const Comp = asChild ? Slot : "span"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="badge"
|
||||||
|
className={cn(badgeVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PageProps } from "@/components/validations/translations/translation";
|
import { PageProps } from "@/validations/translations/translation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
const EventAppendPage: React.FC<PageProps> = () => {
|
const EventAppendPage: React.FC<PageProps> = () => {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { PageProps } from "@/components/validations/translations/translation";
|
import { PageProps } from "@/validations/translations/translation";
|
||||||
|
|
||||||
const AppendersServicePage: React.FC<PageProps> = () => {
|
const AppendersServicePage: React.FC<PageProps> = () => {
|
||||||
return <div>AppendersServicePage</div>;
|
return <div>AppendersServicePage</div>;
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
export type LanguageKey = "en" | "tr";
|
||||||
|
|
||||||
|
export interface TranslationSet {
|
||||||
|
showing: string;
|
||||||
|
of: string;
|
||||||
|
items: string;
|
||||||
|
total: string;
|
||||||
|
filtered: string;
|
||||||
|
page: string;
|
||||||
|
previous: string;
|
||||||
|
next: string;
|
||||||
|
itemsPerPage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fieldLanguageTranslation = {
|
||||||
|
tr: {
|
||||||
|
uu_id: "UUID",
|
||||||
|
name: "Ad",
|
||||||
|
application_code: "Kod",
|
||||||
|
site_url: "URL",
|
||||||
|
application_type: "Tür",
|
||||||
|
application_for: "Uygulama için",
|
||||||
|
description: "Açıklama",
|
||||||
|
active: "Aktif",
|
||||||
|
deleted: "Silindi",
|
||||||
|
created_at: "Oluşturulma Tarihi",
|
||||||
|
updated_at: "Güncellenme Tarihi",
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
uu_id: "UUID",
|
||||||
|
name: "Name",
|
||||||
|
application_code: "Code",
|
||||||
|
site_url: "URL",
|
||||||
|
application_type: "Type",
|
||||||
|
application_for: "Application for",
|
||||||
|
description: "Description",
|
||||||
|
active: "Active",
|
||||||
|
deleted: "Deleted",
|
||||||
|
created_at: "Created At",
|
||||||
|
updated_at: "Updated At",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define translations as a flat object structure to match the common components expectations
|
||||||
|
export const translations = {
|
||||||
|
en: {
|
||||||
|
...fieldLanguageTranslation.en,
|
||||||
|
// Page title
|
||||||
|
applicationTitle: "Applications",
|
||||||
|
|
||||||
|
// Common actions
|
||||||
|
create: "Create",
|
||||||
|
update: "Update",
|
||||||
|
delete: "Delete",
|
||||||
|
view: "View",
|
||||||
|
|
||||||
|
// Search and filters
|
||||||
|
search: "Search",
|
||||||
|
typeSelection: "Type Selection",
|
||||||
|
filterSelection: "Filter Selection",
|
||||||
|
siteUrl: "Site URL",
|
||||||
|
resetAll: "Reset All",
|
||||||
|
|
||||||
|
// Type options
|
||||||
|
web: "Web",
|
||||||
|
mobile: "Mobile",
|
||||||
|
|
||||||
|
// Status options
|
||||||
|
status: "Status",
|
||||||
|
active: "Active",
|
||||||
|
inactive: "Inactive",
|
||||||
|
pending: "Pending",
|
||||||
|
|
||||||
|
// User types
|
||||||
|
employee: "Employee",
|
||||||
|
occupant: "Occupant",
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
showing: "Showing",
|
||||||
|
of: "of",
|
||||||
|
items: "items",
|
||||||
|
total: "Total",
|
||||||
|
filtered: "Filtered",
|
||||||
|
previous: "Previous",
|
||||||
|
next: "Next",
|
||||||
|
page: "Page",
|
||||||
|
itemsPerPage: "Items per page",
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
noData: "No data found",
|
||||||
|
|
||||||
|
// Field labels
|
||||||
|
id: "ID",
|
||||||
|
name: "Name",
|
||||||
|
description: "Description",
|
||||||
|
createdAt: "Created At",
|
||||||
|
type: "Type",
|
||||||
|
application_code: "Code",
|
||||||
|
site_url: "URL",
|
||||||
|
application_type: "Type",
|
||||||
|
|
||||||
|
// Other
|
||||||
|
applicationType: "Application Type",
|
||||||
|
availableApplications: "Available Applications",
|
||||||
|
code: "Code",
|
||||||
|
sortBy: "Sort by:",
|
||||||
|
},
|
||||||
|
tr: {
|
||||||
|
// Page title
|
||||||
|
...fieldLanguageTranslation.tr,
|
||||||
|
applicationTitle: "Uygulamalar",
|
||||||
|
|
||||||
|
// Common actions
|
||||||
|
create: "Oluştur",
|
||||||
|
update: "Güncelle",
|
||||||
|
delete: "Sil",
|
||||||
|
view: "Görüntüle",
|
||||||
|
|
||||||
|
// Search and filters
|
||||||
|
search: "Ara",
|
||||||
|
typeSelection: "Tür Seçimi",
|
||||||
|
filterSelection: "Filtre Seçimi",
|
||||||
|
siteUrl: "Site URL",
|
||||||
|
resetAll: "Tümünü Sıfırla",
|
||||||
|
|
||||||
|
// Type options
|
||||||
|
web: "Web",
|
||||||
|
mobile: "Mobil",
|
||||||
|
|
||||||
|
// Status options
|
||||||
|
status: "Durum",
|
||||||
|
active: "Aktif",
|
||||||
|
inactive: "Pasif",
|
||||||
|
pending: "Beklemede",
|
||||||
|
|
||||||
|
// User types
|
||||||
|
employee: "Çalışan",
|
||||||
|
occupant: "Sakin",
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
showing: "Gösteriliyor",
|
||||||
|
of: "of",
|
||||||
|
items: "öğeler",
|
||||||
|
total: "Toplam",
|
||||||
|
filtered: "Filtreli",
|
||||||
|
previous: "Önceki",
|
||||||
|
next: "Sonraki",
|
||||||
|
page: "Sayfa",
|
||||||
|
itemsPerPage: "Sayfa başına öğeler",
|
||||||
|
|
||||||
|
// Messages
|
||||||
|
noData: "Veri bulunamadı",
|
||||||
|
|
||||||
|
// Field labels
|
||||||
|
id: "ID",
|
||||||
|
name: "Ad",
|
||||||
|
description: "Açıklama",
|
||||||
|
createdAt: "Oluşturulma Tarihi",
|
||||||
|
type: "Tür",
|
||||||
|
application_code: "Kod",
|
||||||
|
site_url: "URL",
|
||||||
|
application_type: "Tür",
|
||||||
|
|
||||||
|
// Other
|
||||||
|
applicationType: "Uygulama Türü",
|
||||||
|
availableApplications: "Mevcut Uygulamalar",
|
||||||
|
code: "Kod",
|
||||||
|
sortBy: "Sırala:",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getTranslation(lang: LanguageKey): TranslationSet {
|
||||||
|
return translations[lang];
|
||||||
|
}
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
"use client";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { PageProps } from "@/validations/translations/translation";
|
||||||
|
import { useApiData } from "@/components/common/hooks/useApiData";
|
||||||
|
import { translations } from "./language";
|
||||||
|
import * as schema from "./schema";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Building, Filter, User } from "lucide-react";
|
||||||
|
import { TextQueryModifier, SelectQueryModifier, TypeQueryModifier } from "@/components/common/QueryModifiers";
|
||||||
|
import { CreateButton } from "@/components/common/ActionButtonsDisplay/CreateButton";
|
||||||
|
import { PaginationToolsComponent } from "@/components/common/PaginationModifiers/PaginationToolsComponent";
|
||||||
|
import { CardDisplay } from "@/components/common/CardDisplay";
|
||||||
|
import { FormDisplay } from "@/components/common/FormDisplay/FormDisplay";
|
||||||
|
import { GridSelectionComponent, GridSize } from "@/components/common/HeaderSelections/GridSelectionComponent";
|
||||||
|
import { LanguageSelectionComponent, Language } from "@/components/common/HeaderSelections/LanguageSelectionComponent";
|
||||||
|
import type { FormMode } from "@/components/common/FormDisplay/types";
|
||||||
|
|
||||||
|
const ApplicationPage: React.FC<PageProps> = ({ lang = "en" }) => {
|
||||||
|
// Use the API data hook directly
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
pagination,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
updatePagination,
|
||||||
|
refetch
|
||||||
|
} = useApiData<schema.ApplicationData>('/api/applications');
|
||||||
|
|
||||||
|
// State for managing view/edit modes
|
||||||
|
const [mode, setMode] = useState<FormMode>("list");
|
||||||
|
const [selectedItem, setSelectedItem] = useState<schema.ApplicationData | null>(null);
|
||||||
|
const [gridCols, setGridCols] = useState<GridSize>(3);
|
||||||
|
|
||||||
|
// Fields to display in the cards
|
||||||
|
const showFields = ["application_code", "site_url", "application_type"];
|
||||||
|
|
||||||
|
// Search options
|
||||||
|
const searchOptions = {
|
||||||
|
typeOptions: [
|
||||||
|
{
|
||||||
|
value: "EMP",
|
||||||
|
label: translations[lang].employee,
|
||||||
|
icon: <User className="mr-2 h-4 w-4" />
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "OCC",
|
||||||
|
label: translations[lang].occupant,
|
||||||
|
icon: <Building className="mr-2 h-4 w-4" />
|
||||||
|
}
|
||||||
|
],
|
||||||
|
urlOptions: [
|
||||||
|
"/dashboard",
|
||||||
|
"/individual",
|
||||||
|
"/user",
|
||||||
|
"/settings",
|
||||||
|
"/reports",
|
||||||
|
],
|
||||||
|
additionalFields: [
|
||||||
|
// {
|
||||||
|
// name: "status",
|
||||||
|
// label: translations[lang as "en" | "tr"].status,
|
||||||
|
// type: "select" as const,
|
||||||
|
// options: [
|
||||||
|
// { value: "active", label: "Active" },
|
||||||
|
// { value: "inactive", label: "Inactive" },
|
||||||
|
// { value: "pending", label: "Pending" }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle query changes
|
||||||
|
const handleQueryChange = (key: string, value: string | null) => {
|
||||||
|
const newQuery = { ...pagination.query };
|
||||||
|
|
||||||
|
if (value === null) {
|
||||||
|
// Remove the key if value is null
|
||||||
|
delete newQuery[key];
|
||||||
|
} else if (value.trim() === "") {
|
||||||
|
// Remove the key if value is empty string
|
||||||
|
delete newQuery[key];
|
||||||
|
} else {
|
||||||
|
// Add/update the key with the value
|
||||||
|
newQuery[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePagination({
|
||||||
|
page: 1, // Reset to first page on new search
|
||||||
|
query: newQuery,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset all filters
|
||||||
|
const handleResetAllFilters = () => {
|
||||||
|
updatePagination({
|
||||||
|
page: 1,
|
||||||
|
query: {},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle card actions
|
||||||
|
const handleCardClick = (item: schema.ApplicationData) => {
|
||||||
|
console.log("Card clicked:", item);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewClick = (item: schema.ApplicationData) => {
|
||||||
|
setSelectedItem(item);
|
||||||
|
setMode("view");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateClick = (item: schema.ApplicationData) => {
|
||||||
|
setSelectedItem(item);
|
||||||
|
setMode("update");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle create button click
|
||||||
|
const handleCreateClick = () => {
|
||||||
|
setSelectedItem(null);
|
||||||
|
setMode("create");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle cancel
|
||||||
|
const handleCancel = () => {
|
||||||
|
setMode("list");
|
||||||
|
setSelectedItem(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<div className="mb-4 flex justify-between items-center">
|
||||||
|
<h1 className="text-2xl font-bold">{translations[lang].applicationTitle || "Applications"}</h1>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<GridSelectionComponent
|
||||||
|
gridCols={gridCols}
|
||||||
|
setGridCols={setGridCols}
|
||||||
|
translations={translations}
|
||||||
|
lang={lang}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<LanguageSelectionComponent
|
||||||
|
lang={lang as Language}
|
||||||
|
setLang={(newLang) => console.log("Language change not implemented", newLang)}
|
||||||
|
translations={translations}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mode === "list" ? (
|
||||||
|
<>
|
||||||
|
{/* Search Filters */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<div className="flex flex-col md:flex-row">
|
||||||
|
{/* Type selection - vertical on the left */}
|
||||||
|
<div className="w-full md:w-1/2 md:pr-4">
|
||||||
|
<TypeQueryModifier
|
||||||
|
fieldKey="application_for"
|
||||||
|
value={pagination.query["application_for__ilike"] ? pagination.query["application_for__ilike"].replace(/%/g, "") : ""}
|
||||||
|
options={searchOptions.typeOptions}
|
||||||
|
onQueryChange={handleQueryChange}
|
||||||
|
translations={translations}
|
||||||
|
lang={lang}
|
||||||
|
defaultValue="EMP"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters on the right */}
|
||||||
|
<div className={`w-full ${searchOptions.typeOptions.length > 0 ? 'md:w-1/2 md:pl-4' : ''} 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 || "Filter Selection"}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleResetAllFilters}
|
||||||
|
className="text-xs"
|
||||||
|
>
|
||||||
|
{translations[lang].resetAll || "Reset All"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search input */}
|
||||||
|
<TextQueryModifier
|
||||||
|
fieldKey="name"
|
||||||
|
value={pagination.query["name__ilike"] ? pagination.query["name__ilike"].replace(/%/g, "") : ""}
|
||||||
|
label={translations[lang].search || "Search"}
|
||||||
|
onQueryChange={handleQueryChange}
|
||||||
|
translations={translations}
|
||||||
|
lang={lang}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Site URL dropdown */}
|
||||||
|
{searchOptions.urlOptions.length > 0 && (
|
||||||
|
<SelectQueryModifier
|
||||||
|
fieldKey="site_url"
|
||||||
|
value={pagination.query["site_url__ilike"] ? pagination.query["site_url__ilike"].replace(/%/g, "") : ""}
|
||||||
|
label={translations[lang].siteUrl || "Site URL"}
|
||||||
|
options={searchOptions.urlOptions.map(url => ({ value: url, label: url }))}
|
||||||
|
onQueryChange={handleQueryChange}
|
||||||
|
translations={translations}
|
||||||
|
lang={lang}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Additional fields */}
|
||||||
|
{/* {searchOptions.additionalFields && searchOptions.additionalFields?.map(field => (
|
||||||
|
<SelectQueryModifier
|
||||||
|
key={field.name}
|
||||||
|
fieldKey={field.name}
|
||||||
|
value={pagination.query[`${field.name}__ilike`] ? pagination.query[`${field.name}__ilike`].replace(/%/g, "") : ""}
|
||||||
|
label={field.label}
|
||||||
|
options={field.options || []}
|
||||||
|
onQueryChange={handleQueryChange}
|
||||||
|
translations={translations}
|
||||||
|
lang={lang}
|
||||||
|
/>
|
||||||
|
))} */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Create Button */}
|
||||||
|
<Card className="my-4">
|
||||||
|
<CardContent className="pt-6">
|
||||||
|
<CreateButton
|
||||||
|
onClick={handleCreateClick}
|
||||||
|
translations={translations}
|
||||||
|
lang={lang}
|
||||||
|
/>
|
||||||
|
</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="name"
|
||||||
|
onCardClick={handleCardClick}
|
||||||
|
gridCols={gridCols}
|
||||||
|
showViewIcon={true}
|
||||||
|
showUpdateIcon={true}
|
||||||
|
onViewClick={handleViewClick}
|
||||||
|
onUpdateClick={handleUpdateClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<FormDisplay<schema.ApplicationData>
|
||||||
|
initialData={selectedItem || undefined}
|
||||||
|
mode={mode}
|
||||||
|
refetch={refetch}
|
||||||
|
setMode={setMode}
|
||||||
|
setSelectedItem={setSelectedItem}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
lang={lang}
|
||||||
|
translations={translations}
|
||||||
|
formProps={{
|
||||||
|
fieldDefinitions: mode === 'create' ? schema.createFieldDefinitions :
|
||||||
|
mode === 'update' ? schema.updateFieldDefinitions :
|
||||||
|
schema.viewFieldDefinitions,
|
||||||
|
validationSchema: mode === 'create' ? schema.CreateApplicationSchema :
|
||||||
|
mode === 'update' ? schema.UpdateApplicationSchema :
|
||||||
|
schema.ViewApplicationSchema,
|
||||||
|
fieldsByMode: schema.fieldsByMode
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApplicationPage;
|
||||||
@@ -0,0 +1,391 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// Base schema with all possible fields
|
||||||
|
const ApplicationBaseSchema = z.object({
|
||||||
|
// Identification fields
|
||||||
|
uu_id: z.number().optional(),
|
||||||
|
name: z.string().min(1, "Name is required"),
|
||||||
|
application_code: z.string().min(1, "Application code is required"),
|
||||||
|
|
||||||
|
// Application details
|
||||||
|
site_url: z.string().min(1, "Site URL is required"),
|
||||||
|
application_type: z.string().min(1, "Application type is required"),
|
||||||
|
application_for: z.string().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
|
||||||
|
// Status fields
|
||||||
|
active: z.boolean().default(true),
|
||||||
|
deleted: z.boolean().default(false),
|
||||||
|
|
||||||
|
// System fields
|
||||||
|
created_at: z.string().optional(),
|
||||||
|
updated_at: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schema for creating a new application
|
||||||
|
export const CreateApplicationSchema = ApplicationBaseSchema.omit({
|
||||||
|
uu_id: true,
|
||||||
|
created_at: true,
|
||||||
|
updated_at: true,
|
||||||
|
deleted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schema for updating an existing application
|
||||||
|
export const UpdateApplicationSchema = ApplicationBaseSchema.omit({
|
||||||
|
created_at: true,
|
||||||
|
updated_at: true,
|
||||||
|
deleted: true,
|
||||||
|
}).required({
|
||||||
|
uu_id: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Schema for viewing an application (all fields)
|
||||||
|
export const ViewApplicationSchema = ApplicationBaseSchema;
|
||||||
|
|
||||||
|
// Default schema (used for validation)
|
||||||
|
export const ApplicationSchema = ApplicationBaseSchema;
|
||||||
|
|
||||||
|
export type ApplicationFormData = z.infer<typeof ApplicationSchema>;
|
||||||
|
export type CreateApplicationFormData = z.infer<typeof CreateApplicationSchema>;
|
||||||
|
export type UpdateApplicationFormData = z.infer<typeof UpdateApplicationSchema>;
|
||||||
|
export type ViewApplicationFormData = z.infer<typeof ViewApplicationSchema>;
|
||||||
|
|
||||||
|
// Define field definition type
|
||||||
|
export interface FieldDefinition {
|
||||||
|
type: string;
|
||||||
|
group: string;
|
||||||
|
label: string;
|
||||||
|
options?: string[];
|
||||||
|
readOnly?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
defaultValue?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplicationData {
|
||||||
|
id?: number;
|
||||||
|
name: string;
|
||||||
|
application_code: string;
|
||||||
|
site_url: string;
|
||||||
|
application_type: string;
|
||||||
|
application_for?: string;
|
||||||
|
description?: string;
|
||||||
|
active: boolean;
|
||||||
|
deleted?: boolean;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base field definitions grouped by section
|
||||||
|
const baseFieldDefinitions = {
|
||||||
|
// Identification fields
|
||||||
|
identificationInfo: {
|
||||||
|
title: "Identification Information",
|
||||||
|
order: 1,
|
||||||
|
fields: {
|
||||||
|
uu_id: { type: "text", label: "UUID", readOnly: true, required: false },
|
||||||
|
name: { type: "text", label: "Name", readOnly: false, required: true },
|
||||||
|
application_code: {
|
||||||
|
type: "text",
|
||||||
|
label: "Application Code",
|
||||||
|
readOnly: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Application details
|
||||||
|
applicationDetails: {
|
||||||
|
title: "Application Details",
|
||||||
|
order: 2,
|
||||||
|
fields: {
|
||||||
|
site_url: {
|
||||||
|
type: "text",
|
||||||
|
label: "Site URL",
|
||||||
|
readOnly: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
application_type: {
|
||||||
|
type: "select",
|
||||||
|
label: "Application Type",
|
||||||
|
options: ["info", "Dash", "Admin"],
|
||||||
|
readOnly: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
application_for: {
|
||||||
|
type: "select",
|
||||||
|
label: "Application For",
|
||||||
|
options: ["EMP", "OCC"],
|
||||||
|
readOnly: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: "textarea",
|
||||||
|
label: "Description",
|
||||||
|
readOnly: false,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Status fields
|
||||||
|
statusInfo: {
|
||||||
|
title: "Status Information",
|
||||||
|
order: 3,
|
||||||
|
fields: {
|
||||||
|
active: {
|
||||||
|
type: "checkbox",
|
||||||
|
label: "Active",
|
||||||
|
readOnly: false,
|
||||||
|
required: false,
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
deleted: {
|
||||||
|
type: "checkbox",
|
||||||
|
label: "Deleted",
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// System fields
|
||||||
|
systemInfo: {
|
||||||
|
title: "System Information",
|
||||||
|
order: 4,
|
||||||
|
fields: {
|
||||||
|
created_at: {
|
||||||
|
type: "date",
|
||||||
|
label: "Created At",
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
type: "date",
|
||||||
|
label: "Updated At",
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to flatten grouped field definitions into a flat structure
|
||||||
|
const flattenFieldDefinitions = (
|
||||||
|
groupedDefs: any
|
||||||
|
): Record<string, FieldDefinition> => {
|
||||||
|
const result: Record<string, FieldDefinition> = {};
|
||||||
|
|
||||||
|
Object.entries(groupedDefs).forEach(
|
||||||
|
([groupName, groupConfig]: [string, any]) => {
|
||||||
|
Object.entries(groupConfig.fields).forEach(
|
||||||
|
([fieldName, fieldConfig]: [string, any]) => {
|
||||||
|
result[fieldName] = {
|
||||||
|
...fieldConfig,
|
||||||
|
group: groupName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a flat version of the field definitions for compatibility
|
||||||
|
const flatFieldDefinitions = flattenFieldDefinitions(baseFieldDefinitions);
|
||||||
|
|
||||||
|
// Create mode-specific field definitions using the flattened structure
|
||||||
|
export const createFieldDefinitions = {
|
||||||
|
name: {
|
||||||
|
...flatFieldDefinitions.name,
|
||||||
|
readOnly: false,
|
||||||
|
required: true,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
application_code: {
|
||||||
|
...flatFieldDefinitions.application_code,
|
||||||
|
readOnly: false,
|
||||||
|
required: true,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
site_url: {
|
||||||
|
...flatFieldDefinitions.site_url,
|
||||||
|
readOnly: false,
|
||||||
|
required: true,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
application_type: {
|
||||||
|
...flatFieldDefinitions.application_type,
|
||||||
|
readOnly: false,
|
||||||
|
required: true,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
application_for: {
|
||||||
|
...flatFieldDefinitions.application_for,
|
||||||
|
readOnly: false,
|
||||||
|
required: false,
|
||||||
|
defaultValue: "EMP",
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
...flatFieldDefinitions.description,
|
||||||
|
readOnly: false,
|
||||||
|
required: false,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
...flatFieldDefinitions.active,
|
||||||
|
readOnly: false,
|
||||||
|
required: false,
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update mode-specific field definitions
|
||||||
|
export const updateFieldDefinitions = {
|
||||||
|
uu_id: {
|
||||||
|
...flatFieldDefinitions.uu_id,
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
...flatFieldDefinitions.name,
|
||||||
|
readOnly: false,
|
||||||
|
required: true,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
application_code: {
|
||||||
|
...flatFieldDefinitions.application_code,
|
||||||
|
readOnly: false,
|
||||||
|
required: true,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
site_url: {
|
||||||
|
...flatFieldDefinitions.site_url,
|
||||||
|
readOnly: false,
|
||||||
|
required: true,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
application_type: {
|
||||||
|
...flatFieldDefinitions.application_type,
|
||||||
|
readOnly: false,
|
||||||
|
required: true,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
application_for: {
|
||||||
|
...flatFieldDefinitions.application_for,
|
||||||
|
readOnly: false,
|
||||||
|
required: false,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
...flatFieldDefinitions.description,
|
||||||
|
readOnly: false,
|
||||||
|
required: false,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
...flatFieldDefinitions.active,
|
||||||
|
readOnly: false,
|
||||||
|
required: false,
|
||||||
|
defaultValue: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// View mode-specific field definitions
|
||||||
|
export const viewFieldDefinitions = {
|
||||||
|
uu_id: {
|
||||||
|
...flatFieldDefinitions.uu_id,
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: 0,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
...flatFieldDefinitions.name,
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
application_code: {
|
||||||
|
...flatFieldDefinitions.application_code,
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
site_url: {
|
||||||
|
...flatFieldDefinitions.site_url,
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
application_type: {
|
||||||
|
...flatFieldDefinitions.application_type,
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
application_for: {
|
||||||
|
...flatFieldDefinitions.application_for,
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
...flatFieldDefinitions.description,
|
||||||
|
readOnly: true,
|
||||||
|
required: false,
|
||||||
|
defaultValue: "",
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
...flatFieldDefinitions.active,
|
||||||
|
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: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Combined field definitions for all modes
|
||||||
|
export const fieldDefinitions = {
|
||||||
|
...baseFieldDefinitions,
|
||||||
|
getDefinitionsByMode: (mode: "create" | "update" | "view") => {
|
||||||
|
switch (mode) {
|
||||||
|
case "create":
|
||||||
|
return createFieldDefinitions;
|
||||||
|
case "update":
|
||||||
|
return updateFieldDefinitions;
|
||||||
|
case "view":
|
||||||
|
return viewFieldDefinitions;
|
||||||
|
default:
|
||||||
|
return baseFieldDefinitions;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fields to show based on mode - dynamically generated from field definitions
|
||||||
|
export const fieldsByMode = {
|
||||||
|
create: Object.keys(createFieldDefinitions),
|
||||||
|
update: Object.keys(updateFieldDefinitions),
|
||||||
|
view: Object.keys(viewFieldDefinitions),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Note: Direct fetch function has been removed to use the API route instead
|
||||||
|
// Data fetching is now handled in the hooks.ts file using the POST endpoint
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { PageProps } from "@/components/validations/translations/translation";
|
import { PageProps } from "@/validations/translations/translation";
|
||||||
|
|
||||||
const DashboardPage: React.FC<PageProps> = () => {
|
const DashboardPage: React.FC<PageProps> = () => {
|
||||||
return <div>DashboardPage</div>;
|
return <div>DashboardPage</div>;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { PageProps } from "@/components/validations/translations/translation";
|
import { PageProps } from "@/validations/translations/translation";
|
||||||
|
|
||||||
const EmployeePage: React.FC<PageProps> = () => {
|
const EmployeePage: React.FC<PageProps> = () => {
|
||||||
return <div>EmployeePage</div>;
|
return <div>EmployeePage</div>;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import DashboardPage from "./dashboard/page";
|
||||||
|
import ApplicationPage from "./application/page";
|
||||||
|
|
||||||
|
const menuPages = {
|
||||||
|
"/dashboard": DashboardPage,
|
||||||
|
"/application": ApplicationPage,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default menuPages;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { PageProps } from "@/components/validations/translations/translation";
|
import { PageProps } from "@/validations/translations/translation";
|
||||||
|
|
||||||
const OcuppantPage: React.FC<PageProps> = () => {
|
const OcuppantPage: React.FC<PageProps> = () => {
|
||||||
return <div>OcuppantPage</div>;
|
return <div>OcuppantPage</div>;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { menuPages } from ".";
|
|
||||||
import { PageProps } from "../validations/translations/translation";
|
import { PageProps } from "../validations/translations/translation";
|
||||||
import { UnAuthorizedPage } from "./unauthorizedpage";
|
import { UnAuthorizedPage } from "./unauthorizedpage";
|
||||||
|
import menuPages from "./index";
|
||||||
|
|
||||||
export function retrievePageByUrl(url: string): React.FC<PageProps> {
|
export function retrievePageByUrl(url: string): React.FC<PageProps> {
|
||||||
if (url in menuPages) {
|
if (url in menuPages) {
|
||||||
Reference in New Issue
Block a user