feat: update all table, add pending approval table.
This commit is contained in:
parent
5fdcfdfdb9
commit
926bd78f53
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
Loading…
Reference in New Issue