From 926bd78f53e028524491033feabe4ccb10506686 Mon Sep 17 00:00:00 2001 From: hanif salafi Date: Tue, 23 Sep 2025 08:20:25 +0700 Subject: [PATCH] feat: update all table, add pending approval table. --- .../components/audio-visual-tabs.tsx | 53 ++++ .../components/pending-approval-columns.tsx | 211 ++++++++++++++ .../components/pending-approval-table.tsx | 271 ++++++++++++++++++ .../admin/content/audio-visual/page.tsx | 69 ++--- .../content/audio/components/audio-tabs.tsx | 53 ++++ .../components/pending-approval-columns.tsx | 211 ++++++++++++++ .../components/pending-approval-table.tsx | 271 ++++++++++++++++++ app/(admin)/admin/content/audio/page.tsx | 63 ++-- .../document/components/document-tabs.tsx | 53 ++++ .../components/pending-approval-columns.tsx | 211 ++++++++++++++ .../components/pending-approval-table.tsx | 255 ++++++++++++++++ app/(admin)/admin/content/document/page.tsx | 63 ++-- .../content/image/components/image-tabs.tsx | 53 ++++ .../components/pending-approval-columns.tsx | 217 ++++++++++++++ .../components/pending-approval-table.tsx | 264 +++++++++++++++++ .../content/image/components/table-image.tsx | 15 +- app/(admin)/admin/content/image/page.tsx | 68 +++-- .../form/content/image/image-detail-form.tsx | 221 ++++++++++---- components/table/table-pagination.tsx | 18 +- components/ui/tabs.tsx | 55 ++++ package-lock.json | 88 ++++++ package.json | 1 + service/content/content.ts | 121 ++++++++ 23 files changed, 2710 insertions(+), 195 deletions(-) create mode 100644 app/(admin)/admin/content/audio-visual/components/audio-visual-tabs.tsx create mode 100644 app/(admin)/admin/content/audio-visual/components/pending-approval-columns.tsx create mode 100644 app/(admin)/admin/content/audio-visual/components/pending-approval-table.tsx create mode 100644 app/(admin)/admin/content/audio/components/audio-tabs.tsx create mode 100644 app/(admin)/admin/content/audio/components/pending-approval-columns.tsx create mode 100644 app/(admin)/admin/content/audio/components/pending-approval-table.tsx create mode 100644 app/(admin)/admin/content/document/components/document-tabs.tsx create mode 100644 app/(admin)/admin/content/document/components/pending-approval-columns.tsx create mode 100644 app/(admin)/admin/content/document/components/pending-approval-table.tsx create mode 100644 app/(admin)/admin/content/image/components/image-tabs.tsx create mode 100644 app/(admin)/admin/content/image/components/pending-approval-columns.tsx create mode 100644 app/(admin)/admin/content/image/components/pending-approval-table.tsx create mode 100644 components/ui/tabs.tsx diff --git a/app/(admin)/admin/content/audio-visual/components/audio-visual-tabs.tsx b/app/(admin)/admin/content/audio-visual/components/audio-visual-tabs.tsx new file mode 100644 index 0000000..b748ae2 --- /dev/null +++ b/app/(admin)/admin/content/audio-visual/components/audio-visual-tabs.tsx @@ -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 ( +
+ +
+ + +
+
+ Submitted Audio-Visual +
+
+ +
+
+ Waiting Approval +
+
+
+
+ + +
+ +
+
+ + +
+ +
+
+
+
+ ); +}; + +export default AudioVisualTabs; diff --git a/app/(admin)/admin/content/audio-visual/components/pending-approval-columns.tsx b/app/(admin)/admin/content/audio-visual/components/pending-approval-columns.tsx new file mode 100644 index 0000000..43ea171 --- /dev/null +++ b/app/(admin)/admin/content/audio-visual/components/pending-approval-columns.tsx @@ -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[] = [ + { + accessorKey: "id", + header: "No", + cell: ({ row, table }) => { + const pageIndex = table.getState().pagination.pageIndex; + const pageSize = table.getState().pagination.pageSize; + const rowIndex = row.index; + return ( +
+
+

+ {pageIndex * pageSize + rowIndex + 1} +

+
+
+ ); + }, + }, + { + accessorKey: "title", + header: "Title", + cell: ({ row }: { row: { getValue: (key: string) => string } }) => { + const title: string = row.getValue("title"); + return ( + + {title.length > 50 ? `${title.slice(0, 30)}...` : title} + + ); + }, + }, + { + accessorKey: "categoryName", + header: "Category", + cell: ({ row }) => { + const categoryName = row.getValue("categoryName") as string; + return ( + + {categoryName || "-"} + + ); + }, + }, + { + accessorKey: "authorName", + header: "Author", + cell: ({ row }) => ( + + {row.getValue("authorName")} + + ), + }, + { + 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 {formattedDate}; + }, + }, + { + accessorKey: "priority", + header: "Priority", + cell: ({ row }) => { + const priority = row.getValue("priority") as string; + const colors: Record = { + 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 ( + + {priority?.toUpperCase() || "N/A"} + + ); + }, + }, + { + 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 ( +
+
+
+
+ + {currentStep}/{totalSteps} + +
+ ); + }, + }, + { + 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 ( +
+ + + {days} days + +
+ ); + }, + }, + { + accessorKey: "estimatedTime", + header: "Est. Time", + cell: ({ row }) => ( + + {row.getValue("estimatedTime") || "-"} + + ), + }, + { + id: "actions", + accessorKey: "action", + header: "Action", + enableHiding: false, + cell: ({ row }) => { + const canApprove = row.original.canApprove; + + return ( + + + + + + + + + View + + + {canApprove && ( + { + // Handle approval logic here + console.log("Approve item:", row.original.id); + }} + > + + Approve + + )} + + + ); + }, + }, + ]; + + return columns; +}; + +export default usePendingApprovalColumns; diff --git a/app/(admin)/admin/content/audio-visual/components/pending-approval-table.tsx b/app/(admin)/admin/content/audio-visual/components/pending-approval-table.tsx new file mode 100644 index 0000000..c202289 --- /dev/null +++ b/app/(admin)/admin/content/audio-visual/components/pending-approval-table.tsx @@ -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([]); + const [totalData, setTotalData] = React.useState(0); + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState([]); + const [columnVisibility, setColumnVisibility] = React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + const [showData, setShowData] = React.useState("10"); + const [pagination, setPagination] = React.useState({ + 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) => { + setSearch(e.target.value); + }; + + return ( +
+
+
+ + +
+
+
+
+ + + + + + + + 10 Data + + + 50 Data + + + 100 Data + + + 250 Data + + + + +
+
+
+ + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No pending approval data found. + + + )} + +
+ +
+ ); +}; + +export default PendingApprovalTable; diff --git a/app/(admin)/admin/content/audio-visual/page.tsx b/app/(admin)/admin/content/audio-visual/page.tsx index 62965a9..91d7e58 100644 --- a/app/(admin)/admin/content/audio-visual/page.tsx +++ b/app/(admin)/admin/content/audio-visual/page.tsx @@ -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 ( -
+
{/* */} -
- - - -
-
- Audio Visual +
+
+ + + +
+
+
+ +
+
+

Audio-Visual Management

+

Manage your submitted audio-visual files and pending approvals

+
+
+
+ + + +
-
- - - - {/* - - */} -
-
- - - - - - + + + + + + +
); }; -export default ReactTableImagePage; +export default ReactTableAudioVisualPage; \ No newline at end of file diff --git a/app/(admin)/admin/content/audio/components/audio-tabs.tsx b/app/(admin)/admin/content/audio/components/audio-tabs.tsx new file mode 100644 index 0000000..4fe7595 --- /dev/null +++ b/app/(admin)/admin/content/audio/components/audio-tabs.tsx @@ -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 ( +
+ +
+ + +
+
+ Submitted Audio +
+
+ +
+
+ Waiting Approval +
+
+
+
+ + +
+ +
+
+ + +
+ +
+
+
+
+ ); +}; + +export default AudioTabs; diff --git a/app/(admin)/admin/content/audio/components/pending-approval-columns.tsx b/app/(admin)/admin/content/audio/components/pending-approval-columns.tsx new file mode 100644 index 0000000..7f66720 --- /dev/null +++ b/app/(admin)/admin/content/audio/components/pending-approval-columns.tsx @@ -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[] = [ + { + accessorKey: "id", + header: "No", + cell: ({ row, table }) => { + const pageIndex = table.getState().pagination.pageIndex; + const pageSize = table.getState().pagination.pageSize; + const rowIndex = row.index; + return ( +
+
+

+ {pageIndex * pageSize + rowIndex + 1} +

+
+
+ ); + }, + }, + { + accessorKey: "title", + header: "Title", + cell: ({ row }: { row: { getValue: (key: string) => string } }) => { + const title: string = row.getValue("title"); + return ( + + {title.length > 50 ? `${title.slice(0, 30)}...` : title} + + ); + }, + }, + { + accessorKey: "categoryName", + header: "Category", + cell: ({ row }) => { + const categoryName = row.getValue("categoryName") as string; + return ( + + {categoryName || "-"} + + ); + }, + }, + { + accessorKey: "authorName", + header: "Author", + cell: ({ row }) => ( + + {row.getValue("authorName")} + + ), + }, + { + 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 {formattedDate}; + }, + }, + { + accessorKey: "priority", + header: "Priority", + cell: ({ row }) => { + const priority = row.getValue("priority") as string; + const colors: Record = { + 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 ( + + {priority?.toUpperCase() || "N/A"} + + ); + }, + }, + { + 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 ( +
+
+
+
+ + {currentStep}/{totalSteps} + +
+ ); + }, + }, + { + 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 ( +
+ + + {days} days + +
+ ); + }, + }, + { + accessorKey: "estimatedTime", + header: "Est. Time", + cell: ({ row }) => ( + + {row.getValue("estimatedTime") || "-"} + + ), + }, + { + id: "actions", + accessorKey: "action", + header: "Action", + enableHiding: false, + cell: ({ row }) => { + const canApprove = row.original.canApprove; + + return ( + + + + + + + + + View + + + {canApprove && ( + { + // Handle approval logic here + console.log("Approve item:", row.original.id); + }} + > + + Approve + + )} + + + ); + }, + }, + ]; + + return columns; +}; + +export default usePendingApprovalColumns; diff --git a/app/(admin)/admin/content/audio/components/pending-approval-table.tsx b/app/(admin)/admin/content/audio/components/pending-approval-table.tsx new file mode 100644 index 0000000..c202289 --- /dev/null +++ b/app/(admin)/admin/content/audio/components/pending-approval-table.tsx @@ -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([]); + const [totalData, setTotalData] = React.useState(0); + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState([]); + const [columnVisibility, setColumnVisibility] = React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + const [showData, setShowData] = React.useState("10"); + const [pagination, setPagination] = React.useState({ + 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) => { + setSearch(e.target.value); + }; + + return ( +
+
+
+ + +
+
+
+
+ + + + + + + + 10 Data + + + 50 Data + + + 100 Data + + + 250 Data + + + + +
+
+
+ + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No pending approval data found. + + + )} + +
+ +
+ ); +}; + +export default PendingApprovalTable; diff --git a/app/(admin)/admin/content/audio/page.tsx b/app/(admin)/admin/content/audio/page.tsx index d7115a7..1478e60 100644 --- a/app/(admin)/admin/content/audio/page.tsx +++ b/app/(admin)/admin/content/audio/page.tsx @@ -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 ( -
+
{/* */} -
- - - -
-
- Audio +
+
+ + + +
+
+
+ +
+
+

Audio Management

+

Manage your submitted audio files and pending approvals

+
+
+
+ + + +
-
- - - - {/* */} -
-
- - - - - - + + + + + + +
); }; -export default ReactTableAudioPage; +export default ReactTableAudioPage; \ No newline at end of file diff --git a/app/(admin)/admin/content/document/components/document-tabs.tsx b/app/(admin)/admin/content/document/components/document-tabs.tsx new file mode 100644 index 0000000..7774889 --- /dev/null +++ b/app/(admin)/admin/content/document/components/document-tabs.tsx @@ -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 ( +
+ +
+ + +
+
+ Submitted Documents +
+
+ +
+
+ Waiting Approval +
+
+
+
+ + +
+ +
+
+ + +
+ +
+
+
+
+ ); +}; + +export default DocumentTabs; diff --git a/app/(admin)/admin/content/document/components/pending-approval-columns.tsx b/app/(admin)/admin/content/document/components/pending-approval-columns.tsx new file mode 100644 index 0000000..8362ba4 --- /dev/null +++ b/app/(admin)/admin/content/document/components/pending-approval-columns.tsx @@ -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[] = [ + { + accessorKey: "id", + header: "No", + cell: ({ row, table }) => { + const pageIndex = table.getState().pagination.pageIndex; + const pageSize = table.getState().pagination.pageSize; + const rowIndex = row.index; + return ( +
+
+

+ {pageIndex * pageSize + rowIndex + 1} +

+
+
+ ); + }, + }, + { + accessorKey: "title", + header: "Title", + cell: ({ row }: { row: { getValue: (key: string) => string } }) => { + const title: string = row.getValue("title"); + return ( + + {title.length > 50 ? `${title.slice(0, 30)}...` : title} + + ); + }, + }, + { + accessorKey: "categoryName", + header: "Category", + cell: ({ row }) => { + const categoryName = row.getValue("categoryName") as string; + return ( + + {categoryName || "-"} + + ); + }, + }, + { + accessorKey: "authorName", + header: "Author", + cell: ({ row }) => ( + + {row.getValue("authorName")} + + ), + }, + { + 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 {formattedDate}; + }, + }, + { + accessorKey: "priority", + header: "Priority", + cell: ({ row }) => { + const priority = row.getValue("priority") as string; + const colors: Record = { + 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 ( + + {priority?.toUpperCase() || "N/A"} + + ); + }, + }, + { + 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 ( +
+
+
+
+ + {currentStep}/{totalSteps} + +
+ ); + }, + }, + { + 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 ( +
+ + + {days} days + +
+ ); + }, + }, + { + accessorKey: "estimatedTime", + header: "Est. Time", + cell: ({ row }) => ( + + {row.getValue("estimatedTime") || "-"} + + ), + }, + { + id: "actions", + accessorKey: "action", + header: "Action", + enableHiding: false, + cell: ({ row }) => { + const canApprove = row.original.canApprove; + + return ( + + + + + + + + + View + + + {canApprove && ( + { + // Handle approval logic here + console.log("Approve item:", row.original.id); + }} + > + + Approve + + )} + + + ); + }, + }, + ]; + + return columns; +}; + +export default usePendingApprovalColumns; diff --git a/app/(admin)/admin/content/document/components/pending-approval-table.tsx b/app/(admin)/admin/content/document/components/pending-approval-table.tsx new file mode 100644 index 0000000..21102c5 --- /dev/null +++ b/app/(admin)/admin/content/document/components/pending-approval-table.tsx @@ -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([]); + const [totalData, setTotalData] = React.useState(0); + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState([]); + const [columnVisibility, setColumnVisibility] = React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + const [showData, setShowData] = React.useState("10"); + const [pagination, setPagination] = React.useState({ + 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) => { + setSearch(e.target.value); + }; + + return ( +
+
+
+ + +
+
+
+
+ + + + + + + + 10 Data + + + 50 Data + + + 100 Data + + + 250 Data + + + + +
+
+
+ + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No pending approval data found. + + + )} + +
+ +
+ ); +}; + +export default PendingApprovalTable; diff --git a/app/(admin)/admin/content/document/page.tsx b/app/(admin)/admin/content/document/page.tsx index 91b0d4f..57a75bd 100644 --- a/app/(admin)/admin/content/document/page.tsx +++ b/app/(admin)/admin/content/document/page.tsx @@ -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 ( -
-
- - - -
-
- Text +
+ {/* */} +
+
+ + + +
+
+
+ +
+
+

Document Management

+

Manage your submitted documents and pending approvals

+
+
+
+ + + +
- -
- - - -
-
- - - - - - + + + + + + +
); }; -export default ReactTableTeksPage; +export default ReactTableDocumentPage; diff --git a/app/(admin)/admin/content/image/components/image-tabs.tsx b/app/(admin)/admin/content/image/components/image-tabs.tsx new file mode 100644 index 0000000..87605aa --- /dev/null +++ b/app/(admin)/admin/content/image/components/image-tabs.tsx @@ -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 ( +
+ +
+ + +
+
+ Waiting Approval +
+
+ +
+
+ Submitted Images +
+
+
+
+ + +
+ +
+
+ + +
+ +
+
+
+
+ ); +}; + +export default ImageTabs; \ No newline at end of file diff --git a/app/(admin)/admin/content/image/components/pending-approval-columns.tsx b/app/(admin)/admin/content/image/components/pending-approval-columns.tsx new file mode 100644 index 0000000..9ebe593 --- /dev/null +++ b/app/(admin)/admin/content/image/components/pending-approval-columns.tsx @@ -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[] = [ + { + 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 ( +
+
+

+ {pageIndex * pageSize + rowIndex + 1} +

+
+
+ ); + }, + }, + { + accessorKey: "title", + header: "Title", + cell: ({ row }: { row: { getValue: (key: string) => string } }) => { + const title: string = row.getValue("title"); + return ( + + {title.length > 50 ? `${title.slice(0, 30)}...` : title} + + ); + }, + }, + { + accessorKey: "categoryName", + header: "Category", + cell: ({ row }) => { + const categoryName = row.getValue("categoryName") as string; + return ( + + {categoryName || "-"} + + ); + }, + }, + { + accessorKey: "authorName", + header: "Author", + cell: ({ row }) => ( + + {row.getValue("authorName")} + + ), + }, + { + 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 {formattedDate}; + }, + }, + { + accessorKey: "priority", + header: "Priority", + cell: ({ row }) => { + const priority = row.getValue("priority") as string; + const colors: Record = { + 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 ( + + {priority?.toUpperCase() || "N/A"} + + ); + }, + }, + { + 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 ( +
+
+
+
+ + {currentStep}/{totalSteps} + +
+ ); + }, + }, + { + 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 ( +
+ + + {days} days + +
+ ); + }, + }, + { + accessorKey: "estimatedTime", + header: "Est. Time", + cell: ({ row }) => ( + + {row.getValue("estimatedTime") || "-"} + + ), + }, + { + id: "actions", + accessorKey: "action", + header: "Action", + enableHiding: false, + cell: ({ row }) => { + const canApprove = row.original.canApprove; + + return ( + + + + + + + + + View + + + {canApprove && ( + { + // Handle approval logic here + console.log("Approve item:", row.original.id); + }} + > + + Approve + + )} + + + ); + }, + }, + ]; + + return columns; +}; + +export default usePendingApprovalColumns; diff --git a/app/(admin)/admin/content/image/components/pending-approval-table.tsx b/app/(admin)/admin/content/image/components/pending-approval-table.tsx new file mode 100644 index 0000000..3d9b052 --- /dev/null +++ b/app/(admin)/admin/content/image/components/pending-approval-table.tsx @@ -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([]); + const [totalData, setTotalData] = React.useState(0); + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState([]); + const [columnVisibility, setColumnVisibility] = React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + const [showData, setShowData] = React.useState("10"); + const [pagination, setPagination] = React.useState({ + 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) => { + setSearch(e.target.value); + }; + + return ( +
+
+
+ + +
+
+
+
+ + + + + + + + 10 Data + + + 50 Data + + + 100 Data + + + 250 Data + + + + +
+
+
+ + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No pending approval data found. + + + )} + +
+ +
+ ); +}; + +export default PendingApprovalTable; diff --git a/app/(admin)/admin/content/image/components/table-image.tsx b/app/(admin)/admin/content/image/components/table-image.tsx index 5fd7a56..5f42118 100644 --- a/app/(admin)/admin/content/image/components/table-image.tsx +++ b/app/(admin)/admin/content/image/components/table-image.tsx @@ -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); diff --git a/app/(admin)/admin/content/image/page.tsx b/app/(admin)/admin/content/image/page.tsx index 53194ec..6c4c765 100644 --- a/app/(admin)/admin/content/image/page.tsx +++ b/app/(admin)/admin/content/image/page.tsx @@ -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 ( -
+
{/* */} -
- - - -
-
- Image +
+
+ + + +
+
+
+ +
+
+

Image Management

+

Manage your submitted images and pending approvals

+
+
+
+ + + + {/* + + */} +
-
- - - - {/* - - */} -
-
- - - - - - + + + + + + +
); diff --git a/components/form/content/image/image-detail-form.tsx b/components/form/content/image/image-detail-form.tsx index 82560b4..fc7f322 100644 --- a/components/form/content/image/image-detail-form.tsx +++ b/components/form/content/image/image-detail-form.tsx @@ -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() { )} /> @@ -603,7 +704,7 @@ export default function FormImageDetail() { Thumbnail Gambar Utama @@ -670,7 +771,7 @@ export default function FormImageDetail() { />

Information:

-

{detail?.statusName}

+

{detail?.statusName || getStatusName(detail?.statusId || 0)}

Komentar

{approval?.message}

@@ -741,7 +842,7 @@ export default function FormImageDetail() {

{`files-${index handleImageLoad(e, index)} className={`h-[100px] object-cover ${ @@ -963,11 +1064,11 @@ export default function FormImageDetail() { - {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) ? ( "" ) : (
diff --git a/components/table/table-pagination.tsx b/components/table/table-pagination.tsx index 06593f5..ff32167 100644 --- a/components/table/table-pagination.tsx +++ b/components/table/table-pagination.tsx @@ -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 = () => { diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx new file mode 100644 index 0000000..c1030c2 --- /dev/null +++ b/components/ui/tabs.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/package-lock.json b/package-lock.json index 7aa83b7..40e9afa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index eb4e186..8f43152 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/service/content/content.ts b/service/content/content.ts index 3b2485c..90a54eb 100644 --- a/service/content/content.ts +++ b/service/content/content.ts @@ -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); } \ No newline at end of file