feat: update all table, add pending approval table.

This commit is contained in:
hanif salafi 2025-09-23 08:20:25 +07:00
parent 5fdcfdfdb9
commit 926bd78f53
23 changed files with 2710 additions and 195 deletions

View File

@ -0,0 +1,53 @@
"use client";
import * as React from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import PendingApprovalTable from "./pending-approval-table";
import TableVideo from "./table-video";
const AudioVisualTabs = () => {
const [activeTab, setActiveTab] = React.useState("submitted");
return (
<div className="w-full">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<div className="flex items-center justify-between mb-6">
<TabsList className="inline-flex h-10 bg-gray-50 border border-gray-200 p-1 rounded-lg shadow-sm">
<TabsTrigger
value="submitted"
className="text-sm font-medium px-6 py-2 h-8 rounded-md transition-all duration-200 data-[state=active]:bg-white data-[state=active]:text-blue-600 data-[state=active]:shadow-sm data-[state=inactive]:text-gray-600 data-[state=inactive]:hover:text-gray-800"
>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
Submitted Audio-Visual
</div>
</TabsTrigger>
<TabsTrigger
value="pending"
className="text-sm font-medium px-6 py-2 h-8 rounded-md transition-all duration-200 data-[state=active]:bg-white data-[state=active]:text-orange-600 data-[state=active]:shadow-sm data-[state=inactive]:text-gray-600 data-[state=inactive]:hover:text-gray-800"
>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-orange-500"></div>
Waiting Approval
</div>
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="submitted" className="mt-0">
<div className="bg-white rounded-lg border border-gray-200 shadow-sm">
<TableVideo />
</div>
</TabsContent>
<TabsContent value="pending" className="mt-0">
<div className="bg-white rounded-lg border border-gray-200 shadow-sm">
<PendingApprovalTable />
</div>
</TabsContent>
</Tabs>
</div>
);
};
export default AudioVisualTabs;

View File

