pull main
This commit is contained in:
commit
9fa3148d48
|
|
@ -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";
|
"use client";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
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 { UploadIcon } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Icon } from "@iconify/react/dist/iconify.js";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
|
||||||
import TableVideo from "./components/table-video";
|
|
||||||
|
|
||||||
const ReactTableImagePage = () => {
|
const ReactTableAudioVisualPage = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="min-h-screen bg-gray-50">
|
||||||
{/* <SiteBreadcrumb /> */}
|
{/* <SiteBreadcrumb /> */}
|
||||||
<div className="space-y-4">
|
<div className="p-6">
|
||||||
<Card className="m-2">
|
<div className="max-w-7xl mx-auto">
|
||||||
<CardHeader className="border-b border-solid border-default-200 mb-6">
|
<Card className="shadow-sm border-0">
|
||||||
<CardTitle>
|
<CardHeader className="border-b border-gray-200 bg-white rounded-t-lg">
|
||||||
<div className="flex items-center">
|
<CardTitle>
|
||||||
<div className="flex-1 text-xl font-medium text-default-900">
|
<div className="flex items-center justify-between">
|
||||||
Audio Visual
|
<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>
|
||||||
<div className="flex-none">
|
</CardTitle>
|
||||||
<Link href={"/admin/content/audio-visual/create"}>
|
</CardHeader>
|
||||||
<Button color="primary" className="text-white">
|
<CardContent className="p-6 bg-gray-50">
|
||||||
<UploadIcon size={18} className="mr-2" />
|
<AudioVisualTabs />
|
||||||
Audio Visual
|
</CardContent>
|
||||||
</Button>
|
</Card>
|
||||||
</Link>
|
</div>
|
||||||
{/* <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>
|
|
||||||
</div>
|
</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";
|
"use client";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||||
|
import AudioTabs from "./components/audio-tabs";
|
||||||
import { UploadIcon } from "lucide-react";
|
import { UploadIcon } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
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";
|
import Link from "next/link";
|
||||||
|
|
||||||
const ReactTableAudioPage = () => {
|
const ReactTableAudioPage = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="min-h-screen bg-gray-50">
|
||||||
{/* <SiteBreadcrumb /> */}
|
{/* <SiteBreadcrumb /> */}
|
||||||
<div className="space-y-4 m-3">
|
<div className="p-6">
|
||||||
<Card>
|
<div className="max-w-7xl mx-auto">
|
||||||
<CardHeader className="border-b border-solid border-default-200 mb-6">
|
<Card className="shadow-sm border-0">
|
||||||
<CardTitle>
|
<CardHeader className="border-b border-gray-200 bg-white rounded-t-lg">
|
||||||
<div className="flex items-center">
|
<CardTitle>
|
||||||
<div className="flex-1 text-xl font-medium text-default-900">
|
<div className="flex items-center justify-between">
|
||||||
Audio
|
<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>
|
||||||
<div className="flex-none">
|
</CardTitle>
|
||||||
<Link href={"/admin/content/audio/create"}>
|
</CardHeader>
|
||||||
<Button color="primary" className="text-white">
|
<CardContent className="p-6 bg-gray-50">
|
||||||
<UploadIcon size={18} className="mr-2" />
|
<AudioTabs />
|
||||||
Create Audio
|
</CardContent>
|
||||||
</Button>
|
</Card>
|
||||||
</Link>
|
</div>
|
||||||
{/* <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>
|
|
||||||
</div>
|
</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";
|
"use client";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
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 { UploadIcon } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import TableTeks from "./components/table-teks";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
const ReactTableTeksPage = () => {
|
const ReactTableDocumentPage = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="min-h-screen bg-gray-50">
|
||||||
<div className="space-y-4 m-3">
|
{/* <SiteBreadcrumb /> */}
|
||||||
<Card>
|
<div className="p-6">
|
||||||
<CardHeader className="border-b border-solid border-default-200 mb-6">
|
<div className="max-w-7xl mx-auto">
|
||||||
<CardTitle>
|
<Card className="shadow-sm border-0">
|
||||||
<div className="flex items-center">
|
<CardHeader className="border-b border-gray-200 bg-white rounded-t-lg">
|
||||||
<div className="flex-1 text-xl font-medium text-default-900">
|
<CardTitle>
|
||||||
Text
|
<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>
|
||||||
|
</CardTitle>
|
||||||
<div className="flex-none">
|
</CardHeader>
|
||||||
<Link href={"/admin/content/document/create"}>
|
<CardContent className="p-6 bg-gray-50">
|
||||||
<Button color="primary" className="text-white">
|
<DocumentTabs />
|
||||||
<UploadIcon size={18} className="mr-2" />
|
</CardContent>
|
||||||
Create Text
|
</Card>
|
||||||
</Button>
|
</div>
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-0">
|
|
||||||
<TableTeks />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</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;
|
||||||
|
|
@ -237,9 +237,8 @@ const TableImage = () => {
|
||||||
totalPage: Number(showData),
|
totalPage: Number(showData),
|
||||||
title: search || undefined,
|
title: search || undefined,
|
||||||
categoryId: categoryFilter ? Number(categoryFilter) : undefined,
|
categoryId: categoryFilter ? Number(categoryFilter) : undefined,
|
||||||
typeId: 1, // image content type
|
typeId: 1, // image content typeoriginalRows
|
||||||
statusId:
|
statusId: statusFilter?.length > 0 ? Number(statusFilter[0]) : undefined,
|
||||||
statusFilter?.length > 0 ? Number(statusFilter[0]) : undefined,
|
|
||||||
startDate: formattedStartDate || undefined,
|
startDate: formattedStartDate || undefined,
|
||||||
endDate: formattedEndDate || undefined,
|
endDate: formattedEndDate || undefined,
|
||||||
};
|
};
|
||||||
|
|
@ -248,25 +247,12 @@ const TableImage = () => {
|
||||||
const data = res?.data?.data;
|
const data = res?.data?.data;
|
||||||
|
|
||||||
if (Array.isArray(data)) {
|
if (Array.isArray(data)) {
|
||||||
// ✅ aman karena data array
|
data.forEach((item: any, index: number) => {
|
||||||
const processed = data.map((item: any, index: number) => ({
|
item.no = (page - 1) * Number(showData) + index + 1;
|
||||||
...item,
|
});
|
||||||
no: (page - 1) * Number(showData) + index + 1,
|
setDataTable(data);
|
||||||
}));
|
setTotalData(res?.data.meta.count);
|
||||||
setDataTable(processed);
|
setTotalPage(Math.ceil(res?.data.meta.count / Number(showData)));
|
||||||
setTotalData(data.length);
|
|
||||||
setTotalPage(Math.ceil(data.length / Number(showData)));
|
|
||||||
} else {
|
|
||||||
// ✅ fallback kalau masih pakai struktur lama
|
|
||||||
const contentData = Array.isArray(data?.content) ? data.content : [];
|
|
||||||
const processed = contentData.map((item: any, index: number) => ({
|
|
||||||
...item,
|
|
||||||
no: (page - 1) * Number(showData) + index + 1,
|
|
||||||
}));
|
|
||||||
|
|
||||||
setDataTable(processed);
|
|
||||||
setTotalData(data?.totalElements ?? 0);
|
|
||||||
setTotalPage(data?.totalPages ?? 1);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error fetching tasks:", err);
|
console.error("Error fetching tasks:", err);
|
||||||
|
|
|
||||||
|
|
@ -1,44 +1,52 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
import SiteBreadcrumb from "@/components/site-breadcrumb";
|
||||||
import TableImage from "./components/table-image";
|
import ImageTabs from "./components/image-tabs";
|
||||||
import { UploadIcon } from "lucide-react";
|
import { UploadIcon } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
const ReactTableImagePage = () => {
|
const ReactTableImagePage = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="min-h-screen bg-gray-50">
|
||||||
{/* <SiteBreadcrumb /> */}
|
{/* <SiteBreadcrumb /> */}
|
||||||
<div className="space-y-4 m-3">
|
<div className="p-6">
|
||||||
<Card>
|
<div className="max-w-7xl mx-auto">
|
||||||
<CardHeader className="border-b border-solid border-default-200 mb-6">
|
<Card className="shadow-sm border-0">
|
||||||
<CardTitle>
|
<CardHeader className="border-b border-gray-200 bg-white rounded-t-lg">
|
||||||
<div className="flex items-center">
|
<CardTitle>
|
||||||
<div className="flex-1 text-xl font-medium text-default-900">
|
<div className="flex items-center justify-between">
|
||||||
Image
|
<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>
|
||||||
<div className="flex-none">
|
</CardTitle>
|
||||||
<Link href={"/admin/content/image/create"}>
|
</CardHeader>
|
||||||
<Button color="primary" className="text-white">
|
<CardContent className="p-6 bg-gray-50">
|
||||||
<UploadIcon size={18} className="mr-2" />
|
<ImageTabs />
|
||||||
Create Image
|
</CardContent>
|
||||||
</Button>
|
</Card>
|
||||||
</Link>
|
</div>
|
||||||
{/* <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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,10 @@ import {
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
||||||
|
|
||||||
import { register } from "module";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import {
|
import {
|
||||||
createMedia,
|
createMedia,
|
||||||
|
getArticleDetail,
|
||||||
getTagsBySubCategoryId,
|
getTagsBySubCategoryId,
|
||||||
listEnableCategory,
|
listEnableCategory,
|
||||||
publishMedia,
|
publishMedia,
|
||||||
|
|
@ -32,7 +29,6 @@ import {
|
||||||
submitApproval,
|
submitApproval,
|
||||||
} from "@/service/content/content";
|
} from "@/service/content/content";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { MailIcon } from "lucide-react";
|
|
||||||
import { Swiper, SwiperSlide } from "swiper/react";
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
import "swiper/css";
|
import "swiper/css";
|
||||||
import "swiper/css/free-mode";
|
import "swiper/css/free-mode";
|
||||||
|
|
@ -84,26 +80,81 @@ type FileType = {
|
||||||
url: string;
|
url: string;
|
||||||
thumbnailFileUrl: string;
|
thumbnailFileUrl: string;
|
||||||
fileName: 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 = {
|
type Detail = {
|
||||||
id: string;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
htmlDescription: string;
|
||||||
slug: 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;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
categoryName: string;
|
creatorName?: string;
|
||||||
creatorName: string;
|
thumbnailLink?: string;
|
||||||
thumbnailLink: string;
|
statusName?: string;
|
||||||
tags: string;
|
needApprovalFromLevel?: number;
|
||||||
statusName: string;
|
uploadedById?: number;
|
||||||
isPublish: boolean;
|
|
||||||
needApprovalFromLevel: number;
|
|
||||||
files: FileType[];
|
|
||||||
uploadedById: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ViewEditor = dynamic(
|
const ViewEditor = dynamic(
|
||||||
|
|
@ -203,7 +254,7 @@ export default function FormImageDetail() {
|
||||||
console.log("data category", resCategory);
|
console.log("data category", resCategory);
|
||||||
|
|
||||||
if (scheduleId && scheduleType === "3") {
|
if (scheduleId && scheduleType === "3") {
|
||||||
const findCategory = resCategory.find((o) =>
|
const findCategory = resCategory?.find((o) =>
|
||||||
o.name.toLowerCase().includes("pers rilis")
|
o.name.toLowerCase().includes("pers rilis")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -269,58 +320,86 @@ export default function FormImageDetail() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function initState() {
|
async function initState() {
|
||||||
if (id) {
|
if (id) {
|
||||||
const response = await detailMedia(id);
|
try {
|
||||||
const details = response?.data;
|
const response = await getArticleDetail(Number(id));
|
||||||
console.log("detail", details);
|
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 || []
|
||||||
|
};
|
||||||
|
|
||||||
// Set detail untuk ditampilkan
|
// Map files from new API structure to expected format
|
||||||
setDetail(details);
|
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
|
||||||
|
}));
|
||||||
|
|
||||||
// Files
|
setFiles(mappedFiles);
|
||||||
setFiles(details?.files);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
// Ambil file pertama sebagai "main"
|
// Set the selected target to the category ID from details
|
||||||
if (details?.files && details.files.length > 0) {
|
setSelectedTarget(String(mappedDetail.categoryId));
|
||||||
setMain({
|
|
||||||
type: "image", // atau mapping sendiri kalau ada typeId
|
|
||||||
url: details.files[0].file_url,
|
|
||||||
names: details.files[0].file_name,
|
|
||||||
format: details.files[0].file_name.split(".").pop(), // ambil ekstensi
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setupPlacementCheck(details?.files?.length ?? 0);
|
const fileUrls = mappedFiles.map((file: any) =>
|
||||||
|
file.thumbnailFileUrl || file.url || mappedDetail.thumbnailUrl || "default-image.jpg"
|
||||||
// Kalau ada publishedForObject
|
|
||||||
if (details?.publishedForObject) {
|
|
||||||
const publisherIds = details.publishedForObject.map(
|
|
||||||
(obj: any) => obj.id
|
|
||||||
);
|
);
|
||||||
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 target category
|
|
||||||
if (details?.categories && details.categories.length > 0) {
|
|
||||||
setSelectedTarget(String(details.categories[0].id));
|
|
||||||
} else if (details?.categoryId) {
|
|
||||||
setSelectedTarget(String(details.categoryId));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Thumbnails
|
|
||||||
const filesData = details?.files || [];
|
|
||||||
const fileUrls = filesData.map((file: any) =>
|
|
||||||
file.file_thumbnail ? file.file_thumbnail : file.file_url
|
|
||||||
);
|
|
||||||
setDetailThumb(fileUrls);
|
|
||||||
|
|
||||||
// Ambil approval
|
|
||||||
const approvals = await getDataApprovalByMediaUpload(details?.id);
|
|
||||||
setApproval(approvals?.data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
initState();
|
initState();
|
||||||
}, [refresh, setValue]);
|
}, [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 actionApproval = (e: string) => {
|
||||||
const temp = [];
|
const temp = [];
|
||||||
for (const element of detail.files) {
|
for (const element of detail.files) {
|
||||||
|
|
@ -533,7 +612,7 @@ export default function FormImageDetail() {
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
disabled
|
disabled
|
||||||
value={String(detail?.category?.id)}
|
value={String(detail?.categoryId || detail?.category?.id)}
|
||||||
// onValueChange={(id) => {
|
// onValueChange={(id) => {
|
||||||
// console.log("Selected Category:", id);
|
// console.log("Selected Category:", id);
|
||||||
// setSelectedTarget(id);
|
// setSelectedTarget(id);
|
||||||
|
|
@ -545,25 +624,23 @@ export default function FormImageDetail() {
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{/* Show the category from details if it doesn't exist in categories list */}
|
{/* Show the category from details if it doesn't exist in categories list */}
|
||||||
{detail &&
|
{detail &&
|
||||||
Array.isArray(categories) &&
|
!categories?.find(
|
||||||
!categories.find(
|
|
||||||
(cat) =>
|
(cat) =>
|
||||||
String(cat.id) === String(detail.category.id)
|
String(cat.id) === String(detail.categoryId || detail?.category?.id)
|
||||||
) && (
|
) && (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={String(detail.category.id)}
|
key={String(detail.categoryId || detail?.category?.id)}
|
||||||
value={String(detail.category.id)}
|
value={String(detail.categoryId || detail?.category?.id)}
|
||||||
>
|
>
|
||||||
{detail.category.name}
|
{detail.categoryName || detail?.category?.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
)}
|
)}
|
||||||
|
{categories?.map((category) => (
|
||||||
{categories?.map((cat) => (
|
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={String(cat.id)}
|
key={String(category.id)}
|
||||||
value={String(cat.id)}
|
value={String(category.id)}
|
||||||
>
|
>
|
||||||
{cat.name}
|
{category.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
|
|
@ -595,12 +672,12 @@ export default function FormImageDetail() {
|
||||||
navigation={false}
|
navigation={false}
|
||||||
className="h-[480px] object-cover w-full"
|
className="h-[480px] object-cover w-full"
|
||||||
>
|
>
|
||||||
{detailThumb?.map((data: any) => (
|
{detailThumb?.map((data: any, index: number) => (
|
||||||
<SwiperSlide key={data.id}>
|
<SwiperSlide key={index}>
|
||||||
<img
|
<img
|
||||||
className="h-[480px] max-w-[600px] rounded-md object-cover mx-auto border-2"
|
className="h-[480px] max-w-[600px] rounded-md object-cover mx-auto border-2"
|
||||||
src={data}
|
src={data}
|
||||||
alt={` ${data.id}`}
|
alt={`Image ${index + 1}`}
|
||||||
/>
|
/>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
|
|
@ -616,12 +693,12 @@ export default function FormImageDetail() {
|
||||||
modules={[Pagination, Thumbs]}
|
modules={[Pagination, Thumbs]}
|
||||||
// className="mySwiper2"
|
// className="mySwiper2"
|
||||||
>
|
>
|
||||||
{detailThumb?.map((data: any) => (
|
{detailThumb?.map((data: any, index: number) => (
|
||||||
<SwiperSlide key={data.id}>
|
<SwiperSlide key={index}>
|
||||||
<img
|
<img
|
||||||
className="object-cover h-[60px] w-[80px] border-2 border-slate-100"
|
className="object-cover h-[60px] w-[80px] border-2 border-slate-100"
|
||||||
src={data}
|
src={data}
|
||||||
alt={` ${data.id}`}
|
alt={`Thumbnail ${index + 1}`}
|
||||||
/>
|
/>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
|
|
@ -643,9 +720,9 @@ export default function FormImageDetail() {
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={detail?.creatorName}
|
value={detail?.createdByName || detail?.creatorName}
|
||||||
onChange={field.onChange}
|
onChange={field.onChange}
|
||||||
placeholder="Enter Title"
|
placeholder="Enter Creator Name"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
@ -660,7 +737,7 @@ export default function FormImageDetail() {
|
||||||
<Label>Preview</Label>
|
<Label>Preview</Label>
|
||||||
<Card className="mt-2 w-fit">
|
<Card className="mt-2 w-fit">
|
||||||
<img
|
<img
|
||||||
src={detail.thumbnailLink}
|
src={detail.thumbnailUrl || detail.thumbnailLink}
|
||||||
alt="Thumbnail Gambar Utama"
|
alt="Thumbnail Gambar Utama"
|
||||||
className="h-[200px] rounded"
|
className="h-[200px] rounded"
|
||||||
/>
|
/>
|
||||||
|
|
@ -727,7 +804,7 @@ export default function FormImageDetail() {
|
||||||
/>
|
/>
|
||||||
<div className="px-3 py-3 border mx-3">
|
<div className="px-3 py-3 border mx-3">
|
||||||
<p>Information:</p>
|
<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>Komentar</p>
|
||||||
<p>{approval?.message}</p>
|
<p>{approval?.message}</p>
|
||||||
<p className="text-right text-sm">
|
<p className="text-right text-sm">
|
||||||
|
|
@ -798,7 +875,7 @@ export default function FormImageDetail() {
|
||||||
<div className="w-[200px] h-[100px] flex justify-center items-center">
|
<div className="w-[200px] h-[100px] flex justify-center items-center">
|
||||||
<img
|
<img
|
||||||
key={index}
|
key={index}
|
||||||
alt={`files-${index + 1}`}
|
alt={file.fileAlt || `files-${index + 1}`}
|
||||||
src={file.url}
|
src={file.url}
|
||||||
onLoad={(e) => handleImageLoad(e, index)}
|
onLoad={(e) => handleImageLoad(e, index)}
|
||||||
className={`h-[100px] object-cover ${
|
className={`h-[100px] object-cover ${
|
||||||
|
|
@ -1020,11 +1097,11 @@ export default function FormImageDetail() {
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Card>
|
</Card>
|
||||||
{Number(detail?.needApprovalFromLevel) == Number(userLevelId) ||
|
{Number(detail?.needApprovalFromLevel || 0) == Number(userLevelId) ||
|
||||||
(detail?.isInternationalMedia == true &&
|
(detail?.isInternationalMedia == true &&
|
||||||
detail?.isForwardFromNational == true &&
|
detail?.isForwardFromNational == true &&
|
||||||
Number(detail?.statusId) == 1) ? (
|
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">
|
<div className="flex flex-col gap-2 p-3">
|
||||||
|
|
|
||||||
|
|
@ -35,10 +35,20 @@ const TablePagination = ({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const pageFromUrl = searchParams?.get("page");
|
const pageFromUrl = searchParams?.get("page");
|
||||||
|
console.log("pagination: pageFromUrl :", pageFromUrl);
|
||||||
|
|
||||||
if (pageFromUrl) {
|
if (pageFromUrl) {
|
||||||
const pageIndex = Math.min(Math.max(1, Number(pageFromUrl)), totalPage);
|
const pageIndex = Math.min(Math.max(1, Number(pageFromUrl)), totalPage);
|
||||||
setCurrentPageIndex(pageIndex);
|
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]);
|
}, [searchParams, totalPage, table]);
|
||||||
|
|
||||||
|
|
@ -47,9 +57,13 @@ const TablePagination = ({
|
||||||
const searchParams = new URLSearchParams(window.location.search);
|
const searchParams = new URLSearchParams(window.location.search);
|
||||||
searchParams.set("page", clampedPageIndex.toString());
|
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()}`);
|
router.push(`${window.location.pathname}?${searchParams.toString()}`);
|
||||||
setCurrentPageIndex(clampedPageIndex);
|
setCurrentPageIndex(clampedPageIndex);
|
||||||
table.setPageIndex(clampedPageIndex - 1); // Perbarui tabel dengan index berbasis 0
|
table.setPageIndex(clampedPageIndex - 1); // ✅ Perbarui tabel dengan index berbasis 0
|
||||||
};
|
};
|
||||||
|
|
||||||
const generatePageNumbers = () => {
|
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-select": "^2.2.5",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-visually-hidden": "^1.2.3",
|
"@radix-ui/react-visually-hidden": "^1.2.3",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@tinymce/tinymce-react": "^6.3.0",
|
"@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": {
|
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
"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-select": "^2.2.5",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
"@radix-ui/react-switch": "^1.2.5",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"@radix-ui/react-visually-hidden": "^1.2.3",
|
"@radix-ui/react-visually-hidden": "^1.2.3",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@tinymce/tinymce-react": "^6.3.0",
|
"@tinymce/tinymce-react": "^6.3.0",
|
||||||
|
|
|
||||||
|
|
@ -427,3 +427,124 @@ export async function listDataTeksNew(
|
||||||
endDate
|
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