2025-07-14 07:31:51 +00:00
|
|
|
"use client";
|
|
|
|
|
import {
|
|
|
|
|
BannerIcon,
|
|
|
|
|
CopyIcon,
|
|
|
|
|
CreateIconIon,
|
|
|
|
|
DeleteIcon,
|
|
|
|
|
DotsYIcon,
|
|
|
|
|
EyeIconMdi,
|
|
|
|
|
SearchIcon,
|
|
|
|
|
} from "@/components/icons";
|
|
|
|
|
import { close, error, loading, success, successToast } from "@/config/swal";
|
|
|
|
|
import { Article } from "@/types/globals";
|
|
|
|
|
import { convertDateFormat } from "@/utils/global";
|
|
|
|
|
import Link from "next/link";
|
|
|
|
|
import { Key, useCallback, useEffect, useState } from "react";
|
|
|
|
|
import Swal from "sweetalert2";
|
|
|
|
|
import withReactContent from "sweetalert2-react-content";
|
|
|
|
|
import Cookies from "js-cookie";
|
|
|
|
|
import {
|
|
|
|
|
deleteArticle,
|
|
|
|
|
getArticleByCategory,
|
|
|
|
|
updateIsBannerArticle,
|
|
|
|
|
} from "@/service/article";
|
|
|
|
|
import {
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
DropdownMenuItem,
|
|
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
} from "../ui/dropdown-menu";
|
|
|
|
|
import { Button } from "../ui/button";
|
|
|
|
|
import { Input } from "../ui/input";
|
|
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
} from "@/components/ui/select";
|
|
|
|
|
import {
|
|
|
|
|
Table,
|
|
|
|
|
TableHeader,
|
|
|
|
|
TableBody,
|
|
|
|
|
TableRow,
|
|
|
|
|
TableHead,
|
|
|
|
|
TableCell,
|
|
|
|
|
} from "@/components/ui/table";
|
|
|
|
|
import CustomPagination from "../layout/custom-pagination";
|
2025-11-18 06:56:39 +00:00
|
|
|
import { EditBannerDialog } from "../form/banner-edit-dialog";
|
|
|
|
|
import { deleteBanner, getBannerData, updateBanner } from "@/service/banner";
|
2026-01-18 17:01:09 +00:00
|
|
|
import { CheckCheck, Eye } from "lucide-react";
|
2026-01-19 11:25:14 +00:00
|
|
|
import { useRouter } from "next/navigation";
|
2025-07-14 07:31:51 +00:00
|
|
|
|
|
|
|
|
const columns = [
|
|
|
|
|
{ name: "No", uid: "no" },
|
|
|
|
|
{ name: "Judul", uid: "title" },
|
|
|
|
|
{ name: "Banner", uid: "isBanner" },
|
|
|
|
|
{ name: "Kategori", uid: "category" },
|
|
|
|
|
{ name: "Tanggal Unggah", uid: "createdAt" },
|
|
|
|
|
{ name: "Kreator", uid: "createdByName" },
|
|
|
|
|
{ name: "Status", uid: "isPublish" },
|
|
|
|
|
{ name: "Aksi", uid: "actions" },
|
|
|
|
|
];
|
|
|
|
|
const columnsOtherRole = [
|
|
|
|
|
{ name: "No", uid: "no" },
|
|
|
|
|
{ name: "Judul", uid: "title" },
|
|
|
|
|
{ name: "Kategori", uid: "category" },
|
|
|
|
|
{ name: "Tanggal Unggah", uid: "createdAt" },
|
|
|
|
|
{ name: "Kreator", uid: "createdByName" },
|
|
|
|
|
{ name: "Status", uid: "isPublish" },
|
|
|
|
|
{ name: "Aksi", uid: "actions" },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// interface Category {
|
|
|
|
|
// id: number;
|
|
|
|
|
// title: string;
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
export default function ArticleTable() {
|
|
|
|
|
const MySwal = withReactContent(Swal);
|
|
|
|
|
const username = Cookies.get("username");
|
|
|
|
|
const userId = Cookies.get("uie");
|
|
|
|
|
|
|
|
|
|
const [page, setPage] = useState(1);
|
|
|
|
|
const [totalPage, setTotalPage] = useState(1);
|
|
|
|
|
const [article, setArticle] = useState<any[]>([]);
|
|
|
|
|
const [showData, setShowData] = useState("10");
|
|
|
|
|
const [search, setSearch] = useState("");
|
|
|
|
|
const [categories, setCategories] = useState<any>([]);
|
|
|
|
|
const [selectedCategories, setSelectedCategories] = useState<any>("");
|
|
|
|
|
const [startDateValue, setStartDateValue] = useState({
|
|
|
|
|
startDate: null,
|
|
|
|
|
endDate: null,
|
|
|
|
|
});
|
2026-01-19 11:25:14 +00:00
|
|
|
const [userLevelId, setUserLevelId] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
|
|
|
|
|
// 🔹 Ambil userlevelId dari cookies
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const ulne = Cookies.get("ulne"); // contoh: "3"
|
|
|
|
|
setUserLevelId(ulne ?? null);
|
|
|
|
|
}, []);
|
2025-07-14 07:31:51 +00:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
initState();
|
|
|
|
|
getCategories();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
async function getCategories() {
|
|
|
|
|
const res = await getArticleByCategory();
|
|
|
|
|
const data = res?.data?.data;
|
|
|
|
|
setCategories(data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const initState = useCallback(async () => {
|
|
|
|
|
loading();
|
|
|
|
|
const req = {
|
|
|
|
|
limit: showData,
|
|
|
|
|
page: page,
|
|
|
|
|
search: search,
|
|
|
|
|
};
|
2025-11-18 06:56:39 +00:00
|
|
|
const res = await getBannerData(req);
|
2025-07-14 07:31:51 +00:00
|
|
|
await getTableNumber(parseInt(showData), res.data?.data);
|
|
|
|
|
setTotalPage(res?.data?.meta?.totalPage);
|
|
|
|
|
close();
|
|
|
|
|
}, [page]);
|
|
|
|
|
|
|
|
|
|
const getTableNumber = async (limit: number, data: Article[]) => {
|
|
|
|
|
if (data) {
|
|
|
|
|
const startIndex = limit * (page - 1);
|
|
|
|
|
let iterate = 0;
|
|
|
|
|
const newData = data.map((value: any) => {
|
|
|
|
|
iterate++;
|
|
|
|
|
value.no = startIndex + iterate;
|
|
|
|
|
return value;
|
|
|
|
|
});
|
|
|
|
|
setArticle(newData);
|
|
|
|
|
} else {
|
|
|
|
|
setArticle([]);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
async function doDelete(id: any) {
|
|
|
|
|
// loading();
|
2025-11-18 06:56:39 +00:00
|
|
|
const resDelete = await deleteBanner(id);
|
2025-07-14 07:31:51 +00:00
|
|
|
|
|
|
|
|
if (resDelete?.error) {
|
|
|
|
|
error(resDelete.message);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
close();
|
|
|
|
|
success("Berhasil Hapus");
|
|
|
|
|
initState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleDelete = (id: any) => {
|
|
|
|
|
MySwal.fire({
|
|
|
|
|
title: "Hapus Data",
|
|
|
|
|
icon: "warning",
|
|
|
|
|
showCancelButton: true,
|
|
|
|
|
cancelButtonColor: "#3085d6",
|
|
|
|
|
confirmButtonColor: "#d33",
|
|
|
|
|
confirmButtonText: "Hapus",
|
|
|
|
|
}).then((result) => {
|
|
|
|
|
if (result.isConfirmed) {
|
|
|
|
|
doDelete(id);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBanner = async (id: number, status: boolean) => {
|
|
|
|
|
const res = await updateIsBannerArticle(id, status);
|
|
|
|
|
if (res?.error) {
|
|
|
|
|
error(res?.message);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
initState();
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-18 06:56:39 +00:00
|
|
|
const [openEditDialog, setOpenEditDialog] = useState(false);
|
|
|
|
|
const [selectedBanner, setSelectedBanner] = useState<any>(null);
|
|
|
|
|
const [openPreview, setOpenPreview] = useState(false);
|
2026-01-18 17:01:09 +00:00
|
|
|
const [openViewDialog, setOpenViewDialog] = useState(false);
|
|
|
|
|
const [viewBanner, setViewBanner] = useState<any>(null);
|
|
|
|
|
const [openApproverHistory, setOpenApproverHistory] = useState(false);
|
2026-01-19 11:25:14 +00:00
|
|
|
const [openCommentModal, setOpenCommentModal] = useState(false);
|
|
|
|
|
const [commentValue, setCommentValue] = useState("");
|
|
|
|
|
|
|
|
|
|
const handleSubmitComment = async () => {
|
|
|
|
|
// await api.post("/banner/comment", {
|
|
|
|
|
// bannerId: viewBanner.id,
|
|
|
|
|
// comment: commentValue,
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
setOpenCommentModal(false);
|
|
|
|
|
};
|
2026-01-18 17:01:09 +00:00
|
|
|
|
|
|
|
|
const handleView = (item: any) => {
|
|
|
|
|
setViewBanner(item);
|
|
|
|
|
setOpenViewDialog(true);
|
|
|
|
|
};
|
2025-11-18 06:56:39 +00:00
|
|
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
|
|
|
|
|
2026-01-18 17:01:09 +00:00
|
|
|
const handleOpenApproverHistory = () => {
|
|
|
|
|
setOpenApproverHistory(true);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-18 06:56:39 +00:00
|
|
|
const handleEdit = (item: any) => {
|
|
|
|
|
setSelectedBanner({
|
|
|
|
|
id: item.id,
|
|
|
|
|
title: item.title,
|
|
|
|
|
thumbnail_url: item.thumbnail_url, // FIX
|
|
|
|
|
position: item.position, // FIX
|
|
|
|
|
});
|
|
|
|
|
setOpenEditDialog(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleUpdateBanner = async (formData: FormData, id: number) => {
|
|
|
|
|
await updateBanner(formData, id);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handlePreview = (imgUrl: string) => {
|
|
|
|
|
setPreviewImage(imgUrl);
|
|
|
|
|
setOpenPreview(true);
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-14 07:31:51 +00:00
|
|
|
const copyUrlArticle = async (id: number, slug: string) => {
|
|
|
|
|
const url =
|
|
|
|
|
`${window.location.protocol}//${window.location.host}` +
|
|
|
|
|
"/news/detail/" +
|
|
|
|
|
`${id}-${slug}`;
|
|
|
|
|
try {
|
|
|
|
|
await navigator.clipboard.writeText(url);
|
|
|
|
|
successToast("Success", "Article Copy to Clipboard");
|
|
|
|
|
setTimeout(() => {}, 1500);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
("Failed to copy!");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderCell = useCallback(
|
|
|
|
|
(article: any, columnKey: Key) => {
|
|
|
|
|
const cellValue = article[columnKey as keyof any];
|
|
|
|
|
|
|
|
|
|
switch (columnKey) {
|
|
|
|
|
case "isPublish":
|
2026-01-18 17:01:09 +00:00
|
|
|
return <p>{article.isPublish ? "Publish" : "Draft"}</p>;
|
2025-07-14 07:31:51 +00:00
|
|
|
case "isBanner":
|
|
|
|
|
return <p>{article.isBanner ? "Ya" : "Tidak"}</p>;
|
|
|
|
|
case "createdAt":
|
|
|
|
|
return <p>{convertDateFormat(article.createdAt)}</p>;
|
|
|
|
|
case "category":
|
|
|
|
|
return (
|
|
|
|
|
<p>
|
|
|
|
|
{article?.categories?.map((list: any) => list.title).join(", ") +
|
|
|
|
|
" "}
|
|
|
|
|
</p>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
case "actions":
|
|
|
|
|
return (
|
|
|
|
|
<div className="relative flex items-center gap-2">
|
|
|
|
|
<DropdownMenu>
|
|
|
|
|
<DropdownMenuTrigger asChild>
|
|
|
|
|
<Button variant="ghost" size="icon">
|
|
|
|
|
<DotsYIcon className="h-5 w-5 text-muted-foreground" />
|
|
|
|
|
</Button>
|
|
|
|
|
</DropdownMenuTrigger>
|
|
|
|
|
<DropdownMenuContent className="w-56">
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={() => copyUrlArticle(article.id, article.slug)}
|
|
|
|
|
>
|
|
|
|
|
<CopyIcon className="mr-2 h-4 w-4" />
|
|
|
|
|
Copy Url Article
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
|
|
|
|
|
<DropdownMenuItem asChild>
|
|
|
|
|
<Link
|
|
|
|
|
href={`/admin/article/detail/${article.id}`}
|
|
|
|
|
className="flex items-center"
|
|
|
|
|
>
|
|
|
|
|
<EyeIconMdi className="mr-2 h-4 w-4" />
|
|
|
|
|
Detail
|
|
|
|
|
</Link>
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
|
|
|
|
|
{(username === "admin-mabes" ||
|
|
|
|
|
Number(userId) === article.createdById) && (
|
|
|
|
|
<DropdownMenuItem asChild>
|
|
|
|
|
<Link
|
|
|
|
|
href={`/admin/article/edit/${article.id}`}
|
|
|
|
|
className="flex items-center"
|
|
|
|
|
>
|
|
|
|
|
<CreateIconIon className="mr-2 h-4 w-4" />
|
|
|
|
|
Edit
|
|
|
|
|
</Link>
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{username === "admin-mabes" && (
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={() =>
|
|
|
|
|
handleBanner(article.id, !article.isBanner)
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<BannerIcon className="mr-2 h-4 w-4" />
|
|
|
|
|
{article.isBanner
|
|
|
|
|
? "Hapus dari Banner"
|
|
|
|
|
: "Jadikan Banner"}
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{(username === "admin-mabes" ||
|
|
|
|
|
Number(userId) === article.createdById) && (
|
|
|
|
|
<DropdownMenuItem onClick={() => handleDelete(article.id)}>
|
|
|
|
|
<DeleteIcon className="mr-2 h-4 w-4 text-red-500" />
|
|
|
|
|
Delete
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
)}
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
</DropdownMenu>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return cellValue;
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-01-19 11:25:14 +00:00
|
|
|
[article, page],
|
2025-07-14 07:31:51 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let typingTimer: NodeJS.Timeout;
|
|
|
|
|
const doneTypingInterval = 1500;
|
|
|
|
|
|
|
|
|
|
const handleKeyUp = () => {
|
|
|
|
|
clearTimeout(typingTimer);
|
|
|
|
|
typingTimer = setTimeout(doneTyping, doneTypingInterval);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleKeyDown = () => {
|
|
|
|
|
clearTimeout(typingTimer);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
async function doneTyping() {
|
|
|
|
|
setPage(1);
|
|
|
|
|
initState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<div className="py-3">
|
2025-11-18 06:56:39 +00:00
|
|
|
<div className="w-full overflow-x-auto rounded-2xl shadow-sm border border-gray-200">
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="bg-[#0F6C75] text-white text-lg rounded-t-sm px-6 py-3">
|
|
|
|
|
Daftar Banner
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Table */}
|
|
|
|
|
<Table className="w-full text-sm">
|
|
|
|
|
<TableHeader>
|
|
|
|
|
<TableRow className="bg-[#BCD4DF] text-[#008080]">
|
|
|
|
|
<TableHead className="w-[40px] text-[#008080]">NO</TableHead>
|
|
|
|
|
<TableHead className="text-[#008080]">JUDUL / NAMA</TableHead>
|
|
|
|
|
<TableHead className="text-[#008080] text-center">
|
|
|
|
|
PREVIEW KONTEN
|
|
|
|
|
</TableHead>
|
|
|
|
|
<TableHead className="text-[#008080] text-center w-[100px]">
|
|
|
|
|
URUTAN
|
|
|
|
|
</TableHead>
|
|
|
|
|
<TableHead className="text-[#008080] text-center w-[120px]">
|
|
|
|
|
STATUS
|
|
|
|
|
</TableHead>
|
|
|
|
|
<TableHead className="text-[#008080] text-center w-[120px]">
|
|
|
|
|
AKSI
|
|
|
|
|
</TableHead>
|
|
|
|
|
</TableRow>
|
|
|
|
|
</TableHeader>
|
|
|
|
|
|
|
|
|
|
<TableBody>
|
|
|
|
|
{article.length > 0 ? (
|
|
|
|
|
article.map((item, index) => (
|
|
|
|
|
<TableRow
|
|
|
|
|
key={item.id}
|
|
|
|
|
className="border-b hover:bg-gray-50 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<TableCell className="text-gray-700">{index + 1}</TableCell>
|
|
|
|
|
|
|
|
|
|
{/* JUDUL */}
|
|
|
|
|
<TableCell className="font-medium text-gray-900">
|
|
|
|
|
<p className="font-semibold">{item.title}</p>
|
|
|
|
|
<p className="text-gray-500 text-sm">
|
|
|
|
|
{item.subtitle ?? ""}
|
|
|
|
|
</p>
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
|
|
|
|
{/* PREVIEW */}
|
|
|
|
|
<TableCell className="flex justify-center">
|
|
|
|
|
<div
|
|
|
|
|
className="w-[80px] h-[80px] overflow-hidden rounded-md border bg-gray-100 cursor-pointer hover:opacity-80 transition"
|
|
|
|
|
onClick={() =>
|
|
|
|
|
item.thumbnail_url &&
|
|
|
|
|
handlePreview(item.thumbnail_url)
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{item.thumbnail_url ? (
|
|
|
|
|
<img
|
|
|
|
|
src={item.thumbnail_url}
|
|
|
|
|
alt={item.title}
|
|
|
|
|
className="w-full h-full object-cover"
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex items-center justify-center h-full text-gray-400 text-xs">
|
|
|
|
|
No Image
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
|
|
|
|
{/* URUTAN */}
|
|
|
|
|
<TableCell className="text-center font-medium text-gray-700">
|
|
|
|
|
{item.position}
|
|
|
|
|
</TableCell>
|
|
|
|
|
|
|
|
|
|
{/* STATUS */}
|
|
|
|
|
<TableCell className="text-center">
|
2026-01-19 11:25:14 +00:00
|
|
|
{/* {item.status === "Disetujui" ? (
|
2025-11-18 06:56:39 +00:00
|
|
|
<span className="bg-green-100 text-green-700 text-xs px-3 py-1 rounded-full font-medium">
|
|
|
|
|
Disetujui
|
|
|
|
|
</span>
|
2026-01-19 11:25:14 +00:00
|
|
|
) : item.status === "Menunggu" ? ( */}
|
|
|
|
|
<span className="bg-yellow-100 text-yellow-700 text-xs px-3 py-1 rounded-full font-medium">
|
|
|
|
|
Menunggu
|
|
|
|
|
</span>
|
|
|
|
|
{/* ) : (
|
2025-11-18 06:56:39 +00:00
|
|
|
<span className="bg-red-100 text-red-700 text-xs px-3 py-1 rounded-full font-medium">
|
|
|
|
|
Ditolak
|
|
|
|
|
</span>
|
2026-01-19 11:25:14 +00:00
|
|
|
)} */}
|
2025-11-18 06:56:39 +00:00
|
|
|
</TableCell>
|
|
|
|
|
|
|
|
|
|
{/* AKSI */}
|
|
|
|
|
<TableCell className="text-center">
|
|
|
|
|
<div className="flex justify-center gap-3">
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="text-[#0F6C75] hover:bg-transparent hover:underline p-0"
|
2026-01-18 17:01:09 +00:00
|
|
|
onClick={() => handleView(item)}
|
2025-11-18 06:56:39 +00:00
|
|
|
>
|
2026-01-18 17:01:09 +00:00
|
|
|
<Eye className="w-4 h-4 mr-1" />
|
|
|
|
|
Lihat
|
2025-11-18 06:56:39 +00:00
|
|
|
</Button>
|
2026-01-19 11:25:14 +00:00
|
|
|
{userLevelId !== "3" && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="text-[#0F6C75] hover:bg-transparent hover:underline p-0"
|
|
|
|
|
onClick={() => handleEdit(item)}
|
|
|
|
|
>
|
|
|
|
|
<CreateIconIon className="w-4 h-4 mr-1" />
|
|
|
|
|
Edit
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
2025-11-18 06:56:39 +00:00
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="text-red-600 hover:bg-transparent hover:underline p-0"
|
|
|
|
|
onClick={() => handleDelete(item.id)}
|
|
|
|
|
>
|
|
|
|
|
<DeleteIcon className="w-4 h-4 mr-1 text-red-600" />
|
|
|
|
|
Hapus
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
))
|
|
|
|
|
) : (
|
|
|
|
|
<TableRow>
|
|
|
|
|
<TableCell
|
|
|
|
|
colSpan={6}
|
|
|
|
|
className="text-center py-6 text-gray-500"
|
|
|
|
|
>
|
|
|
|
|
Tidak ada data untuk ditampilkan.
|
|
|
|
|
</TableCell>
|
|
|
|
|
</TableRow>
|
|
|
|
|
)}
|
|
|
|
|
</TableBody>
|
|
|
|
|
</Table>
|
|
|
|
|
|
|
|
|
|
{/* Footer Pagination */}
|
|
|
|
|
<div className="flex items-center justify-between px-6 py-3 border-t text-sm text-gray-600">
|
|
|
|
|
<p>
|
|
|
|
|
Menampilkan {article.length} dari {article.length} data
|
|
|
|
|
</p>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="rounded-full px-3"
|
|
|
|
|
disabled={page === 1}
|
|
|
|
|
onClick={() => setPage(page - 1)}
|
2025-07-14 07:31:51 +00:00
|
|
|
>
|
2025-11-18 06:56:39 +00:00
|
|
|
Previous
|
|
|
|
|
</Button>
|
|
|
|
|
<p>
|
|
|
|
|
Halaman {page} dari {totalPage}
|
|
|
|
|
</p>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="rounded-full px-3"
|
|
|
|
|
disabled={page === totalPage}
|
|
|
|
|
onClick={() => setPage(page + 1)}
|
|
|
|
|
>
|
|
|
|
|
Next
|
|
|
|
|
</Button>
|
2025-07-14 07:31:51 +00:00
|
|
|
</div>
|
2025-11-18 06:56:39 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<EditBannerDialog
|
|
|
|
|
open={openEditDialog}
|
|
|
|
|
onOpenChange={setOpenEditDialog}
|
|
|
|
|
bannerData={selectedBanner}
|
|
|
|
|
onSubmit={handleUpdateBanner}
|
|
|
|
|
/>
|
|
|
|
|
{/* Preview Dialog */}
|
|
|
|
|
{openPreview && (
|
|
|
|
|
<div
|
|
|
|
|
className="fixed inset-0 flex items-center justify-center bg-black/50 z-50 p-4"
|
|
|
|
|
onClick={() => setOpenPreview(false)}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className="bg-white rounded-xl overflow-hidden shadow-2xl max-w-md w-full relative"
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
|
|
|
|
{/* HEADER */}
|
|
|
|
|
<div className="bg-[#0F6C75] text-white px-5 py-4 flex flex-col gap-1 relative">
|
|
|
|
|
{/* Tombol close */}
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setOpenPreview(false)}
|
|
|
|
|
className="absolute top-3 right-4 text-white/80 hover:text-white text-lg"
|
2025-07-14 07:31:51 +00:00
|
|
|
>
|
2025-11-18 06:56:39 +00:00
|
|
|
✕
|
|
|
|
|
</button>
|
|
|
|
|
|
2026-01-18 17:01:09 +00:00
|
|
|
<h2 className="text-lg font-semibold">JAEC00 J7 SHS-P</h2>
|
2025-11-18 06:56:39 +00:00
|
|
|
<p className="text-sm text-white/90">DELICATE OFF-ROAD SUV</p>
|
|
|
|
|
|
|
|
|
|
{/* Status badge */}
|
|
|
|
|
<div className="flex items-center gap-2 mt-1">
|
|
|
|
|
<span className="bg-yellow-100 text-yellow-800 text-xs font-medium px-3 py-1 rounded-full">
|
|
|
|
|
Menunggu
|
|
|
|
|
</span>
|
|
|
|
|
<span className="bg-white/20 text-white text-xs px-2 py-[1px] rounded-full">
|
|
|
|
|
1
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
2025-07-14 07:31:51 +00:00
|
|
|
</div>
|
2025-11-18 06:56:39 +00:00
|
|
|
|
|
|
|
|
{/* IMAGE PREVIEW */}
|
|
|
|
|
<div className="bg-[#f8fafc] p-4 flex justify-center items-center">
|
|
|
|
|
<img
|
|
|
|
|
src={previewImage ?? ""}
|
|
|
|
|
alt="Preview"
|
|
|
|
|
className="rounded-lg w-full h-auto object-contain"
|
2025-07-14 07:31:51 +00:00
|
|
|
/>
|
|
|
|
|
</div>
|
2025-11-18 06:56:39 +00:00
|
|
|
|
|
|
|
|
{/* FOOTER */}
|
|
|
|
|
<div className="border-t text-center py-3 bg-[#E3EFF4]">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setOpenPreview(false)}
|
|
|
|
|
className="text-[#0F6C75] font-medium hover:underline"
|
|
|
|
|
>
|
|
|
|
|
Tutup
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
2025-07-14 07:31:51 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-11-18 06:56:39 +00:00
|
|
|
)}
|
2026-01-18 17:01:09 +00:00
|
|
|
{openViewDialog && viewBanner && (
|
|
|
|
|
<div
|
|
|
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
|
|
|
|
|
onClick={() => setOpenViewDialog(false)}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className="bg-white rounded-2xl shadow-2xl max-w-xl w-full overflow-hidden"
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
|
|
|
|
{/* HEADER */}
|
|
|
|
|
<div className="bg-gradient-to-br from-[#1F6779] to-[#0F6C75] text-white px-6 py-5 relative">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setOpenViewDialog(false)}
|
|
|
|
|
className="absolute top-4 right-4 text-white/80 hover:text-white text-xl"
|
|
|
|
|
>
|
|
|
|
|
✕
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<h2 className="text-lg font-semibold">Detail Banner</h2>
|
|
|
|
|
|
|
|
|
|
{/* Badge */}
|
|
|
|
|
<div className="flex items-center gap-2 mt-3">
|
|
|
|
|
<span
|
|
|
|
|
className={`text-xs font-medium px-3 py-1 rounded-full
|
|
|
|
|
${
|
|
|
|
|
viewBanner.status === "Menunggu"
|
|
|
|
|
? "bg-yellow-100 text-yellow-800"
|
|
|
|
|
: viewBanner.status === "Disetujui"
|
2026-01-19 11:25:14 +00:00
|
|
|
? "bg-green-100 text-green-800"
|
|
|
|
|
: "bg-red-100 text-red-800"
|
2026-01-18 17:01:09 +00:00
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{viewBanner.status}
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
<span className="bg-white text-[#0F6C75] text-xs font-medium px-3 py-1 rounded-full">
|
|
|
|
|
Banner
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
<span className="bg-white/20 text-white text-xs px-2 py-[2px] rounded-full">
|
|
|
|
|
{viewBanner.position}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* BODY */}
|
|
|
|
|
<div className="p-6 space-y-6">
|
|
|
|
|
{/* JUDUL */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-500 mb-2">
|
|
|
|
|
Judul Banner <span className="text-red-500">*</span>
|
|
|
|
|
</label>
|
|
|
|
|
<div className="border rounded-lg p-3 text-gray-800 bg-gray-50 whitespace-pre-line">
|
|
|
|
|
{viewBanner.title}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* IMAGE */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-500 mb-2">
|
|
|
|
|
Upload File <span className="text-red-500">*</span>
|
|
|
|
|
</label>
|
|
|
|
|
<div className="w-[140px] h-[140px] rounded-lg overflow-hidden border bg-gray-100">
|
|
|
|
|
{viewBanner.thumbnail_url ? (
|
|
|
|
|
<img
|
|
|
|
|
src={viewBanner.thumbnail_url}
|
|
|
|
|
alt={viewBanner.title}
|
|
|
|
|
className="w-full h-full object-cover"
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex items-center justify-center h-full text-gray-400 text-xs">
|
|
|
|
|
No Image
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* TIMELINE */}
|
|
|
|
|
<div>
|
|
|
|
|
<h4 className="text-sm font-semibold text-gray-700 mb-3">
|
|
|
|
|
Status Timeline
|
|
|
|
|
</h4>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="flex gap-3">
|
|
|
|
|
<div className="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center">
|
|
|
|
|
<CheckCheck className="w-4 h-4 text-green-600" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<p className="font-medium text-gray-800">
|
|
|
|
|
Diupload oleh {viewBanner.createdByName}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm text-gray-500">
|
|
|
|
|
{convertDateFormat(viewBanner.created_at)} WIB
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-3">
|
|
|
|
|
<div className="w-6 h-6 rounded-full bg-yellow-100 flex items-center justify-center">
|
|
|
|
|
⏳
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<p className="font-medium text-gray-800">
|
|
|
|
|
Menunggu disetujui oleh Approver
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-sm text-gray-500">
|
|
|
|
|
{convertDateFormat(viewBanner.updated_at)} WIB
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleOpenApproverHistory}
|
|
|
|
|
className="text-sm text-blue-600 hover:underline mt-2"
|
|
|
|
|
>
|
|
|
|
|
View Approver History
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* FOOTER */}
|
2026-01-19 11:25:14 +00:00
|
|
|
{userLevelId !== "2" && (
|
|
|
|
|
<div className="flex justify-between items-center gap-3 px-6 py-4 border-t bg-[#F2F7FA]">
|
|
|
|
|
<Button
|
|
|
|
|
variant="secondary"
|
|
|
|
|
className="bg-blue-200 hover:bg-blue-400"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
setOpenCommentModal(true);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Beri Tanggapan
|
|
|
|
|
</Button>
|
2026-01-18 17:01:09 +00:00
|
|
|
|
2026-01-19 11:25:14 +00:00
|
|
|
<Button variant="destructive" className="w-[180]">
|
|
|
|
|
Reject
|
|
|
|
|
</Button>
|
|
|
|
|
<Button className="bg-green-600 hover:bg-green-700 text-white w-[180]">
|
2026-01-18 17:01:09 +00:00
|
|
|
Approved
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2026-01-19 11:25:14 +00:00
|
|
|
)}
|
2026-01-18 17:01:09 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{openApproverHistory && (
|
|
|
|
|
<div
|
|
|
|
|
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4"
|
|
|
|
|
onClick={() => setOpenApproverHistory(false)}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className="bg-white rounded-2xl shadow-2xl max-w-3xl w-full overflow-hidden"
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
|
|
|
|
{/* HEADER */}
|
|
|
|
|
<div className="bg-gradient-to-br from-[#1F6779] to-[#0F6C75] text-white px-6 py-5 relative">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setOpenApproverHistory(false)}
|
|
|
|
|
className="absolute top-4 right-4 text-white/80 hover:text-white text-xl"
|
|
|
|
|
>
|
|
|
|
|
✕
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<h2 className="text-lg font-semibold">Approver History</h2>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center gap-2 mt-3">
|
|
|
|
|
<span className="bg-yellow-100 text-yellow-800 text-xs font-medium px-3 py-1 rounded-full">
|
|
|
|
|
Menunggu
|
|
|
|
|
</span>
|
|
|
|
|
<span className="bg-white text-[#0F6C75] text-xs font-medium px-3 py-1 rounded-full">
|
|
|
|
|
Banner
|
|
|
|
|
</span>
|
|
|
|
|
<span className="bg-white/20 text-white text-xs px-2 py-[2px] rounded-full">
|
|
|
|
|
1
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* BODY */}
|
|
|
|
|
<div className="p-6 grid grid-cols-[1fr_auto_1fr] gap-6 items-start">
|
|
|
|
|
{/* LEFT TIMELINE */}
|
|
|
|
|
<div className="relative space-y-6">
|
|
|
|
|
{/* Upload */}
|
|
|
|
|
<div className="flex flex-col items-center">
|
|
|
|
|
<span className="bg-[#C7DDE4] text-[#0F6C75] text-xs px-4 py-1 rounded-full">
|
|
|
|
|
Upload
|
|
|
|
|
</span>
|
|
|
|
|
<div className="w-px h-6 bg-gray-300" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Diterima */}
|
|
|
|
|
<div className="relative bg-[#1F6779] text-white rounded-xl p-4">
|
|
|
|
|
<h4 className="font-semibold text-sm mb-2">Diterima</h4>
|
|
|
|
|
<span className="inline-block bg-[#E3EFF4] text-[#0F6C75] text-xs px-3 py-1 rounded-full">
|
|
|
|
|
Direview oleh: approver-jaecoo1
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="w-px h-6 bg-gray-300 mx-auto" />
|
|
|
|
|
|
|
|
|
|
{/* Pending */}
|
|
|
|
|
<div className="relative bg-[#B36A00] text-white rounded-xl p-4">
|
|
|
|
|
<h4 className="font-semibold text-sm mb-2">Pending</h4>
|
|
|
|
|
<span className="inline-block bg-[#FFF6CC] text-[#7A4A00] text-xs px-3 py-1 rounded-full">
|
|
|
|
|
Direview oleh: approver-jaecoo1
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* ARROW */}
|
|
|
|
|
<div className="flex flex-col gap-20 text-gray-500 font-bold">
|
|
|
|
|
<span>></span>
|
|
|
|
|
<span>></span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* RIGHT NOTES */}
|
|
|
|
|
<div className="space-y-14">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="bg-[#C7DDE4] text-sm px-4 py-2 rounded-lg">
|
|
|
|
|
Catatan:
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<div className="bg-[#FFF9C4] text-sm px-4 py-2 rounded-lg">
|
|
|
|
|
Catatan:
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* FOOTER */}
|
|
|
|
|
<div className="border-t bg-[#F2F7FA] text-center py-3">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setOpenApproverHistory(false)}
|
|
|
|
|
className="text-[#0F6C75] font-medium hover:underline"
|
|
|
|
|
>
|
|
|
|
|
Tutup
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-01-19 11:25:14 +00:00
|
|
|
{openCommentModal && viewBanner && (
|
|
|
|
|
<div
|
|
|
|
|
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4"
|
|
|
|
|
onClick={() => setOpenCommentModal(false)}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className="bg-white rounded-2xl shadow-2xl max-w-lg w-full overflow-hidden"
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
>
|
|
|
|
|
{/* HEADER */}
|
|
|
|
|
<div className="bg-gradient-to-br from-[#1F6779] to-[#0F6C75] text-white px-6 py-5 relative">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setOpenCommentModal(false)}
|
|
|
|
|
className="absolute top-4 right-4 text-white/80 hover:text-white text-xl"
|
|
|
|
|
>
|
|
|
|
|
✕
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<h2 className="text-lg font-semibold">Beri Tanggapan</h2>
|
|
|
|
|
|
|
|
|
|
{/* Badge */}
|
|
|
|
|
<div className="flex items-center gap-2 mt-3">
|
|
|
|
|
<span className="bg-yellow-100 text-yellow-800 text-xs font-medium px-3 py-1 rounded-full">
|
|
|
|
|
Menunggu
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
<span className="bg-white text-[#0F6C75] text-xs font-medium px-3 py-1 rounded-full">
|
|
|
|
|
Banner
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
<span className="bg-white/20 text-white text-xs px-2 py-[2px] rounded-full">
|
|
|
|
|
{viewBanner.position}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* BODY */}
|
|
|
|
|
<div className="p-6 space-y-4">
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-500 mb-2">
|
|
|
|
|
Comment
|
|
|
|
|
</label>
|
|
|
|
|
<textarea
|
|
|
|
|
value={commentValue}
|
|
|
|
|
onChange={(e) => setCommentValue(e.target.value)}
|
|
|
|
|
placeholder="Masukkan komentar"
|
|
|
|
|
className="w-full min-h-[100px] border rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-[#0F6C75]"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* FOOTER */}
|
|
|
|
|
<div className="flex justify-between gap-3 px-6 py-4 border-t bg-[#F2F7FA]">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setOpenCommentModal(false)}
|
|
|
|
|
className="flex-1 py-2 rounded-xl bg-blue-100 hover:bg-blue-200 text-gray-700 font-medium"
|
|
|
|
|
>
|
|
|
|
|
Batal
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleSubmitComment}
|
|
|
|
|
className="flex-1 py-2 rounded-xl bg-[#1F6779] hover:bg-[#0F6C75] text-white font-medium"
|
|
|
|
|
>
|
|
|
|
|
Submit
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-07-14 07:31:51 +00:00
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|