@ -0,0 +1,211 @@
"use client";
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, CheckCircle, Clock } from "lucide-react";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { PendingApprovalData } from "@/service/content/content";
const usePendingApprovalColumns = () => {
const router = useRouter();
const userLevelId = getCookiesDecrypt("ulie");
const columns: ColumnDef<PendingApprovalData>[] = [
{
accessorKey: "id",
header: "No",
cell: ({ row, table }) => {
const pageIndex = table.getState().pagination.pageIndex;
const pageSize = table.getState().pagination.pageSize;
const rowIndex = row.index;
return (
<div className="flex items-center gap-5">
<div className="flex-1 text-start">
<h4 className="text-sm font-medium text-default-600 whitespace-nowrap mb-1">
{pageIndex * pageSize + rowIndex + 1}
</h4>
</div>
</div>
);
},
},
{
accessorKey: "title",
header: "Title",
cell: ({ row }: { row: { getValue: (key: string) => string } }) => {
const title: string = row.getValue("title");
return (
<span className="whitespace-nowrap">
{title.length > 50 ? `${title.slice(0, 30)}...` : title}
</span>
);
},
},
{
accessorKey: "categoryName",
header: "Category",
cell: ({ row }) => {
const categoryName = row.getValue("categoryName") as string;
return (
<span className="whitespace-nowrap">
{categoryName || "-"}
</span>
);
},
},
{
accessorKey: "authorName",
header: "Author",
cell: ({ row }) => (
<span className="whitespace-nowrap">
{row.getValue("authorName")}
</span>
),
},
{
accessorKey: "submittedAt",
header: "Submitted Date",
cell: ({ row }) => {
const submittedAt = row.getValue("submittedAt") as string;
const formattedDate = submittedAt
? format(new Date(submittedAt), "dd-MM-yyyy HH:mm:ss")
: "-";
return <span className="whitespace-nowrap">{formattedDate}</span>;
},
},
{
accessorKey: "priority",
header: "Priority",
cell: ({ row }) => {
const priority = row.getValue("priority") as string;
const colors: Record<string, string> = {
high: "bg-red-100 text-red-600",
medium: "bg-yellow-100 text-yellow-600",
low: "bg-green-100 text-green-600",
};
const priorityStyles = colors[priority] || "bg-gray-100 text-gray-600";
return (
<Badge
className={cn(
"rounded-full px-3 py-1 text-xs font-medium",
priorityStyles
)}
>
{priority?.toUpperCase() || "N/A"}
</Badge>
);
},
},
{
accessorKey: "currentStep",
header: "Progress",
cell: ({ row }) => {
const currentStep = row.getValue("currentStep") as number;
const totalSteps = row.original.totalSteps;
const progress = totalSteps > 0 ? (currentStep / totalSteps) * 100 : 0;
return (
<div className="flex items-center gap-2">
<div className="w-16 bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
<span className="text-xs text-gray-600">
{currentStep}/{totalSteps}
</span>
</div>
);
},
},
{
accessorKey: "daysInQueue",
header: "Days in Queue",
cell: ({ row }) => {
const days = row.getValue("daysInQueue") as number;
const colorClass = days > 7 ? "text-red-600" : days > 3 ? "text-yellow-600" : "text-green-600";
return (
<div className="flex items-center gap-1">
<Clock className="h-4 w-4" />
<span className={`text-sm font-medium ${colorClass}`}>
{days} days
</span>
</div>
);
},
},
{
accessorKey: "estimatedTime",
header: "Est. Time",
cell: ({ row }) => (
<span className="whitespace-nowrap text-sm text-gray-600">
{row.getValue("estimatedTime") || "-"}
</span>
),
},
{
id: "actions",
accessorKey: "action",
header: "Action",
enableHiding: false,
cell: ({ row }) => {
const canApprove = row.original.canApprove;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0 hover:text-black" align="end">
<Link
href={`/admin/content/audio-visual/detail/${row.original.id}`}
className="hover:text-black"
>
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View
</DropdownMenuItem>
</Link>
{canApprove && (
<DropdownMenuItem
className="p-2 border-b text-green-700 bg-green-50 group rounded-none"
onClick={() => {
// Handle approval logic here
console.log("Approve item:", row.original.id);
}}
>
<CheckCircle className="w-4 h-4 me-1.5" />
Approve
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
return columns;
};
export default usePendingApprovalColumns;

View File

@ -0,0 +1,271 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
ChevronDown,
Search,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import { listPendingApproval, PendingApprovalData } from "@/service/content/content";
import usePendingApprovalColumns from "./pending-approval-columns";
const PendingApprovalTable = () => {
const searchParams = useSearchParams();
const [dataTable, setDataTable] = React.useState<PendingApprovalData[]>([]);
const [totalData, setTotalData] = React.useState<number>(0);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [showData, setShowData] = React.useState("10");
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10, // Set default value
});
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const [search, setSearch] = React.useState("");
const columns = usePendingApprovalColumns();
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
// Disable client-side pagination karena kita menggunakan server-side pagination
// getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
// Manual pagination state untuk server-side pagination
manualPagination: true,
pageCount: totalPage,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
// Sync showData dengan pagination state
React.useEffect(() => {
console.log("showData changed to:", showData);
setPagination(prev => ({
...prev,
pageSize: Number(showData)
}));
// Reset ke halaman 1 ketika page size berubah
setPage(1);
}, [showData]);
React.useEffect(() => {
fetchPendingData();
}, [page, showData, search]);
async function fetchPendingData() {
try {
console.log("fetchPendingData: page =", page, "showData =", showData, "Number(showData) =", Number(showData));
const res = await listPendingApproval(page, Number(showData));
if (res && !res.error && res.data) {
// Filter data based on search if provided
let filteredData = res.data;
if (search) {
filteredData = res.data.filter((item: PendingApprovalData) =>
item.title.toLowerCase().includes(search.toLowerCase()) ||
item.authorName.toLowerCase().includes(search.toLowerCase()) ||
item.categoryName.toLowerCase().includes(search.toLowerCase())
);
}
setDataTable(filteredData);
setTotalData(filteredData.length);
setTotalPage(Math.ceil(filteredData.length / Number(showData)));
} else {
setDataTable([]);
setTotalData(0);
setTotalPage(1);
}
} catch (error) {
console.error("Error fetching pending approval data:", error);
setDataTable([]);
setTotalData(0);
setTotalPage(1);
}
}
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
};
return (
<div className="w-full overflow-x-auto">
<div className="flex flex-col md:flex-row lg:flex-row md:justify-between lg:justify-between items-center md:px-5 lg:px-5">
<div className="relative w-full md:w-[200px] lg:w-[200px] px-2">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400 dark:text-white" />
<Input
type="text"
placeholder="Search title, author, category..."
className="pl-9 bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
value={search}
onChange={handleSearch}
/>
</div>
<div className="flex flex-row items-center gap-3">
<div className="flex items-center py-4">
<div className="mx-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
{showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 text-sm">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
50 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="100">
100 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="250">
250 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="flex items-center py-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Columns <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<Table className="overflow-hidden mt-3 mx-3">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No pending approval data found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
</div>
);
};
export default PendingApprovalTable;

View File

@ -1,48 +1,49 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import AudioVisualTabs from "./components/audio-visual-tabs";
import { UploadIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Icon } from "@iconify/react/dist/iconify.js";
import Link from "next/link";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import TableVideo from "./components/table-video";
const ReactTableImagePage = () => {
const ReactTableAudioVisualPage = () => {
return (
<div>
<div className="min-h-screen bg-gray-50">
{/* <SiteBreadcrumb /> */}
<div className="space-y-4">
<Card className="m-2">
<CardHeader className="border-b border-solid border-default-200 mb-6">
<CardTitle>
<div className="flex items-center">
<div className="flex-1 text-xl font-medium text-default-900">
Audio Visual
<div className="p-6">
<div className="max-w-7xl mx-auto">
<Card className="shadow-sm border-0">
<CardHeader className="border-b border-gray-200 bg-white rounded-t-lg">
<CardTitle>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
<UploadIcon className="w-4 h-4 text-blue-600" />
</div>
<div>
<h1 className="text-xl font-semibold text-gray-900">Audio-Visual Management</h1>
<p className="text-sm text-gray-500">Manage your submitted audio-visual files and pending approvals</p>
</div>
</div>
<div className="flex-none">
<Link href={"/admin/content/audio-visual/create"}>
<Button color="primary" className="text-white shadow-sm hover:shadow-md transition-shadow">
<UploadIcon size={18} className="mr-2" />
Create Audio-Visual
</Button>
</Link>
</div>
</div>
<div className="flex-none">
<Link href={"/admin/content/audio-visual/create"}>
<Button color="primary" className="text-white">
<UploadIcon size={18} className="mr-2" />
Audio Visual
</Button>
</Link>
{/* <Link href={"/contributor/content/image/createAi"}>
<Button color="primary" className="text-white ml-3">
<UploadIcon />
Unggah Foto Dengan AI
</Button>
</Link> */}
</div>
</div>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<TableVideo />
</CardContent>
</Card>
</CardTitle>
</CardHeader>
<CardContent className="p-6 bg-gray-50">
<AudioVisualTabs />
</CardContent>
</Card>
</div>
</div>
</div>
);
};
export default ReactTableImagePage;
export default ReactTableAudioVisualPage;

View File

@ -0,0 +1,53 @@
"use client";
import * as React from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import TableAudio from "./table-audio";
import PendingApprovalTable from "./pending-approval-table";
const AudioTabs = () => {
const [activeTab, setActiveTab] = React.useState("submitted");
return (
<div className="w-full">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<div className="flex items-center justify-between mb-6">
<TabsList className="inline-flex h-10 bg-gray-50 border border-gray-200 p-1 rounded-lg shadow-sm">
<TabsTrigger
value="submitted"
className="text-sm font-medium px-6 py-2 h-8 rounded-md transition-all duration-200 data-[state=active]:bg-white data-[state=active]:text-blue-600 data-[state=active]:shadow-sm data-[state=inactive]:text-gray-600 data-[state=inactive]:hover:text-gray-800"
>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
Submitted Audio
</div>
</TabsTrigger>
<TabsTrigger
value="pending"
className="text-sm font-medium px-6 py-2 h-8 rounded-md transition-all duration-200 data-[state=active]:bg-white data-[state=active]:text-orange-600 data-[state=active]:shadow-sm data-[state=inactive]:text-gray-600 data-[state=inactive]:hover:text-gray-800"
>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-orange-500"></div>
Waiting Approval
</div>
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="submitted" className="mt-0">
<div className="bg-white rounded-lg border border-gray-200 shadow-sm">
<TableAudio />
</div>
</TabsContent>
<TabsContent value="pending" className="mt-0">
<div className="bg-white rounded-lg border border-gray-200 shadow-sm">
<PendingApprovalTable />
</div>
</TabsContent>
</Tabs>
</div>
);
};
export default AudioTabs;

View File

@ -0,0 +1,211 @@
"use client";
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, CheckCircle, Clock } from "lucide-react";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { PendingApprovalData } from "@/service/content/content";
const usePendingApprovalColumns = () => {
const router = useRouter();
const userLevelId = getCookiesDecrypt("ulie");
const columns: ColumnDef<PendingApprovalData>[] = [
{
accessorKey: "id",
header: "No",
cell: ({ row, table }) => {
const pageIndex = table.getState().pagination.pageIndex;
const pageSize = table.getState().pagination.pageSize;
const rowIndex = row.index;
return (
<div className="flex items-center gap-5">
<div className="flex-1 text-start">
<h4 className="text-sm font-medium text-default-600 whitespace-nowrap mb-1">
{pageIndex * pageSize + rowIndex + 1}
</h4>
</div>
</div>
);
},
},
{
accessorKey: "title",
header: "Title",
cell: ({ row }: { row: { getValue: (key: string) => string } }) => {
const title: string = row.getValue("title");
return (
<span className="whitespace-nowrap">
{title.length > 50 ? `${title.slice(0, 30)}...` : title}
</span>
);
},
},
{
accessorKey: "categoryName",
header: "Category",
cell: ({ row }) => {
const categoryName = row.getValue("categoryName") as string;
return (
<span className="whitespace-nowrap">
{categoryName || "-"}
</span>
);
},
},
{
accessorKey: "authorName",
header: "Author",
cell: ({ row }) => (
<span className="whitespace-nowrap">
{row.getValue("authorName")}
</span>
),
},
{
accessorKey: "submittedAt",
header: "Submitted Date",
cell: ({ row }) => {
const submittedAt = row.getValue("submittedAt") as string;
const formattedDate = submittedAt
? format(new Date(submittedAt), "dd-MM-yyyy HH:mm:ss")
: "-";
return <span className="whitespace-nowrap">{formattedDate}</span>;
},
},
{
accessorKey: "priority",
header: "Priority",
cell: ({ row }) => {
const priority = row.getValue("priority") as string;
const colors: Record<string, string> = {
high: "bg-red-100 text-red-600",
medium: "bg-yellow-100 text-yellow-600",
low: "bg-green-100 text-green-600",
};
const priorityStyles = colors[priority] || "bg-gray-100 text-gray-600";
return (
<Badge
className={cn(
"rounded-full px-3 py-1 text-xs font-medium",
priorityStyles
)}
>
{priority?.toUpperCase() || "N/A"}
</Badge>
);
},
},
{
accessorKey: "currentStep",
header: "Progress",
cell: ({ row }) => {
const currentStep = row.getValue("currentStep") as number;
const totalSteps = row.original.totalSteps;
const progress = totalSteps > 0 ? (currentStep / totalSteps) * 100 : 0;
return (
<div className="flex items-center gap-2">
<div className="w-16 bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
<span className="text-xs text-gray-600">
{currentStep}/{totalSteps}
</span>
</div>
);
},
},
{
accessorKey: "daysInQueue",
header: "Days in Queue",
cell: ({ row }) => {
const days = row.getValue("daysInQueue") as number;
const colorClass = days > 7 ? "text-red-600" : days > 3 ? "text-yellow-600" : "text-green-600";
return (
<div className="flex items-center gap-1">
<Clock className="h-4 w-4" />
<span className={`text-sm font-medium ${colorClass}`}>
{days} days
</span>
</div>
);
},
},
{
accessorKey: "estimatedTime",
header: "Est. Time",
cell: ({ row }) => (
<span className="whitespace-nowrap text-sm text-gray-600">
{row.getValue("estimatedTime") || "-"}
</span>
),
},
{
id: "actions",
accessorKey: "action",
header: "Action",
enableHiding: false,
cell: ({ row }) => {
const canApprove = row.original.canApprove;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0 hover:text-black" align="end">
<Link
href={`/admin/content/audio/detail/${row.original.id}`}
className="hover:text-black"
>
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View
</DropdownMenuItem>
</Link>
{canApprove && (
<DropdownMenuItem
className="p-2 border-b text-green-700 bg-green-50 group rounded-none"
onClick={() => {
// Handle approval logic here
console.log("Approve item:", row.original.id);
}}
>
<CheckCircle className="w-4 h-4 me-1.5" />
Approve
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
return columns;
};
export default usePendingApprovalColumns;

View File

@ -0,0 +1,271 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
ChevronDown,
Search,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import { listPendingApproval, PendingApprovalData } from "@/service/content/content";
import usePendingApprovalColumns from "./pending-approval-columns";
const PendingApprovalTable = () => {
const searchParams = useSearchParams();
const [dataTable, setDataTable] = React.useState<PendingApprovalData[]>([]);
const [totalData, setTotalData] = React.useState<number>(0);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [showData, setShowData] = React.useState("10");
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10, // Set default value
});
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const [search, setSearch] = React.useState("");
const columns = usePendingApprovalColumns();
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
// Disable client-side pagination karena kita menggunakan server-side pagination
// getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
// Manual pagination state untuk server-side pagination
manualPagination: true,
pageCount: totalPage,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
// Sync showData dengan pagination state
React.useEffect(() => {
console.log("showData changed to:", showData);
setPagination(prev => ({
...prev,
pageSize: Number(showData)
}));
// Reset ke halaman 1 ketika page size berubah
setPage(1);
}, [showData]);
React.useEffect(() => {
fetchPendingData();
}, [page, showData, search]);
async function fetchPendingData() {
try {
console.log("fetchPendingData: page =", page, "showData =", showData, "Number(showData) =", Number(showData));
const res = await listPendingApproval(page, Number(showData));
if (res && !res.error && res.data) {
// Filter data based on search if provided
let filteredData = res.data;
if (search) {
filteredData = res.data.filter((item: PendingApprovalData) =>
item.title.toLowerCase().includes(search.toLowerCase()) ||
item.authorName.toLowerCase().includes(search.toLowerCase()) ||
item.categoryName.toLowerCase().includes(search.toLowerCase())
);
}
setDataTable(filteredData);
setTotalData(filteredData.length);
setTotalPage(Math.ceil(filteredData.length / Number(showData)));
} else {
setDataTable([]);
setTotalData(0);
setTotalPage(1);
}
} catch (error) {
console.error("Error fetching pending approval data:", error);
setDataTable([]);
setTotalData(0);
setTotalPage(1);
}
}
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
};
return (
<div className="w-full overflow-x-auto">
<div className="flex flex-col md:flex-row lg:flex-row md:justify-between lg:justify-between items-center md:px-5 lg:px-5">
<div className="relative w-full md:w-[200px] lg:w-[200px] px-2">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400 dark:text-white" />
<Input
type="text"
placeholder="Search title, author, category..."
className="pl-9 bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
value={search}
onChange={handleSearch}
/>
</div>
<div className="flex flex-row items-center gap-3">
<div className="flex items-center py-4">
<div className="mx-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
{showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 text-sm">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
50 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="100">
100 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="250">
250 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="flex items-center py-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Columns <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<Table className="overflow-hidden mt-3 mx-3">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No pending approval data found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
</div>
);
};
export default PendingApprovalTable;

View File

@ -1,46 +1,49 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import AudioTabs from "./components/audio-tabs";
import { UploadIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Icon } from "@iconify/react/dist/iconify.js";
import TableAudio from "./components/table-audio";
import Link from "next/link";
const ReactTableAudioPage = () => {
return (
<div>
<div className="min-h-screen bg-gray-50">
{/* <SiteBreadcrumb /> */}
<div className="space-y-4 m-3">
<Card>
<CardHeader className="border-b border-solid border-default-200 mb-6">
<CardTitle>
<div className="flex items-center">
<div className="flex-1 text-xl font-medium text-default-900">
Audio
<div className="p-6">
<div className="max-w-7xl mx-auto">
<Card className="shadow-sm border-0">
<CardHeader className="border-b border-gray-200 bg-white rounded-t-lg">
<CardTitle>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
<UploadIcon className="w-4 h-4 text-blue-600" />
</div>
<div>
<h1 className="text-xl font-semibold text-gray-900">Audio Management</h1>
<p className="text-sm text-gray-500">Manage your submitted audio files and pending approvals</p>
</div>
</div>
<div className="flex-none">
<Link href={"/admin/content/audio/create"}>
<Button color="primary" className="text-white shadow-sm hover:shadow-md transition-shadow">
<UploadIcon size={18} className="mr-2" />
Create Audio
</Button>
</Link>
</div>
</div>
<div className="flex-none">
<Link href={"/admin/content/audio/create"}>
<Button color="primary" className="text-white">
<UploadIcon size={18} className="mr-2" />
Create Audio
</Button>
</Link>
{/* <Button color="primary" className="text-white ml-3">
<UploadIcon />
Unggah Audio Dengan AI
</Button> */}
</div>
</div>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<TableAudio />
</CardContent>
</Card>
</CardTitle>
</CardHeader>
<CardContent className="p-6 bg-gray-50">
<AudioTabs />
</CardContent>
</Card>
</div>
</div>
</div>
);
};
export default ReactTableAudioPage;
export default ReactTableAudioPage;

View File

@ -0,0 +1,53 @@
"use client";
import * as React from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import TableTeks from "./table-teks";
import PendingApprovalTable from "./pending-approval-table";
const DocumentTabs = () => {
const [activeTab, setActiveTab] = React.useState("submitted");
return (
<div className="w-full">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<div className="flex items-center justify-between mb-6">
<TabsList className="inline-flex h-10 bg-gray-50 border border-gray-200 p-1 rounded-lg shadow-sm">
<TabsTrigger
value="submitted"
className="text-sm font-medium px-6 py-2 h-8 rounded-md transition-all duration-200 data-[state=active]:bg-white data-[state=active]:text-blue-600 data-[state=active]:shadow-sm data-[state=inactive]:text-gray-600 data-[state=inactive]:hover:text-gray-800"
>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
Submitted Documents
</div>
</TabsTrigger>
<TabsTrigger
value="pending"
className="text-sm font-medium px-6 py-2 h-8 rounded-md transition-all duration-200 data-[state=active]:bg-white data-[state=active]:text-orange-600 data-[state=active]:shadow-sm data-[state=inactive]:text-gray-600 data-[state=inactive]:hover:text-gray-800"
>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-orange-500"></div>
Waiting Approval
</div>
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="submitted" className="mt-0">
<div className="bg-white rounded-lg border border-gray-200 shadow-sm">
<TableTeks />
</div>
</TabsContent>
<TabsContent value="pending" className="mt-0">
<div className="bg-white rounded-lg border border-gray-200 shadow-sm">
<PendingApprovalTable />
</div>
</TabsContent>
</Tabs>
</div>
);
};
export default DocumentTabs;

View File

@ -0,0 +1,211 @@
"use client";
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, CheckCircle, Clock } from "lucide-react";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { PendingApprovalData } from "@/service/content/content";
const usePendingApprovalColumns = () => {
const router = useRouter();
const userLevelId = getCookiesDecrypt("ulie");
const columns: ColumnDef<PendingApprovalData>[] = [
{
accessorKey: "id",
header: "No",
cell: ({ row, table }) => {
const pageIndex = table.getState().pagination.pageIndex;
const pageSize = table.getState().pagination.pageSize;
const rowIndex = row.index;
return (
<div className="flex items-center gap-5">
<div className="flex-1 text-start">
<h4 className="text-sm font-medium text-default-600 whitespace-nowrap mb-1">
{pageIndex * pageSize + rowIndex + 1}
</h4>
</div>
</div>
);
},
},
{
accessorKey: "title",
header: "Title",
cell: ({ row }: { row: { getValue: (key: string) => string } }) => {
const title: string = row.getValue("title");
return (
<span className="whitespace-nowrap">
{title.length > 50 ? `${title.slice(0, 30)}...` : title}
</span>
);
},
},
{
accessorKey: "categoryName",
header: "Category",
cell: ({ row }) => {
const categoryName = row.getValue("categoryName") as string;
return (
<span className="whitespace-nowrap">
{categoryName || "-"}
</span>
);
},
},
{
accessorKey: "authorName",
header: "Author",
cell: ({ row }) => (
<span className="whitespace-nowrap">
{row.getValue("authorName")}
</span>
),
},
{
accessorKey: "submittedAt",
header: "Submitted Date",
cell: ({ row }) => {
const submittedAt = row.getValue("submittedAt") as string;
const formattedDate = submittedAt
? format(new Date(submittedAt), "dd-MM-yyyy HH:mm:ss")
: "-";
return <span className="whitespace-nowrap">{formattedDate}</span>;
},
},
{
accessorKey: "priority",
header: "Priority",
cell: ({ row }) => {
const priority = row.getValue("priority") as string;
const colors: Record<string, string> = {
high: "bg-red-100 text-red-600",
medium: "bg-yellow-100 text-yellow-600",
low: "bg-green-100 text-green-600",
};
const priorityStyles = colors[priority] || "bg-gray-100 text-gray-600";
return (
<Badge
className={cn(
"rounded-full px-3 py-1 text-xs font-medium",
priorityStyles
)}
>
{priority?.toUpperCase() || "N/A"}
</Badge>
);
},
},
{
accessorKey: "currentStep",
header: "Progress",
cell: ({ row }) => {
const currentStep = row.getValue("currentStep") as number;
const totalSteps = row.original.totalSteps;
const progress = totalSteps > 0 ? (currentStep / totalSteps) * 100 : 0;
return (
<div className="flex items-center gap-2">
<div className="w-16 bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
<span className="text-xs text-gray-600">
{currentStep}/{totalSteps}
</span>
</div>
);
},
},
{
accessorKey: "daysInQueue",
header: "Days in Queue",
cell: ({ row }) => {
const days = row.getValue("daysInQueue") as number;
const colorClass = days > 7 ? "text-red-600" : days > 3 ? "text-yellow-600" : "text-green-600";
return (
<div className="flex items-center gap-1">
<Clock className="h-4 w-4" />
<span className={`text-sm font-medium ${colorClass}`}>
{days} days
</span>
</div>
);
},
},
{
accessorKey: "estimatedTime",
header: "Est. Time",
cell: ({ row }) => (
<span className="whitespace-nowrap text-sm text-gray-600">
{row.getValue("estimatedTime") || "-"}
</span>
),
},
{
id: "actions",
accessorKey: "action",
header: "Action",
enableHiding: false,
cell: ({ row }) => {
const canApprove = row.original.canApprove;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0 hover:text-black" align="end">
<Link
href={`/admin/content/document/detail/${row.original.id}`}
className="hover:text-black"
>
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View
</DropdownMenuItem>
</Link>
{canApprove && (
<DropdownMenuItem
className="p-2 border-b text-green-700 bg-green-50 group rounded-none"
onClick={() => {
// Handle approval logic here
console.log("Approve item:", row.original.id);
}}
>
<CheckCircle className="w-4 h-4 me-1.5" />
Approve
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
return columns;
};
export default usePendingApprovalColumns;

View File

@ -0,0 +1,255 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
ChevronDown,
Search,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import { listPendingApproval, PendingApprovalData } from "@/service/content/content";
import usePendingApprovalColumns from "./pending-approval-columns";
const PendingApprovalTable = () => {
const searchParams = useSearchParams();
const [dataTable, setDataTable] = React.useState<PendingApprovalData[]>([]);
const [totalData, setTotalData] = React.useState<number>(0);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [showData, setShowData] = React.useState("10");
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: Number(showData),
});
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const [search, setSearch] = React.useState("");
const columns = usePendingApprovalColumns();
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
React.useEffect(() => {
fetchPendingData();
}, [page, showData, search]);
async function fetchPendingData() {
try {
const res = await listPendingApproval(page, Number(showData));
if (res && !res.error && res.data) {
// Filter data based on search if provided
let filteredData = res.data;
if (search) {
filteredData = res.data.filter((item: PendingApprovalData) =>
item.title.toLowerCase().includes(search.toLowerCase()) ||
item.authorName.toLowerCase().includes(search.toLowerCase()) ||
item.categoryName.toLowerCase().includes(search.toLowerCase())
);
}
setDataTable(filteredData);
setTotalData(filteredData.length);
setTotalPage(Math.ceil(filteredData.length / Number(showData)));
} else {
setDataTable([]);
setTotalData(0);
setTotalPage(1);
}
} catch (error) {
console.error("Error fetching pending approval data:", error);
setDataTable([]);
setTotalData(0);
setTotalPage(1);
}
}
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
};
return (
<div className="w-full overflow-x-auto">
<div className="flex flex-col md:flex-row lg:flex-row md:justify-between lg:justify-between items-center md:px-5 lg:px-5">
<div className="relative w-full md:w-[200px] lg:w-[200px] px-2">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400 dark:text-white" />
<Input
type="text"
placeholder="Search title, author, category..."
className="pl-9 bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
value={search}
onChange={handleSearch}
/>
</div>
<div className="flex flex-row items-center gap-3">
<div className="flex items-center py-4">
<div className="mx-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
{showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 text-sm">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
50 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="100">
100 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="250">
250 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="flex items-center py-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Columns <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<Table className="overflow-hidden mt-3 mx-3">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No pending approval data found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
</div>
);
};
export default PendingApprovalTable;

View File

@ -1,40 +1,49 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import DocumentTabs from "./components/document-tabs";
import { UploadIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import TableTeks from "./components/table-teks";
import Link from "next/link";
const ReactTableTeksPage = () => {
const ReactTableDocumentPage = () => {
return (
<div>
<div className="space-y-4 m-3">
<Card>
<CardHeader className="border-b border-solid border-default-200 mb-6">
<CardTitle>
<div className="flex items-center">
<div className="flex-1 text-xl font-medium text-default-900">
Text
<div className="min-h-screen bg-gray-50">
{/* <SiteBreadcrumb /> */}
<div className="p-6">
<div className="max-w-7xl mx-auto">
<Card className="shadow-sm border-0">
<CardHeader className="border-b border-gray-200 bg-white rounded-t-lg">
<CardTitle>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
<UploadIcon className="w-4 h-4 text-blue-600" />
</div>
<div>
<h1 className="text-xl font-semibold text-gray-900">Document Management</h1>
<p className="text-sm text-gray-500">Manage your submitted documents and pending approvals</p>
</div>
</div>
<div className="flex-none">
<Link href={"/admin/content/document/create"}>
<Button color="primary" className="text-white shadow-sm hover:shadow-md transition-shadow">
<UploadIcon size={18} className="mr-2" />
Create Document
</Button>
</Link>
</div>
</div>
<div className="flex-none">
<Link href={"/admin/content/document/create"}>
<Button color="primary" className="text-white">
<UploadIcon size={18} className="mr-2" />
Create Text
</Button>
</Link>
</div>
</div>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<TableTeks />
</CardContent>
</Card>
</CardTitle>
</CardHeader>
<CardContent className="p-6 bg-gray-50">
<DocumentTabs />
</CardContent>
</Card>
</div>
</div>
</div>
);
};
export default ReactTableTeksPage;
export default ReactTableDocumentPage;

View File

@ -0,0 +1,53 @@
"use client";
import * as React from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import TableImage from "./table-image";
import PendingApprovalTable from "./pending-approval-table";
const ImageTabs = () => {
const [activeTab, setActiveTab] = React.useState("pending");
return (
<div className="w-full">
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<div className="flex items-center justify-between mb-6">
<TabsList className="inline-flex h-10 bg-gray-50 border border-gray-200 p-1 rounded-lg shadow-sm">
<TabsTrigger
value="pending"
className="text-sm font-medium px-6 py-2 h-8 rounded-md transition-all duration-200 data-[state=active]:bg-white data-[state=active]:text-orange-600 data-[state=active]:shadow-sm data-[state=inactive]:text-gray-600 data-[state=inactive]:hover:text-gray-800"
>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-orange-500"></div>
Waiting Approval
</div>
</TabsTrigger>
<TabsTrigger
value="submitted"
className="text-sm font-medium px-6 py-2 h-8 rounded-md transition-all duration-200 data-[state=active]:bg-white data-[state=active]:text-blue-600 data-[state=active]:shadow-sm data-[state=inactive]:text-gray-600 data-[state=inactive]:hover:text-gray-800"
>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500"></div>
Submitted Images
</div>
</TabsTrigger>
</TabsList>
</div>
<TabsContent value="submitted" className="mt-0">
<div className="bg-white rounded-lg border border-gray-200 shadow-sm">
<TableImage />
</div>
</TabsContent>
<TabsContent value="pending" className="mt-0">
<div className="bg-white rounded-lg border border-gray-200 shadow-sm">
<PendingApprovalTable />
</div>
</TabsContent>
</Tabs>
</div>
);
};
export default ImageTabs;

View File

@ -0,0 +1,217 @@
"use client";
import * as React from "react";
import { ColumnDef } from "@tanstack/react-table";
import { Eye, MoreVertical, CheckCircle, Clock } from "lucide-react";
import { cn, getCookiesDecrypt } from "@/lib/utils";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
} from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { PendingApprovalData } from "@/service/content/content";
const usePendingApprovalColumns = () => {
const router = useRouter();
const userLevelId = getCookiesDecrypt("ulie");
const columns: ColumnDef<PendingApprovalData>[] = [
{
accessorKey: "id",
header: "No",
cell: ({ row, table }) => {
const pageIndex = table.getState().pagination.pageIndex;
const pageSize = table.getState().pagination.pageSize;
const rowIndex = row.index;
console.log("pageIndex :", pageIndex);
console.log("pageSize :", pageSize);
console.log("rowIndex :", rowIndex);
console.log("calculated number :", pageIndex * pageSize + rowIndex + 1);
return (
<div className="flex items-center gap-5">
<div className="flex-1 text-start">
<h4 className="text-sm font-medium text-default-600 whitespace-nowrap mb-1">
{pageIndex * pageSize + rowIndex + 1}
</h4>
</div>
</div>
);
},
},
{
accessorKey: "title",
header: "Title",
cell: ({ row }: { row: { getValue: (key: string) => string } }) => {
const title: string = row.getValue("title");
return (
<span className="whitespace-nowrap">
{title.length > 50 ? `${title.slice(0, 30)}...` : title}
</span>
);
},
},
{
accessorKey: "categoryName",
header: "Category",
cell: ({ row }) => {
const categoryName = row.getValue("categoryName") as string;
return (
<span className="whitespace-nowrap">
{categoryName || "-"}
</span>
);
},
},
{
accessorKey: "authorName",
header: "Author",
cell: ({ row }) => (
<span className="whitespace-nowrap">
{row.getValue("authorName")}
</span>
),
},
{
accessorKey: "submittedAt",
header: "Submitted Date",
cell: ({ row }) => {
const submittedAt = row.getValue("submittedAt") as string;
const formattedDate = submittedAt
? format(new Date(submittedAt), "dd-MM-yyyy HH:mm:ss")
: "-";
return <span className="whitespace-nowrap">{formattedDate}</span>;
},
},
{
accessorKey: "priority",
header: "Priority",
cell: ({ row }) => {
const priority = row.getValue("priority") as string;
const colors: Record<string, string> = {
high: "bg-red-100 text-red-600",
medium: "bg-yellow-100 text-yellow-600",
low: "bg-green-100 text-green-600",
};
const priorityStyles = colors[priority] || "bg-gray-100 text-gray-600";
return (
<Badge
className={cn(
"rounded-full px-3 py-1 text-xs font-medium",
priorityStyles
)}
>
{priority?.toUpperCase() || "N/A"}
</Badge>
);
},
},
{
accessorKey: "currentStep",
header: "Progress",
cell: ({ row }) => {
const currentStep = row.getValue("currentStep") as number;
const totalSteps = row.original.totalSteps;
const progress = totalSteps > 0 ? (currentStep / totalSteps) * 100 : 0;
return (
<div className="flex items-center gap-2">
<div className="w-16 bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${progress}%` }}
/>
</div>
<span className="text-xs text-gray-600">
{currentStep}/{totalSteps}
</span>
</div>
);
},
},
{
accessorKey: "daysInQueue",
header: "Days in Queue",
cell: ({ row }) => {
const days = row.getValue("daysInQueue") as number;
const colorClass = days > 7 ? "text-red-600" : days > 3 ? "text-yellow-600" : "text-green-600";
return (
<div className="flex items-center gap-1">
<Clock className="h-4 w-4" />
<span className={`text-sm font-medium ${colorClass}`}>
{days} days
</span>
</div>
);
},
},
{
accessorKey: "estimatedTime",
header: "Est. Time",
cell: ({ row }) => (
<span className="whitespace-nowrap text-sm text-gray-600">
{row.getValue("estimatedTime") || "-"}
</span>
),
},
{
id: "actions",
accessorKey: "action",
header: "Action",
enableHiding: false,
cell: ({ row }) => {
const canApprove = row.original.canApprove;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="icon"
className="bg-transparent ring-offset-transparent hover:bg-transparent hover:ring-0 hover:ring-transparent"
>
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4 text-default-800" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0 hover:text-black" align="end">
<Link
href={`/admin/content/image/detail/${row.original.id}`}
className="hover:text-black"
>
<DropdownMenuItem className="p-2 border-b text-default-700 group rounded-none">
<Eye className="w-4 h-4 me-1.5" />
View
</DropdownMenuItem>
</Link>
{canApprove && (
<DropdownMenuItem
className="p-2 border-b text-green-700 bg-green-50 group rounded-none"
onClick={() => {
// Handle approval logic here
console.log("Approve item:", row.original.id);
}}
>
<CheckCircle className="w-4 h-4 me-1.5" />
Approve
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
return columns;
};
export default usePendingApprovalColumns;

View File

@ -0,0 +1,264 @@
"use client";
import * as React from "react";
import {
ColumnDef,
ColumnFiltersState,
PaginationState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
ChevronDown,
Search,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { useSearchParams } from "next/navigation";
import TablePagination from "@/components/table/table-pagination";
import { listPendingApproval, PendingApprovalData } from "@/service/content/content";
import usePendingApprovalColumns from "./pending-approval-columns";
import { fi } from "date-fns/locale";
const PendingApprovalTable = () => {
const searchParams = useSearchParams();
const [dataTable, setDataTable] = React.useState<PendingApprovalData[]>([]);
const [totalData, setTotalData] = React.useState<number>(0);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const [showData, setShowData] = React.useState("10");
const [pagination, setPagination] = React.useState<PaginationState>({
pageIndex: 0,
pageSize: 10, // Set default value
});
const [page, setPage] = React.useState(1);
const [totalPage, setTotalPage] = React.useState(1);
const [search, setSearch] = React.useState("");
const columns = usePendingApprovalColumns();
const table = useReactTable({
data: dataTable,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
// Disable client-side pagination karena kita menggunakan server-side pagination
// getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
onPaginationChange: setPagination,
// Manual pagination state untuk server-side pagination
manualPagination: true,
pageCount: totalPage,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
pagination,
},
});
React.useEffect(() => {
const pageFromUrl = searchParams?.get("page");
if (pageFromUrl) {
setPage(Number(pageFromUrl));
}
}, [searchParams]);
// Sync showData dengan pagination state
React.useEffect(() => {
console.log("showData changed to:", showData);
setPagination(prev => ({
...prev,
pageSize: Number(showData)
}));
// Reset ke halaman 1 ketika page size berubah
setPage(1);
}, [showData]);
React.useEffect(() => {
fetchPendingData();
}, [page, showData, search]);
async function fetchPendingData() {
try {
console.log("fetchPendingData: page =", page, "showData =", showData, "Number(showData) =", Number(showData));
const res = await listPendingApproval(page, Number(showData));
if (res && !res.error && res.data.data) {
// Filter data based on search if provided
let filteredData = res.data.data;
setDataTable(filteredData);
setTotalData(res.data.meta.count);
setTotalPage(Math.ceil(res.data.meta.count / Number(showData)));
} else {
setDataTable([]);
setTotalData(0);
setTotalPage(1);
}
} catch (error) {
console.error("Error fetching pending approval data:", error);
setDataTable([]);
setTotalData(0);
setTotalPage(1);
}
}
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
};
return (
<div className="w-full overflow-x-auto">
<div className="flex flex-col md:flex-row lg:flex-row md:justify-between lg:justify-between items-center md:px-5 lg:px-5">
<div className="relative w-full md:w-[200px] lg:w-[200px] px-2">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400 dark:text-white" />
<Input
type="text"
placeholder="Search title, author, category..."
className="pl-9 bg-transparent dark:border-secondary dark:placeholder-white/80 dark:focus:border-secondary dark:text-white"
value={search}
onChange={handleSearch}
/>
</div>
<div className="flex flex-row items-center gap-3">
<div className="flex items-center py-4">
<div className="mx-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="md" variant="outline">
{showData} Data
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 text-sm">
<DropdownMenuRadioGroup
value={showData}
onValueChange={setShowData}
>
<DropdownMenuRadioItem value="10">
10 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="50">
50 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="100">
100 Data
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="250">
250 Data
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="flex items-center py-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="ml-auto" size="md">
Columns <ChevronDown />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
</div>
<Table className="overflow-hidden mt-3 mx-3">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="bg-default-200">
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="h-[75px]"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns?.length} className="h-24 text-center">
No pending approval data found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<TablePagination
table={table}
totalData={totalData}
totalPage={totalPage}
/>
</div>
);
};
export default PendingApprovalTable;

View File

@ -192,7 +192,7 @@ const TableImage = () => {
totalPage: Number(showData),
title: search || undefined,
categoryId: categoryFilter ? Number(categoryFilter) : undefined,
typeId: 1, // image content type
typeId: 1, // image content typeoriginalRows
statusId: statusFilter?.length > 0 ? Number(statusFilter[0]) : undefined,
startDate: formattedStartDate || undefined,
endDate: formattedEndDate || undefined,
@ -207,17 +207,8 @@ const TableImage = () => {
item.no = (page - 1) * Number(showData) + index + 1;
});
setDataTable(data);
setTotalData(data.length);
setTotalPage(Math.ceil(data.length / Number(showData)));
} else {
// Fallback to old structure if API still returns old format
const contentData = data?.content;
contentData.forEach((item: any, index: number) => {
item.no = (page - 1) * Number(showData) + index + 1;
});
setDataTable(contentData);
setTotalData(data?.totalElements);
setTotalPage(data?.totalPages);
setTotalData(res?.data.meta.count);
setTotalPage(Math.ceil(res?.data.meta.count / Number(showData)));
}
} catch (error) {
console.error("Error fetching tasks:", error);

View File

@ -1,44 +1,52 @@
"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import SiteBreadcrumb from "@/components/site-breadcrumb";
import TableImage from "./components/table-image";
import ImageTabs from "./components/image-tabs";
import { UploadIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import Link from "next/link";
const ReactTableImagePage = () => {
return (
<div>
<div className="min-h-screen bg-gray-50">
{/* <SiteBreadcrumb /> */}
<div className="space-y-4 m-3">
<Card>
<CardHeader className="border-b border-solid border-default-200 mb-6">
<CardTitle>
<div className="flex items-center">
<div className="flex-1 text-xl font-medium text-default-900">
Image
<div className="p-6">
<div className="max-w-7xl mx-auto">
<Card className="shadow-sm border-0">
<CardHeader className="border-b border-gray-200 bg-white rounded-t-lg">
<CardTitle>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
<UploadIcon className="w-4 h-4 text-blue-600" />
</div>
<div>
<h1 className="text-xl font-semibold text-gray-900">Image Management</h1>
<p className="text-sm text-gray-500">Manage your submitted images and pending approvals</p>
</div>
</div>
<div className="flex-none">
<Link href={"/admin/content/image/create"}>
<Button color="primary" className="text-white shadow-sm hover:shadow-md transition-shadow">
<UploadIcon size={18} className="mr-2" />
Create Image
</Button>
</Link>
{/* <Link href={"/contributor/content/image/createAi"}>
<Button color="primary" className="text-white ml-3">
<UploadIcon />
Unggah Foto Dengan AI
</Button>
</Link> */}
</div>
</div>
<div className="flex-none">
<Link href={"/admin/content/image/create"}>
<Button color="primary" className="text-white">
<UploadIcon size={18} className="mr-2" />
Create Image
</Button>
</Link>
{/* <Link href={"/contributor/content/image/createAi"}>
<Button color="primary" className="text-white ml-3">
<UploadIcon />
Unggah Foto Dengan AI
</Button>
</Link> */}
</div>
</div>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<TableImage />
</CardContent>
</Card>
</CardTitle>
</CardHeader>
<CardContent className="p-6 bg-gray-50">
<ImageTabs />
</CardContent>
</Card>
</div>
</div>
</div>
);

View File

@ -30,6 +30,8 @@ import {
publishMedia,
rejectFiles,
submitApproval,
getArticleDetail,
ArticleDetailData,
} from "@/service/content/content";
import { Badge } from "@/components/ui/badge";
import { MailIcon } from "lucide-react";
@ -84,26 +86,81 @@ type FileType = {
url: string;
thumbnailFileUrl: string;
fileName: string;
// New API fields
articleId?: number;
filePath?: string;
fileUrl?: string;
fileThumbnail?: string | null;
fileAlt?: string;
widthPixel?: number | null;
heightPixel?: number | null;
size?: string;
downloadCount?: number;
createdById?: number;
statusId?: number;
isPublish?: boolean;
publishedAt?: string | null;
isActive?: boolean;
createdAt?: string;
updatedAt?: string;
};
type Detail = {
id: string;
id: number;
title: string;
description: string;
htmlDescription: string;
slug: string;
category: {
categoryId: number;
categoryName: string;
typeId: number;
tags: string;
thumbnailUrl: string;
pageUrl: string | null;
createdById: number;
createdByName: string;
shareCount: number;
viewCount: number;
commentCount: number;
aiArticleId: number | null;
oldId: number;
statusId: number;
isBanner: boolean;
isPublish: boolean;
publishedAt: string | null;
isActive: boolean;
createdAt: string;
updatedAt: string;
files: FileType[] | null;
categories: {
id: number;
title: string;
description: string;
thumbnailUrl: string;
slug: string | null;
tags: string[];
thumbnailPath: string | null;
parentId: number;
oldCategoryId: number | null;
createdById: number;
statusId: number;
isPublish: boolean;
publishedAt: string | null;
isEnabled: boolean | null;
isActive: boolean;
createdAt: string;
updatedAt: string;
}[];
// Legacy fields for backward compatibility
category?: {
id: number;
name: string;
};
categoryName: string;
creatorName: string;
thumbnailLink: string;
tags: string;
statusName: string;
isPublish: boolean;
needApprovalFromLevel: number;
files: FileType[];
uploadedById: number;
creatorName?: string;
thumbnailLink?: string;
statusName?: string;
needApprovalFromLevel?: number;
uploadedById?: number;
};
const ViewEditor = dynamic(
@ -203,7 +260,7 @@ export default function FormImageDetail() {
console.log("data category", resCategory);
if (scheduleId && scheduleType === "3") {
const findCategory = resCategory.find((o) =>
const findCategory = resCategory?.find((o) =>
o.name.toLowerCase().includes("pers rilis")
);
@ -230,42 +287,86 @@ export default function FormImageDetail() {
useEffect(() => {
async function initState() {
if (id) {
const response = await detailMedia(id);
const details = response?.data?.data;
console.log("detail", details);
setFiles(details?.files);
setDetail(details);
setMain({
type: details?.fileType.name,
url: details?.files[0]?.url,
names: details?.files[0]?.fileName,
format: details?.files[0]?.format,
});
setupPlacementCheck(details?.files?.length);
try {
const response = await getArticleDetail(Number(id));
const details = response?.data?.data;
console.log("detail", details);
// Map the new API response to the expected format
const mappedDetail: Detail = {
...details,
// Map legacy fields for backward compatibility
category: details.categories && details.categories.length > 0 ? {
id: details.categories[0].id,
name: details.categories[0].title
} : undefined,
creatorName: details.createdByName,
thumbnailLink: details.thumbnailUrl,
statusName: getStatusName(details.statusId),
needApprovalFromLevel: 0, // This might need to be updated based on your business logic
uploadedById: details.createdById,
files: details.files || []
};
if (details.publishedForObject) {
const publisherIds = details.publishedForObject.map(
(obj: any) => obj.id
// Map files from new API structure to expected format
const mappedFiles = (mappedDetail.files || []).map((file: any) => ({
id: file.id,
url: file.fileUrl || file.url,
thumbnailFileUrl: file.fileThumbnail || file.thumbnailFileUrl || file.fileUrl,
fileName: file.fileName || file.fileName,
// Keep original API fields for reference
...file
}));
setFiles(mappedFiles);
setDetail(mappedDetail);
if (mappedFiles && mappedFiles.length > 0) {
setMain({
type: "image", // Default type for articles
url: mappedFiles[0]?.url || mappedDetail.thumbnailUrl,
names: mappedFiles[0]?.fileName || "image",
format: getFileExtension(mappedFiles[0]?.fileName || "jpg"),
});
setupPlacementCheck(mappedFiles.length);
}
// Set the selected target to the category ID from details
setSelectedTarget(String(mappedDetail.categoryId));
const fileUrls = mappedFiles.map((file: any) =>
file.thumbnailFileUrl || file.url || mappedDetail.thumbnailUrl || "default-image.jpg"
);
setSelectedPublishers(publisherIds);
setDetailThumb(fileUrls);
// Note: You might need to update this API call as well
const approvals = await getDataApprovalByMediaUpload(mappedDetail.id);
setApproval(approvals?.data?.data);
} catch (error) {
console.error("Error fetching article detail:", error);
}
// Set the selected target to the category ID from details
setSelectedTarget(String(details.category.id));
const filesData = details.files || [];
const fileUrls = filesData.map((file: { thumbnailFileUrl: string }) =>
file.thumbnailFileUrl ? file.thumbnailFileUrl : "default-image.jpg"
);
setDetailThumb(fileUrls);
const approvals = await getDataApprovalByMediaUpload(details?.id);
setApproval(approvals?.data?.data);
}
}
initState();
}, [refresh, setValue]);
// Helper function to get status name from status ID
const getStatusName = (statusId: number): string => {
const statusMap: { [key: number]: string } = {
1: "Menunggu Review",
2: "Diterima",
3: "Minta Update",
4: "Ditolak"
};
return statusMap[statusId] || "Unknown";
};
// Helper function to get file extension
const getFileExtension = (filename: string): string => {
const ext = filename.split('.').pop()?.toLowerCase();
return ext || 'jpg';
};
const actionApproval = (e: string) => {
const temp = [];
for (const element of detail.files) {
@ -478,7 +579,7 @@ export default function FormImageDetail() {
<Select
disabled
value={String(detail?.category?.id)}
value={String(detail?.categoryId || detail?.category?.id)}
// onValueChange={(id) => {
// console.log("Selected Category:", id);
// setSelectedTarget(id);
@ -490,18 +591,18 @@ export default function FormImageDetail() {
<SelectContent>
{/* Show the category from details if it doesn't exist in categories list */}
{detail &&
!categories.find(
!categories?.find(
(cat) =>
String(cat.id) === String(detail.category.id)
String(cat.id) === String(detail.categoryId || detail?.category?.id)
) && (
<SelectItem
key={String(detail.category.id)}
value={String(detail.category.id)}
key={String(detail.categoryId || detail?.category?.id)}
value={String(detail.categoryId || detail?.category?.id)}
>
{detail.category.name}
{detail.categoryName || detail?.category?.name}
</SelectItem>
)}
{categories.map((category) => (
{categories?.map((category) => (
<SelectItem
key={String(category.id)}
value={String(category.id)}
@ -538,12 +639,12 @@ export default function FormImageDetail() {
navigation={false}
className="h-[480px] object-cover w-full"
>
{detailThumb?.map((data: any) => (
<SwiperSlide key={data.id}>
{detailThumb?.map((data: any, index: number) => (
<SwiperSlide key={index}>
<img
className="h-[480px] max-w-[600px] rounded-md object-cover mx-auto border-2"
src={data}
alt={` ${data.id}`}
alt={`Image ${index + 1}`}
/>
</SwiperSlide>
))}
@ -559,12 +660,12 @@ export default function FormImageDetail() {
modules={[Pagination, Thumbs]}
// className="mySwiper2"
>
{detailThumb?.map((data: any) => (
<SwiperSlide key={data.id}>
{detailThumb?.map((data: any, index: number) => (
<SwiperSlide key={index}>
<img
className="object-cover h-[60px] w-[80px] border-2 border-slate-100"
src={data}
alt={` ${data.id}`}
alt={`Thumbnail ${index + 1}`}
/>
</SwiperSlide>
))}
@ -586,9 +687,9 @@ export default function FormImageDetail() {
render={({ field }) => (
<Input
type="text"
value={detail?.creatorName}
value={detail?.createdByName || detail?.creatorName}
onChange={field.onChange}
placeholder="Enter Title"
placeholder="Enter Creator Name"
/>
)}
/>
@ -603,7 +704,7 @@ export default function FormImageDetail() {
<Label>Preview</Label>
<Card className="mt-2 w-fit">
<img
src={detail.thumbnailLink}
src={detail.thumbnailUrl || detail.thumbnailLink}
alt="Thumbnail Gambar Utama"
className="h-[200px] rounded"
/>
@ -670,7 +771,7 @@ export default function FormImageDetail() {
/>
<div className="px-3 py-3 border mx-3">
<p>Information:</p>
<p className="text-sm text-slate-400">{detail?.statusName}</p>
<p className="text-sm text-slate-400">{detail?.statusName || getStatusName(detail?.statusId || 0)}</p>
<p>Komentar</p>
<p>{approval?.message}</p>
<p className="text-right text-sm">
@ -741,7 +842,7 @@ export default function FormImageDetail() {
<div className="w-[200px] h-[100px] flex justify-center items-center">
<img
key={index}
alt={`files-${index + 1}`}
alt={file.fileAlt || `files-${index + 1}`}
src={file.url}
onLoad={(e) => handleImageLoad(e, index)}
className={`h-[100px] object-cover ${
@ -963,11 +1064,11 @@ export default function FormImageDetail() {
</DialogContent>
</Dialog>
</Card>
{Number(detail?.needApprovalFromLevel) == Number(userLevelId) ||
{Number(detail?.needApprovalFromLevel || 0) == Number(userLevelId) ||
(detail?.isInternationalMedia == true &&
detail?.isForwardFromNational == true &&
Number(detail?.statusId) == 1) ? (
Number(detail?.uploadedById) == Number(userId) ? (
Number(detail?.createdById || detail?.uploadedById) == Number(userId) ? (
""
) : (
<div className="flex flex-col gap-2 p-3">

View File

@ -35,10 +35,20 @@ const TablePagination = ({
useEffect(() => {
const pageFromUrl = searchParams?.get("page");
console.log("pagination: pageFromUrl :", pageFromUrl);
if (pageFromUrl) {
const pageIndex = Math.min(Math.max(1, Number(pageFromUrl)), totalPage);
setCurrentPageIndex(pageIndex);
table.setPageIndex(pageIndex - 1); // Sinkronisasi tabel dengan URL
console.log("handlePageChange: pageIndex :", pageIndex);
console.log("handlePageChange: table.setPageIndex with :", pageIndex - 1);
table.setPageIndex(pageIndex - 1); // ✅ Konversi 1-based ke 0-based
} else {
// Jika tidak ada page di URL, set ke halaman 1
setCurrentPageIndex(1);
table.setPageIndex(0); // ✅ Set ke index 0 untuk halaman 1
}
}, [searchParams, totalPage, table]);
@ -47,9 +57,13 @@ const TablePagination = ({
const searchParams = new URLSearchParams(window.location.search);
searchParams.set("page", clampedPageIndex.toString());
console.log("handlePageChange: pageIndex :", pageIndex);
console.log("handlePageChange: clampedPageIndex :", clampedPageIndex);
console.log("handlePageChange: table.setPageIndex with :", clampedPageIndex - 1);
router.push(`${window.location.pathname}?${searchParams.toString()}`);
setCurrentPageIndex(clampedPageIndex);
table.setPageIndex(clampedPageIndex - 1); // Perbarui tabel dengan index berbasis 0
table.setPageIndex(clampedPageIndex - 1); // Perbarui tabel dengan index berbasis 0
};
const generatePageNumbers = () => {

55
components/ui/tabs.tsx Normal file
View File

@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-8 items-center justify-center rounded-md bg-muted p-0.5 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-2 py-1 text-xs font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

88
package-lock.json generated
View File

@ -20,6 +20,7 @@
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-visually-hidden": "^1.2.3",
"@tanstack/react-table": "^8.21.3",
"@tinymce/tinymce-react": "^6.3.0",
@ -2333,6 +2334,93 @@
}
}
},
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz",
"integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-roving-focus": "1.1.11",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
"integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",

View File

@ -21,6 +21,7 @@
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-visually-hidden": "^1.2.3",
"@tanstack/react-table": "^8.21.3",
"@tinymce/tinymce-react": "^6.3.0",

View File

@ -413,4 +413,125 @@ export async function listDataTeksNew(
startDate,
endDate
);
}
// Interface for pending approval data
export interface PendingApprovalData {
id: number;
title: string;
slug: string;
description: string;
categoryName: string;
authorName: string;
submittedAt: string;
currentStep: number;
totalSteps: number;
priority: string;
daysInQueue: number;
workflowName: string;
canApprove: boolean;
estimatedTime: string;
}
export interface PendingApprovalResponse {
success: boolean;
code: number;
messages: string[];
data: PendingApprovalData[];
}
// Function to fetch pending approval data
export async function listPendingApproval(
page: number = 1,
limit: number = 10
) {
const url = `articles/pending-approval?page=${page}&limit=${limit}`;
return await httpGetInterceptor(url);
}
// Interface for article file data
export interface ArticleFileData {
id: number;
articleId: number;
filePath: string;
fileUrl: string;
fileName: string;
fileThumbnail: string | null;
fileAlt: string;
widthPixel: number | null;
heightPixel: number | null;
size: string;
downloadCount: number;
createdById: number;
statusId: number;
isPublish: boolean;
publishedAt: string | null;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
// Interface for article category data
export interface ArticleCategoryData {
id: number;
title: string;
description: string;
thumbnailUrl: string;
slug: string | null;
tags: string[];
thumbnailPath: string | null;
parentId: number;
oldCategoryId: number | null;
createdById: number;
statusId: number;
isPublish: boolean;
publishedAt: string | null;
isEnabled: boolean | null;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
// Interface for article detail data
export interface ArticleDetailData {
id: number;
title: string;
slug: string;
description: string;
htmlDescription: string;
categoryId: number;
categoryName: string;
typeId: number;
tags: string;
thumbnailUrl: string;
pageUrl: string | null;
createdById: number;
createdByName: string;
shareCount: number;
viewCount: number;
commentCount: number;
aiArticleId: number | null;
oldId: number;
statusId: number;
isBanner: boolean;
isPublish: boolean;
publishedAt: string | null;
isActive: boolean;
createdAt: string;
updatedAt: string;
files: ArticleFileData[];
categories: ArticleCategoryData[];
}
export interface ArticleDetailResponse {
success: boolean;
code: number;
messages: string[];
data: ArticleDetailData;
}
// Function to fetch article detail
export async function getArticleDetail(id: number) {
const url = `articles/${id}`;
return await httpGetInterceptor(url);
}