first commit
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Search, Award } from "lucide-react";
|
||||
import {
|
||||
ColumnDef,
|
||||
DataTableLayout,
|
||||
Card,
|
||||
Input,
|
||||
} from "ikoncomponents";
|
||||
import { getAllGrade } from "@/app/utils/api/companyData/gradeApi.ts";
|
||||
import { GradeResponseDto } from "@/app/utils/api/companyData/gradeApi.ts/type";
|
||||
|
||||
function GradeDataTable() {
|
||||
const [gradeTableData, setGradeTableData] = useState<GradeResponseDto[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
// DataTableLayout defaults to list view and has no prop to start in grid, so
|
||||
// once mounted we click its built-in "Grid View" toggle once to default to cards.
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||
const defaultedToGrid = useRef(false);
|
||||
|
||||
const filteredGradeTableData = useMemo(() => {
|
||||
if (!search.trim()) return gradeTableData;
|
||||
return gradeTableData.filter((gradeData) =>
|
||||
gradeData.grade?.toLowerCase().includes(search.toLowerCase()),
|
||||
);
|
||||
}, [gradeTableData, search]);
|
||||
|
||||
const fetchGradeTableData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const gradeData = await getAllGrade();
|
||||
setGradeTableData(gradeData?.content || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching grade data:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchGradeTableData();
|
||||
}, []);
|
||||
|
||||
// Switch DataTableLayout into grid (card) view as soon as it mounts, once.
|
||||
useEffect(() => {
|
||||
if (isLoading || defaultedToGrid.current) return;
|
||||
const gridButton =
|
||||
tableContainerRef.current?.querySelector<HTMLButtonElement>(
|
||||
'button[title="Grid View"]',
|
||||
);
|
||||
if (gridButton) {
|
||||
gridButton.click();
|
||||
defaultedToGrid.current = true;
|
||||
}
|
||||
}, [isLoading]);
|
||||
|
||||
// ikoncomponents passes the row object directly to cell(), not { row }.
|
||||
const columns: ColumnDef<GradeResponseDto>[] = [
|
||||
{
|
||||
accessorKey: "grade",
|
||||
header: "Grade",
|
||||
cell: (row) => <span>{row.grade || "n/a"}</span>,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-5 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold">Grades</h1>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{filteredGradeTableData.length} of {gradeTableData.length} grades
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref={tableContainerRef}>
|
||||
<DataTableLayout
|
||||
data={filteredGradeTableData}
|
||||
columns={columns}
|
||||
extraTools={{
|
||||
keyExtractor: (row: GradeResponseDto) => row.id,
|
||||
totalPages: 1,
|
||||
currentPage: 1,
|
||||
isLoading,
|
||||
onReload: fetchGradeTableData,
|
||||
actionNode: (
|
||||
<div className="flex w-full min-w-[260px] flex-1 items-center gap-2 rounded-lg border px-3 h-9">
|
||||
<Search className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<div className="h-4 w-px shrink-0 bg-border" />
|
||||
<Input
|
||||
placeholder="Search grades..."
|
||||
className="h-8 border-none p-0 text-sm shadow-none placeholder:text-muted-foreground focus-visible:ring-0"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
gridComponent: (data: GradeResponseDto[]) => (
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
|
||||
{data.length > 0 ? (
|
||||
data.map((gradeItem) => (
|
||||
<Card
|
||||
key={gradeItem.id}
|
||||
// flex-row + py-4 explicitly override the base Card's
|
||||
// `flex flex-col gap-6 py-6`, otherwise the icon stacks on
|
||||
// top and the content centers with a lot of empty space.
|
||||
className="group relative flex flex-row items-center gap-3.5 overflow-hidden rounded-xl border py-4! px-4 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-md"
|
||||
>
|
||||
{/* Left accent rail */}
|
||||
<span className="absolute inset-y-0 left-0 w-1 bg-primary/60 opacity-0 transition-opacity duration-200 group-hover:opacity-100" />
|
||||
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary transition-transform duration-200 group-hover:scale-105">
|
||||
<Award className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
|
||||
Grade
|
||||
</p>
|
||||
<p
|
||||
className="truncate text-sm font-semibold leading-tight text-foreground"
|
||||
title={gradeItem.grade}
|
||||
>
|
||||
{gradeItem.grade || "-"}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className="col-span-2 flex items-center justify-center rounded-lg border border-dashed p-8 text-muted-foreground sm:col-span-3 lg:col-span-4">
|
||||
No grades found.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GradeDataTable;
|
||||
@@ -0,0 +1,150 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Search, Briefcase } from "lucide-react";
|
||||
import {
|
||||
ColumnDef,
|
||||
DataTableLayout,
|
||||
Card,
|
||||
Input,
|
||||
} from "ikoncomponents";
|
||||
import { getAllRoles } from "@/app/utils/api/companyData/roleApi.ts";
|
||||
|
||||
interface RoleData {
|
||||
id: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
function RoleDataTable() {
|
||||
const [roleTableData, setRoleTableData] = useState<RoleData[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
// DataTableLayout defaults to list view and has no prop to start in grid, so
|
||||
// once mounted we click its built-in "Grid View" toggle once to default to cards.
|
||||
const tableContainerRef = useRef<HTMLDivElement>(null);
|
||||
const defaultedToGrid = useRef(false);
|
||||
|
||||
const filteredRoleTableData = useMemo(() => {
|
||||
if (!search.trim()) return roleTableData;
|
||||
return roleTableData.filter((roleData) =>
|
||||
roleData.role?.toLowerCase().includes(search.toLowerCase()),
|
||||
);
|
||||
}, [roleTableData, search]);
|
||||
|
||||
const fetchRoleTableData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const roleData = await getAllRoles();
|
||||
setRoleTableData(roleData || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching role data:", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchRoleTableData();
|
||||
}, []);
|
||||
|
||||
// Switch DataTableLayout into grid (card) view as soon as it mounts, once.
|
||||
useEffect(() => {
|
||||
if (isLoading || defaultedToGrid.current) return;
|
||||
const gridButton =
|
||||
tableContainerRef.current?.querySelector<HTMLButtonElement>(
|
||||
'button[title="Grid View"]',
|
||||
);
|
||||
if (gridButton) {
|
||||
gridButton.click();
|
||||
defaultedToGrid.current = true;
|
||||
}
|
||||
}, [isLoading]);
|
||||
|
||||
// ikoncomponents passes the row object directly to cell(), not { row }.
|
||||
const columns: ColumnDef<RoleData>[] = [
|
||||
{
|
||||
accessorKey: "role",
|
||||
header: "Role",
|
||||
cell: (row) => <span>{row.role || "n/a"}</span>,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-5 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold">Roles</h1>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{filteredRoleTableData.length} of {roleTableData.length} roles
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref={tableContainerRef}>
|
||||
<DataTableLayout
|
||||
data={filteredRoleTableData}
|
||||
columns={columns}
|
||||
extraTools={{
|
||||
keyExtractor: (row: RoleData) => row.id,
|
||||
totalPages: 1,
|
||||
currentPage: 1,
|
||||
isLoading,
|
||||
onReload: fetchRoleTableData,
|
||||
actionNode: (
|
||||
<div className="flex w-full min-w-[260px] flex-1 items-center gap-2 rounded-lg border px-3 h-9">
|
||||
<Search className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<div className="h-4 w-px shrink-0 bg-border" />
|
||||
<Input
|
||||
placeholder="Search roles..."
|
||||
className="h-8 border-none p-0 text-sm shadow-none placeholder:text-muted-foreground focus-visible:ring-0"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
gridComponent: (data: RoleData[]) => (
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
|
||||
{data.length > 0 ? (
|
||||
data.map((roleItem) => (
|
||||
<Card
|
||||
key={roleItem.id}
|
||||
// flex-row + py-4 explicitly override the base Card's
|
||||
// `flex flex-col gap-6 py-6`, otherwise the icon stacks on
|
||||
// top and the content centers with a lot of empty space.
|
||||
className="group relative flex flex-row items-center gap-3.5 overflow-hidden rounded-xl border py-4! px-4 shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/40 hover:shadow-md"
|
||||
>
|
||||
{/* Left accent rail */}
|
||||
<span className="absolute inset-y-0 left-0 w-1 bg-primary/60 opacity-0 transition-opacity duration-200 group-hover:opacity-100" />
|
||||
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary transition-transform duration-200 group-hover:scale-105">
|
||||
<Briefcase className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] font-semibold uppercase tracking-[0.08em] text-muted-foreground">
|
||||
Role
|
||||
</p>
|
||||
<p
|
||||
className="truncate text-sm font-semibold leading-tight text-foreground"
|
||||
title={roleItem.role}
|
||||
>
|
||||
{roleItem.role || "-"}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className="col-span-2 flex items-center justify-center rounded-lg border border-dashed p-8 text-muted-foreground sm:col-span-3 lg:col-span-4">
|
||||
No roles found.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RoleDataTable;
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { CustomTabs, TabArray } from "ikoncomponents";
|
||||
import GradeDataTable from "../grade-table";
|
||||
import RoleDataTable from "../role-table";
|
||||
|
||||
const tabArray: TabArray[] = [
|
||||
{
|
||||
tabName: "Role",
|
||||
tabId: "tab-role",
|
||||
default: true,
|
||||
tabContent: <RoleDataTable />,
|
||||
},
|
||||
{
|
||||
tabName: "Grade",
|
||||
tabId: "tab-grade",
|
||||
default: false,
|
||||
tabContent: <GradeDataTable />,
|
||||
},
|
||||
];
|
||||
|
||||
export default function CompanyDataTab() {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<CustomTabs tabArray={tabArray} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user