update
This commit is contained in:
parent
2afa91edd1
commit
87d2e8c830
|
|
@ -0,0 +1,11 @@
|
||||||
|
import AddAgentForm from "@/components/form/agent/agent-form";
|
||||||
|
import CreateArticleForm from "@/components/form/article/create-article-form";
|
||||||
|
import AddProductForm from "@/components/form/product/create-product-form";
|
||||||
|
|
||||||
|
export default function CreateAgent() {
|
||||||
|
return (
|
||||||
|
<div className="bg-slate-100 lg:p-3 dark:!bg-black overflow-y-auto">
|
||||||
|
<AddAgentForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import ArticleTable from "@/components/table/article-table";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
import { BannerDialog } from "@/components/form/banner-dialog";
|
||||||
|
import Link from "next/link";
|
||||||
|
import ProductTable from "@/components/table/product-table";
|
||||||
|
import AgentTable from "@/components/table/agent-table";
|
||||||
|
|
||||||
|
export default function AgentPage() {
|
||||||
|
const [openDialog, setOpenDialog] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmitBanner = (data: any) => {
|
||||||
|
console.log("Banner Data:", data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="overflow-x-hidden overflow-y-scroll w-full">
|
||||||
|
<div className="px-2 md:px-4 md:py-4 w-full">
|
||||||
|
<div className="pl-3">
|
||||||
|
<h1 className="text-[#1F6779] text-2xl font-semibold">Agent</h1>
|
||||||
|
<p>Kelola Agent JAECOO</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="dark:bg-[#18181b] rounded-xl p-3">
|
||||||
|
<Link href={"/admin/agent/create"}>
|
||||||
|
<Button className="bg-[#1F6779] text-white w-full lg:w-fit hover:bg-[#1a9bb5] flex items-center gap-2">
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Tambah Agent
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<AgentTable />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import UpdateAgentForm from "@/components/form/agent/update-agent-form";
|
||||||
|
|
||||||
|
export default async function EditAgentPage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
}) {
|
||||||
|
const { id } = await params;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-slate-100 lg:p-3 dark:!bg-black">
|
||||||
|
<UpdateAgentForm id={Number(id)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import CreateArticleForm from "@/components/form/article/create-article-form";
|
||||||
|
|
||||||
|
export default function CreateArticle() {
|
||||||
|
return (
|
||||||
|
<div className="bg-slate-100 p-3 lg:p-8 dark:!bg-black overflow-y-auto">
|
||||||
|
<CreateArticleForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import EditArticleForm from "@/components/form/article/edit-article-form";
|
||||||
|
|
||||||
|
export default function DetailArticlePage() {
|
||||||
|
return (
|
||||||
|
<div className="">
|
||||||
|
{/* <div className="flex flex-row justify-between border-b-2 px-4 bg-white shadow-md">
|
||||||
|
<div className="flex flex-col gap-1 py-2">
|
||||||
|
<h1 className="font-bold text-[25px]">Article</h1>
|
||||||
|
<p className="text-[14px]">Article</p>
|
||||||
|
</div>
|
||||||
|
<span className="flex items-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 20 20">
|
||||||
|
<path fill="currentColor" d="M5 1a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2zm0 3h5v1H5zm0 2h5v1H5zm0 2h5v1H5zm10 7H5v-1h10zm0-2H5v-1h10zm0-2H5v-1h10zm0-2h-4V4h4z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
<div className="h-[96vh] p-3 lg:p-8 bg-slate-100 dark:!bg-black overflow-y-auto">
|
||||||
|
<EditArticleForm isDetail={true} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import EditArticleForm from "@/components/form/article/edit-article-form";
|
||||||
|
|
||||||
|
export default function UpdateArticlePage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* <div className="flex flex-row justify-between border-b-2 px-4 bg-white shadow-md">
|
||||||
|
<div className="flex flex-col gap-1 py-2">
|
||||||
|
<h1 className="font-bold text-[25px]">Article</h1>
|
||||||
|
<p className="text-[14px]">Article</p>
|
||||||
|
</div>
|
||||||
|
<span className="flex items-center">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 20 20">
|
||||||
|
<path fill="currentColor" d="M5 1a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2zm0 3h5v1H5zm0 2h5v1H5zm0 2h5v1H5zm10 7H5v-1h10zm0-2H5v-1h10zm0-2H5v-1h10zm0-2h-4V4h4z" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</div> */}
|
||||||
|
<div className="h-[96vh] p-3 lg:p-8 bg-slate-100 dark:!bg-black overflow-y-auto">
|
||||||
|
<EditArticleForm isDetail={false} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import ArticleTable from "@/components/table/article-table";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
import { BannerDialog } from "@/components/form/banner-dialog";
|
||||||
|
import { createBanner } from "@/service/banner";
|
||||||
|
import router from "next/router";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import withReactContent from "sweetalert2-react-content";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
|
export default function BasicPage() {
|
||||||
|
const [openDialog, setOpenDialog] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
const MySwal = withReactContent(Swal);
|
||||||
|
const [refreshKey, setRefreshKey] = useState(0);
|
||||||
|
const handleSubmitBanner = async (formData: FormData) => {
|
||||||
|
try {
|
||||||
|
const response = await createBanner(formData);
|
||||||
|
console.log("Banner created:", response);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating banner:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const successSubmit = () => {
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Sukses",
|
||||||
|
icon: "success",
|
||||||
|
confirmButtonColor: "#3085d6",
|
||||||
|
confirmButtonText: "OK",
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
setRefreshKey((prev) => prev + 1); // ⬅️ trigger refresh
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="overflow-x-hidden overflow-y-scroll w-full">
|
||||||
|
<div className="px-2 md:px-4 md:py-4 w-full">
|
||||||
|
<div className="pl-3">
|
||||||
|
<h1 className="text-[#1F6779] text-2xl font-semibold">Banner</h1>
|
||||||
|
<p>Kelola gambar banner yang tampil di halaman Utama website</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="dark:bg-[#18181b] rounded-xl p-3">
|
||||||
|
<Button
|
||||||
|
className="bg-[#1F6779] text-white w-full lg:w-fit hover:bg-[#1a9bb5] flex items-center gap-2"
|
||||||
|
onClick={() => setOpenDialog(true)}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Tambah Banner
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<ArticleTable />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dialog Tambah Banner */}
|
||||||
|
<BannerDialog
|
||||||
|
open={openDialog}
|
||||||
|
onOpenChange={setOpenDialog}
|
||||||
|
onSubmit={handleSubmitBanner}
|
||||||
|
onSuccess={successSubmit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
import Galery from "@/components/table/galery";
|
||||||
|
import { GaleriDialog } from "@/components/dialog/galery-dialog";
|
||||||
|
|
||||||
|
export default function GaleryPage() {
|
||||||
|
const [openDialog, setOpenDialog] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmitGaleri = () => {
|
||||||
|
console.log("Submit galeri...");
|
||||||
|
setOpenDialog(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="overflow-x-hidden overflow-y-scroll w-full">
|
||||||
|
<div className="px-2 md:px-4 md:py-4 w-full">
|
||||||
|
<div className="pl-3">
|
||||||
|
<h1 className="text-[#1F6779] text-2xl font-semibold">Galeri</h1>
|
||||||
|
<p>Kelola Galeri JAECOO</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="dark:bg-[#18181b] rounded-xl p-3">
|
||||||
|
<Button
|
||||||
|
className="bg-[#1F6779] text-white w-full lg:w-fit hover:bg-[#1a9bb5] flex items-center gap-2"
|
||||||
|
onClick={() => setOpenDialog(true)}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Tambah Galeri Baru
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Galery />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<GaleriDialog
|
||||||
|
open={openDialog}
|
||||||
|
onClose={() => setOpenDialog(false)}
|
||||||
|
onSubmit={handleSubmitGaleri}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ export default function MasterUserPage() {
|
||||||
<Button
|
<Button
|
||||||
size="default"
|
size="default"
|
||||||
color="primary"
|
color="primary"
|
||||||
className="bg-[#F07C00] text-white"
|
className="bg-[#1F6779] text-white"
|
||||||
>
|
>
|
||||||
Pengguna Baru
|
Pengguna Baru
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import CreateArticleForm from "@/components/form/article/create-article-form";
|
||||||
|
import AddProductForm from "@/components/form/product/create-product-form";
|
||||||
|
|
||||||
|
export default function CreateProduct() {
|
||||||
|
return (
|
||||||
|
<div className="bg-slate-100 lg:p-3 dark:!bg-black overflow-y-auto">
|
||||||
|
<AddProductForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import ArticleTable from "@/components/table/article-table";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
import { BannerDialog } from "@/components/form/banner-dialog";
|
||||||
|
import Link from "next/link";
|
||||||
|
import ProductTable from "@/components/table/product-table";
|
||||||
|
|
||||||
|
export default function ProductPage() {
|
||||||
|
const [openDialog, setOpenDialog] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmitBanner = (data: any) => {
|
||||||
|
console.log("Banner Data:", data);
|
||||||
|
// TODO: kirim data ke API di sini
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="overflow-x-hidden overflow-y-scroll w-full">
|
||||||
|
<div className="px-2 md:px-4 md:py-4 w-full">
|
||||||
|
<div className="pl-3">
|
||||||
|
<h1 className="text-[#1F6779] text-2xl font-semibold">Product</h1>
|
||||||
|
<p>Kelola Informasi Product Kendaraan JAECOO</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="dark:bg-[#18181b] rounded-xl p-3">
|
||||||
|
<Link href={"/admin/product/create"}>
|
||||||
|
<Button className="bg-[#1F6779] text-white w-full lg:w-fit hover:bg-[#1a9bb5] flex items-center gap-2">
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Tambah Product Baru
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<ProductTable />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import UpdateProductForm from "@/components/form/product/update-product-form";
|
||||||
|
|
||||||
|
export default function CreateProduct() {
|
||||||
|
return (
|
||||||
|
<div className="bg-slate-100 lg:p-3 dark:!bg-black overflow-y-auto">
|
||||||
|
<UpdateProductForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import AddAgentForm from "@/components/form/agent/agent-form";
|
||||||
|
import CreateArticleForm from "@/components/form/article/create-article-form";
|
||||||
|
import AddProductForm from "@/components/form/product/create-product-form";
|
||||||
|
import AddPromoForm from "@/components/form/promotion/create-promo-form";
|
||||||
|
|
||||||
|
export default function CreatePromo() {
|
||||||
|
return (
|
||||||
|
<div className="bg-slate-100 lg:p-3 dark:!bg-black overflow-y-auto">
|
||||||
|
<AddPromoForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import ArticleTable from "@/components/table/article-table";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Plus } from "lucide-react";
|
||||||
|
import { BannerDialog } from "@/components/form/banner-dialog";
|
||||||
|
import Link from "next/link";
|
||||||
|
import ProductTable from "@/components/table/product-table";
|
||||||
|
import AgentTable from "@/components/table/agent-table";
|
||||||
|
import PromotionTable from "@/components/table/promotion-table";
|
||||||
|
|
||||||
|
export default function PromotionPage() {
|
||||||
|
const [openDialog, setOpenDialog] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmitBanner = (data: any) => {
|
||||||
|
console.log("Banner Data:", data);
|
||||||
|
// TODO: kirim data ke API di sini
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="overflow-x-hidden overflow-y-scroll w-full">
|
||||||
|
<div className="px-2 md:px-4 md:py-4 w-full">
|
||||||
|
<div className="pl-3">
|
||||||
|
<h1 className="text-[#1F6779] text-2xl font-semibold">Promo</h1>
|
||||||
|
<p>Kelola Promo JAECOO</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="dark:bg-[#18181b] rounded-xl p-3">
|
||||||
|
<Link href={"/admin/promotion/create"}>
|
||||||
|
<Button className="bg-[#1F6779] text-white w-full lg:w-fit hover:bg-[#1a9bb5] flex items-center gap-2">
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Tambah Promo Baru
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<PromotionTable />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { CheckCircle2 } from "lucide-react";
|
||||||
|
|
||||||
|
type AgentDetailProps = {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
data: {
|
||||||
|
name: string;
|
||||||
|
position: string;
|
||||||
|
phone: string;
|
||||||
|
status: "Aktif" | "Nonaktif";
|
||||||
|
roles: string[];
|
||||||
|
imageUrl: string;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AgentDetailDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
data,
|
||||||
|
}: AgentDetailProps) {
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="p-0 max-w-lg overflow-hidden rounded-2xl">
|
||||||
|
<div className="bg-gradient-to-r from-[#0f6c75] to-[#145f66] text-white px-6 py-5 relative">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-white text-xl font-semibold">
|
||||||
|
Detail Agen
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="mt-2 bg-white/20 text-white px-3 py-1 rounded-full text-xs inline-flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
className={`w-2 h-2 rounded-full ${
|
||||||
|
data.status === "Aktif" ? "bg-lime-300" : "bg-red-300"
|
||||||
|
}`}
|
||||||
|
></span>
|
||||||
|
{data.status}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 text-center">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Image
|
||||||
|
src={data.imageUrl}
|
||||||
|
alt={data.name}
|
||||||
|
width={160}
|
||||||
|
height={160}
|
||||||
|
className="rounded-lg object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-xl font-semibold mt-5">{data.name}</h2>
|
||||||
|
|
||||||
|
<p className="text-gray-600">{data.position}</p>
|
||||||
|
|
||||||
|
<p className="text-gray-700 mt-2 font-medium">{data.phone}</p>
|
||||||
|
|
||||||
|
<div className="text-left mt-5">
|
||||||
|
<p className="font-semibold mb-2">Jenis Agen</p>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-4">
|
||||||
|
{data.roles.map((role) => (
|
||||||
|
<div
|
||||||
|
key={role}
|
||||||
|
className="flex items-center gap-2 bg-green-100 text-green-700 px-3 py-1 rounded-full font-medium"
|
||||||
|
>
|
||||||
|
<CheckCircle2 size={16} className="text-green-700" />
|
||||||
|
{role}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FOOTER */}
|
||||||
|
<div className="bg-[#E3EFF4] text-center py-3">
|
||||||
|
<button
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="text-[#0F6C75] font-medium hover:underline"
|
||||||
|
>
|
||||||
|
Tutup
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { CheckCircle } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { getGaleryFileData } from "@/service/galery";
|
||||||
|
|
||||||
|
export function DialogDetailGaleri({ open, onClose, data }: any) {
|
||||||
|
const [images, setImages] = useState<any[]>([]);
|
||||||
|
|
||||||
|
const fetchImages = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getGaleryFileData(data.id);
|
||||||
|
const allImages = res?.data?.data ?? [];
|
||||||
|
|
||||||
|
const filteredImages = allImages.filter(
|
||||||
|
(img: any) => img.gallery_id === data.id
|
||||||
|
);
|
||||||
|
|
||||||
|
setImages(filteredImages);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error fetch files:", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && data?.id) {
|
||||||
|
fetchImages();
|
||||||
|
}
|
||||||
|
}, [open, data]);
|
||||||
|
|
||||||
|
const openFile = (url: string) => {
|
||||||
|
window.open(url, "_blank");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-3xl rounded-2xl p-0 overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-[#1F6779] text-white px-6 py-4">
|
||||||
|
<DialogTitle className="text-white">Detail Galeri</DialogTitle>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-6 py-6 space-y-6">
|
||||||
|
{/* Images List */}
|
||||||
|
<div>
|
||||||
|
<p className="font-medium mb-2">Daftar Gambar</p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
{images.length === 0 && (
|
||||||
|
<p className="text-gray-500 col-span-3 text-center">
|
||||||
|
Tidak ada gambar.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{images.map((img) => (
|
||||||
|
<div
|
||||||
|
key={img.id}
|
||||||
|
className="relative h-32 w-full cursor-pointer group"
|
||||||
|
onClick={() => openFile(img.image_url)}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={img.image_url}
|
||||||
|
alt={img.title}
|
||||||
|
fill
|
||||||
|
className="object-cover rounded-lg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 text-white flex items-center justify-center text-sm transition">
|
||||||
|
Lihat File
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h2 className="text-2xl font-semibold text-[#1F6779]">
|
||||||
|
{data.title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-sm text-gray-700">Deskripsi</p>
|
||||||
|
<p className="text-gray-600">{data.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tanggal Upload */}
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-sm text-gray-700">Tanggal Upload</p>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
{new Date(data.created_at).toLocaleDateString("id-ID")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timeline */}
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-sm text-gray-700 mb-2">
|
||||||
|
Status Timeline
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<CheckCircle className="text-green-600" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Upload Berhasil</p>
|
||||||
|
<p className="text-gray-500 text-sm">
|
||||||
|
{new Date(data.created_at).toLocaleString("id-ID")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{data.approved_at && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<CheckCircle className="text-green-600" />
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">Disetujui oleh Approver</p>
|
||||||
|
<p className="text-gray-500 text-sm">
|
||||||
|
{new Date(data.approved_at).toLocaleString("id-ID")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="px-6 pb-6">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="bg-gray-300 text-gray-700 px-6 py-2 rounded-lg"
|
||||||
|
>
|
||||||
|
Tutup
|
||||||
|
</button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Upload, X } from "lucide-react";
|
||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import { createGalery, uploadGaleryFile } from "@/service/galery";
|
||||||
|
|
||||||
|
export function GaleriDialog({ open, onClose, onSubmit }: any) {
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
|
const [previews, setPreviews] = useState<string[]>([]);
|
||||||
|
const fileRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
setPreviews([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectUrls = files.map((file) => URL.createObjectURL(file));
|
||||||
|
setPreviews(objectUrls);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
objectUrls.forEach((url) => URL.revokeObjectURL(url));
|
||||||
|
};
|
||||||
|
}, [files]);
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = e.target.files;
|
||||||
|
if (!files) return;
|
||||||
|
setFiles((prev: File[]) => [...prev, ...Array.from(files)]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFile = (index: number) => {
|
||||||
|
setFiles((prev) => prev.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
if (!title) return alert("Judul wajib diisi!");
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("title", title);
|
||||||
|
formData.append("description", description);
|
||||||
|
|
||||||
|
const res = await createGalery(formData);
|
||||||
|
|
||||||
|
const galleryId = res?.data?.data?.id;
|
||||||
|
if (!galleryId) {
|
||||||
|
alert("Galeri gagal dibuat");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Galeri Created:", galleryId);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const fileForm = new FormData();
|
||||||
|
fileForm.append("gallery_id", galleryId);
|
||||||
|
fileForm.append("title", title);
|
||||||
|
fileForm.append("file", file);
|
||||||
|
|
||||||
|
const uploadRes = await uploadGaleryFile(galleryId, fileForm);
|
||||||
|
console.log("File Uploaded:", uploadRes?.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubmit();
|
||||||
|
|
||||||
|
setTitle("");
|
||||||
|
setDescription("");
|
||||||
|
setFiles([]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Submit failed:", error);
|
||||||
|
alert("Gagal mengupload galeri");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-2xl rounded-2xl p-0 overflow-hidden">
|
||||||
|
{/* HEADER */}
|
||||||
|
<div className="bg-[#1F6779] text-white px-6 py-4 flex justify-between items-center">
|
||||||
|
<DialogTitle className="text-white">Tambah Galeri</DialogTitle>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* BODY */}
|
||||||
|
<div className="px-6 py-6 space-y-6">
|
||||||
|
{/* Judul */}
|
||||||
|
<div>
|
||||||
|
<label className="font-medium text-sm">
|
||||||
|
Judul Galeri <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
placeholder="Masukkan judul galeri"
|
||||||
|
className="mt-1 h-12"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Deskripsi */}
|
||||||
|
<div>
|
||||||
|
<label className="font-medium text-sm">
|
||||||
|
Deskripsi Galeri <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
placeholder="Masukkan deskripsi galeri"
|
||||||
|
className="mt-1 h-12"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Upload File */}
|
||||||
|
<div>
|
||||||
|
<label className="font-medium text-sm">
|
||||||
|
Upload File (opsional)
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="border-2 border-dashed rounded-xl flex flex-col items-center justify-center py-10 cursor-pointer"
|
||||||
|
onClick={() => fileRef.current?.click()}
|
||||||
|
>
|
||||||
|
<Upload className="w-10 h-10 text-[#1F6779]" />
|
||||||
|
|
||||||
|
<p className="text-sm text-gray-600 mt-2">
|
||||||
|
Klik untuk upload atau drag & drop
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400">PNG, JPG (max 2MB)</p>
|
||||||
|
|
||||||
|
<input
|
||||||
|
ref={fileRef}
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
accept="image/*"
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Preview Gambar */}
|
||||||
|
{previews.length > 0 && (
|
||||||
|
<div className="mt-4 flex flex-wrap gap-4">
|
||||||
|
{previews.map((src, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="relative w-24 h-24 rounded-lg overflow-hidden border border-gray-300"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={`preview-${i}`}
|
||||||
|
className="object-cover w-full h-full"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => removeFile(i)}
|
||||||
|
className="absolute top-1 right-1 bg-red-600 rounded-full p-1 text-white hover:bg-red-700 transition"
|
||||||
|
title="Hapus gambar"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FOOTER */}
|
||||||
|
<DialogFooter className="flex justify-between px-6 pb-6 gap-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={onClose}
|
||||||
|
className="bg-slate-200 text-slate-700 w-full h-12"
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
className="bg-[#1F6779] hover:bg-[#165766] text-white w-full h-12"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getGaleryFileData,
|
||||||
|
updateGalery,
|
||||||
|
updateUploadGaleryFile,
|
||||||
|
deleteGaleryFile,
|
||||||
|
} from "@/service/galery";
|
||||||
|
import { error, success } from "@/config/swal";
|
||||||
|
import withReactContent from "sweetalert2-react-content";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
|
export function DialogUpdateGaleri({ open, onClose, data, onUpdated }: any) {
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const MySwal = withReactContent(Swal);
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [oldFiles, setOldFiles] = useState<any[]>([]);
|
||||||
|
const [newFiles, setNewFiles] = useState<File[]>([]);
|
||||||
|
const [newFilePreviews, setNewFilePreviews] = useState<string[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const fetchOldFiles = async (galleryId: any) => {
|
||||||
|
try {
|
||||||
|
const res = await getGaleryFileData(galleryId);
|
||||||
|
const allImages = res?.data?.data ?? [];
|
||||||
|
const filtered = allImages.filter(
|
||||||
|
(img: any) => img.gallery_id === galleryId
|
||||||
|
);
|
||||||
|
setOldFiles(filtered);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error fetching files:", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && data?.id) {
|
||||||
|
setTitle(data.title ?? "");
|
||||||
|
|
||||||
|
setDescription(data.description ?? data.desc ?? "");
|
||||||
|
fetchOldFiles(data.id);
|
||||||
|
|
||||||
|
setNewFiles([]);
|
||||||
|
setNewFilePreviews([]);
|
||||||
|
} else if (!open) {
|
||||||
|
}
|
||||||
|
}, [open, data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!newFiles || newFiles.length === 0) {
|
||||||
|
setNewFilePreviews([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urls = newFiles.map((f) => URL.createObjectURL(f));
|
||||||
|
setNewFilePreviews(urls);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
urls.forEach((u) => URL.revokeObjectURL(u));
|
||||||
|
};
|
||||||
|
}, [newFiles]);
|
||||||
|
|
||||||
|
const handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const uploaded = Array.from(e.target.files || []) as File[];
|
||||||
|
if (uploaded.length === 0) return;
|
||||||
|
setNewFiles((prev) => [...prev, ...uploaded]);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function doDelete(id: any) {
|
||||||
|
const resDelete = await deleteGaleryFile(id);
|
||||||
|
if (resDelete?.error) {
|
||||||
|
error(resDelete.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
success("Berhasil Hapus");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeOldFile = (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 removeNewFile = (index: number) => {
|
||||||
|
setNewFiles((prev) => prev.filter((_, i) => i !== index));
|
||||||
|
setNewFilePreviews((prev) => prev.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
if (!data?.id) {
|
||||||
|
alert("Gallery ID tidak tersedia.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const formMain = new FormData();
|
||||||
|
formMain.append("title", title);
|
||||||
|
formMain.append("description", description);
|
||||||
|
formMain.append("old_files", JSON.stringify(oldFiles.map((f) => f.id)));
|
||||||
|
|
||||||
|
const updateRes = await updateGalery(data.id, formMain);
|
||||||
|
console.log("updateGalery response:", updateRes);
|
||||||
|
|
||||||
|
if (updateRes?.error) {
|
||||||
|
alert(updateRes.message || "Gagal update galeri");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const galleryId = data.id;
|
||||||
|
const failedUploads: any[] = [];
|
||||||
|
|
||||||
|
for (const file of newFiles) {
|
||||||
|
try {
|
||||||
|
const formFile = new FormData();
|
||||||
|
formFile.append("gallery_id", String(galleryId));
|
||||||
|
formFile.append("title", title);
|
||||||
|
formFile.append("is_active", "true");
|
||||||
|
formFile.append("file", file);
|
||||||
|
|
||||||
|
const uploadRes = await updateUploadGaleryFile(data.id, formFile);
|
||||||
|
|
||||||
|
console.log("uploadRes:", uploadRes);
|
||||||
|
|
||||||
|
if (uploadRes?.error) {
|
||||||
|
failedUploads.push({
|
||||||
|
name: file.name,
|
||||||
|
error: uploadRes.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
failedUploads.push({ name: file.name, error: err });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
if (failedUploads.length > 0) {
|
||||||
|
let msg = failedUploads.map((f) => `${f.name}: ${f.error}`).join("\n");
|
||||||
|
alert("Ada file gagal diupload:\n" + msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
onUpdated && onUpdated();
|
||||||
|
} catch (err: any) {
|
||||||
|
setLoading(false);
|
||||||
|
console.error("Update error:", err);
|
||||||
|
|
||||||
|
if (err?.response?.data) {
|
||||||
|
alert("Server error: " + JSON.stringify(err.response.data));
|
||||||
|
} else {
|
||||||
|
alert("Gagal menyimpan galeri");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-xl rounded-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Edit Galeri</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 px-4 pb-4">
|
||||||
|
{/* TITLE */}
|
||||||
|
<div>
|
||||||
|
<label className="font-medium text-sm">Judul Galeri *</label>
|
||||||
|
<input
|
||||||
|
className="w-full border rounded-lg px-3 py-2 mt-1"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* DESCRIPTION */}
|
||||||
|
<div>
|
||||||
|
<label className="font-medium text-sm">Deskripsi Galeri *</label>
|
||||||
|
<textarea
|
||||||
|
className="w-full border rounded-lg px-3 py-2 mt-1"
|
||||||
|
rows={3}
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* UPLOAD FILE */}
|
||||||
|
<div>
|
||||||
|
<label className="font-medium text-sm">Upload File</label>
|
||||||
|
<label className="mt-2 flex flex-col items-center justify-center border border-dashed rounded-xl py-6 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
multiple
|
||||||
|
onChange={handleUpload}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col items-center text-gray-500">
|
||||||
|
<svg width="32" height="32" fill="#1F6779">
|
||||||
|
<path d="M5 20h14v-2H5v2zm7-16l-5 5h3v4h4v-4h3l-5-5z" />
|
||||||
|
</svg>
|
||||||
|
<p>Klik untuk upload atau drag & drop</p>
|
||||||
|
<p className="text-xs">PNG, JPG (max 2MB)</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{oldFiles.length > 0 && (
|
||||||
|
<div className="flex gap-3 mt-3 flex-wrap">
|
||||||
|
{oldFiles.map((f) => (
|
||||||
|
<div key={f.id} className="relative w-20 h-20">
|
||||||
|
<Image
|
||||||
|
src={f.image_url}
|
||||||
|
alt={f.title || "image"}
|
||||||
|
fill
|
||||||
|
className="object-cover rounded-lg"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => removeOldFile(f.id)}
|
||||||
|
className="absolute -top-2 -right-2 bg-red-600 text-white rounded-full p-1"
|
||||||
|
>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{newFilePreviews.length > 0 && (
|
||||||
|
<div className="flex gap-3 mt-3 flex-wrap">
|
||||||
|
{newFilePreviews.map((src, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="relative w-20 h-20 overflow-hidden rounded-lg border"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={`new-${idx}`}
|
||||||
|
className="object-cover w-full h-full"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => removeNewFile(idx)}
|
||||||
|
className="absolute -top-2 -right-2 bg-red-600 text-white rounded-full p-1"
|
||||||
|
>
|
||||||
|
<X size={12} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="mt-6 flex gap-2 px-4 pb-4">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-6 py-2 rounded-lg bg-gray-200 text-gray-700"
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
disabled={loading}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
className="px-6 py-2 rounded-lg bg-[#1F6779] text-white"
|
||||||
|
>
|
||||||
|
{loading ? "Menyimpan..." : "Simpan"}
|
||||||
|
</button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { CheckCircle2, FileText } from "lucide-react";
|
||||||
|
import { getPromotionById } from "@/service/promotion";
|
||||||
|
|
||||||
|
type PromoDetailDialogProps = {
|
||||||
|
promoId: number | null;
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PromoDetailDialog({
|
||||||
|
promoId,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: PromoDetailDialogProps) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [promo, setPromo] = useState<any>(null);
|
||||||
|
|
||||||
|
// FORMAT TANGGAL → DD-MM-YYYY
|
||||||
|
const formatDate = (dateStr: string) => {
|
||||||
|
const d = new Date(dateStr);
|
||||||
|
const day = String(d.getDate()).padStart(2, "0");
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, "0");
|
||||||
|
const year = d.getFullYear();
|
||||||
|
return `${day}-${month}-${year}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// FETCH API PROMO BY ID
|
||||||
|
useEffect(() => {
|
||||||
|
if (!promoId || !open) return;
|
||||||
|
|
||||||
|
async function fetchData() {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const res = await getPromotionById(promoId);
|
||||||
|
|
||||||
|
// Mapping ke format dialog
|
||||||
|
const mapped = {
|
||||||
|
title: res?.data?.data?.title,
|
||||||
|
fileSize: "Tidak diketahui",
|
||||||
|
uploadDate: formatDate(res?.data?.data?.created_at),
|
||||||
|
status: res?.data?.data?.is_active ? "Aktif" : "Nonaktif",
|
||||||
|
timeline: [
|
||||||
|
{
|
||||||
|
label: "Dibuat",
|
||||||
|
date: formatDate(res?.data?.data?.created_at),
|
||||||
|
time: new Date(res?.data?.data?.created_at).toLocaleTimeString(
|
||||||
|
"id-ID",
|
||||||
|
{ hour: "2-digit", minute: "2-digit" }
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Diupdate",
|
||||||
|
date: formatDate(res?.data?.data?.updated_at),
|
||||||
|
time: new Date(res?.data?.data?.updated_at).toLocaleTimeString(
|
||||||
|
"id-ID",
|
||||||
|
{ hour: "2-digit", minute: "2-digit" }
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
setPromo(mapped);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("ERROR FETCH PROMO:", err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, [promoId, open]);
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="p-0 max-w-xl overflow-hidden rounded-2xl">
|
||||||
|
{/* HEADER */}
|
||||||
|
<div className="bg-gradient-to-r from-[#0f6c75] to-[#145f66] text-white px-6 py-5 relative">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-white text-xl font-semibold">
|
||||||
|
Detail Promo
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* STATUS BADGE */}
|
||||||
|
{promo && (
|
||||||
|
<div className="mt-2 bg-white/20 text-white px-4 py-[3px] rounded-full text-xs inline-flex items-center gap-2">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-lime-300"></span>
|
||||||
|
{promo.status}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* BODY */}
|
||||||
|
<div className="p-6">
|
||||||
|
{loading ? (
|
||||||
|
<p className="text-center text-gray-600">Memuat data...</p>
|
||||||
|
) : promo ? (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-center mb-4">
|
||||||
|
<div className="bg-[#E8F1F6] p-4 rounded-xl">
|
||||||
|
<FileText size={60} className="text-[#0f6c75]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-xl font-semibold text-center mb-6">
|
||||||
|
{promo.title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-600">Ukuran File</p>
|
||||||
|
<p className="font-medium">{promo.fileSize}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-600">Tanggal Upload</p>
|
||||||
|
<p className="font-medium">{promo.uploadDate}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* TIMELINE */}
|
||||||
|
<div className="mt-4">
|
||||||
|
<p className="text-gray-600 mb-3 font-medium">
|
||||||
|
Status Timeline
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{promo.timeline.map((item: any, idx: number) => (
|
||||||
|
<div key={idx} className="flex gap-3 items-start">
|
||||||
|
<CheckCircle2
|
||||||
|
size={20}
|
||||||
|
className="text-green-600 shrink-0"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium">{item.label}</p>
|
||||||
|
<p className="text-gray-600 text-sm">
|
||||||
|
{item.date} • {item.time} WIB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="text-center text-gray-600">Data tidak ditemukan</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FOOTER */}
|
||||||
|
<div className="bg-[#E3EFF4] text-center py-3">
|
||||||
|
<button
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="text-[#0F6C75] font-medium hover:underline w-full"
|
||||||
|
>
|
||||||
|
Tutup
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,236 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { createAgent } from "@/service/agent";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import withReactContent from "sweetalert2-react-content";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { UploadCloud } from "lucide-react";
|
||||||
|
|
||||||
|
type AgentFormValues = {
|
||||||
|
fullName: string;
|
||||||
|
position: string;
|
||||||
|
phone: string;
|
||||||
|
roles: string[];
|
||||||
|
profileImage: File | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const agentTypes = ["After Sales", "Sales", "Spv", "Branch Manager"];
|
||||||
|
|
||||||
|
export default function AddAgentForm() {
|
||||||
|
// const [previewImg, setPreviewImg] = useState<string | null>(null);
|
||||||
|
const router = useRouter();
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [previewImg, setPreview] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const MySwal = withReactContent(Swal);
|
||||||
|
const form = useForm<AgentFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
fullName: "",
|
||||||
|
position: "",
|
||||||
|
phone: "",
|
||||||
|
roles: [],
|
||||||
|
profileImage: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFileChange = (e: any) => {
|
||||||
|
const selected = e.target.files[0];
|
||||||
|
if (selected) setFile(selected);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFile = () => {
|
||||||
|
setFile(null);
|
||||||
|
setPreview(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: AgentFormValues) => {
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("name", data.fullName);
|
||||||
|
formData.append("job_title", data.position);
|
||||||
|
formData.append("phone", data.phone);
|
||||||
|
|
||||||
|
data.roles.forEach((role) => {
|
||||||
|
formData.append("agent_type[]", role);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
formData.append("file", file);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await createAgent(formData);
|
||||||
|
console.log("SUCCESS:", res);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("ERROR CREATE AGENT:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
successSubmit("/admin/agent");
|
||||||
|
};
|
||||||
|
|
||||||
|
function successSubmit(redirect: any) {
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Sukses",
|
||||||
|
icon: "success",
|
||||||
|
confirmButtonColor: "#3085d6",
|
||||||
|
confirmButtonText: "OK",
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
router.push(redirect);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white dark:bg-neutral-900 p-6 rounded-lg shadow-sm">
|
||||||
|
<h2 className="text-2xl font-bold text-teal-800 dark:text-white mb-6">
|
||||||
|
Form Tambah Agen
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="fullName"
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
Nama Lengkap <span className="text-red-500">*</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Masukkan Nama" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="position"
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
Jabatan <span className="text-red-500">*</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Contoh: Spv atau Branch Manager"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* No Telp */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="phone"
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
No Telp <span className="text-red-500">*</span>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Contoh: 021-123-xxx" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FormLabel>
|
||||||
|
Pilih Jenis Agen <span className="text-red-500">*</span>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-6 mt-3">
|
||||||
|
{agentTypes.map((role) => (
|
||||||
|
<FormField
|
||||||
|
key={role}
|
||||||
|
control={form.control}
|
||||||
|
name="roles"
|
||||||
|
render={({ field }) => {
|
||||||
|
const selected = field.value || [];
|
||||||
|
return (
|
||||||
|
<FormItem
|
||||||
|
key={role}
|
||||||
|
className="flex flex-row items-center space-x-2"
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={selected.includes(role)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
const updated = checked
|
||||||
|
? [...selected, role]
|
||||||
|
: selected.filter((i) => i !== role);
|
||||||
|
field.onChange(updated);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="font-normal">{role}</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-gray-700">
|
||||||
|
Upload Foto Profil <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<label
|
||||||
|
htmlFor="uploadFile"
|
||||||
|
className="mt-1 border-2 border-dashed border-gray-300 rounded-xl p-6 flex flex-col items-center justify-center text-gray-500 cursor-pointer hover:border-[#1F6779]/50 transition"
|
||||||
|
>
|
||||||
|
<UploadCloud className="w-10 h-10 text-[#1F6779] mb-2" />
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
Klik untuk upload atau drag & drop
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">PNG, JPG (max 2 MB)</p>
|
||||||
|
<input
|
||||||
|
id="uploadFile"
|
||||||
|
type="file"
|
||||||
|
accept="image/png, image/jpeg"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
{file && (
|
||||||
|
<p className="mt-2 text-xs text-[#1F6779] font-medium">
|
||||||
|
{file.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* BUTTON */}
|
||||||
|
<Button type="submit" className="bg-teal-700 hover:bg-teal-800">
|
||||||
|
Tambahkan Agen
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,285 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { getAgentById, updateAgent } from "@/service/agent";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
import withReactContent from "sweetalert2-react-content";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
|
type AgentFormValues = {
|
||||||
|
fullName: string;
|
||||||
|
position: string;
|
||||||
|
phone: string;
|
||||||
|
roles: string[];
|
||||||
|
profileImage: File | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const agentTypes = ["After Sales", "Sales", "Spv", "Branch Manager"];
|
||||||
|
|
||||||
|
export default function UpdateAgentForm({ id }: { id: number }) {
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [previewImg, setPreviewImg] = useState<string | null>(null);
|
||||||
|
const [agentData, setAgentData] = useState<any>(null);
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [preview, setPreview] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const MySwal = withReactContent(Swal);
|
||||||
|
|
||||||
|
const form = useForm<AgentFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
fullName: "",
|
||||||
|
position: "",
|
||||||
|
phone: "",
|
||||||
|
roles: [],
|
||||||
|
profileImage: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchData() {
|
||||||
|
try {
|
||||||
|
const res = await getAgentById(id);
|
||||||
|
|
||||||
|
if (!res || !res.data) {
|
||||||
|
console.error("DATA AGENT TIDAK DITEMUKAN");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAgentData(res.data);
|
||||||
|
|
||||||
|
form.reset({
|
||||||
|
fullName: res?.data?.data?.name,
|
||||||
|
position: res?.data?.data?.job_title,
|
||||||
|
phone: res?.data?.data?.phone,
|
||||||
|
roles: res?.data?.data?.agent_type || [],
|
||||||
|
profileImage: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("name", res?.data?.data?.name);
|
||||||
|
setPreviewImg(res.data.data.profile_picture_path || null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("ERROR FETCH DATA AGENT:", err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
}, [id, form]);
|
||||||
|
|
||||||
|
const handleFileChange = (e: any) => {
|
||||||
|
const selected = e.target.files[0];
|
||||||
|
if (selected) {
|
||||||
|
setFile(selected);
|
||||||
|
setPreview(URL.createObjectURL(selected));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFile = () => {
|
||||||
|
setFile(null);
|
||||||
|
setPreview(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: AgentFormValues) => {
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("name", data.fullName);
|
||||||
|
formData.append("job_title", data.position);
|
||||||
|
formData.append("phone", data.phone);
|
||||||
|
|
||||||
|
data.roles.forEach((role) => {
|
||||||
|
formData.append("agent_type", role);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
formData.append("file", file);
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateAgent(id, formData);
|
||||||
|
|
||||||
|
successSubmit("/admin/agent");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("ERROR UPDATE AGENT:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function successSubmit(redirect: string) {
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Data Berhasil Diupdate",
|
||||||
|
icon: "success",
|
||||||
|
confirmButtonColor: "#3085d6",
|
||||||
|
confirmButtonText: "OK",
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.isConfirmed) router.push(redirect);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className="p-6 text-gray-600">Loading data...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white dark:bg-neutral-900 p-6 rounded-lg shadow-sm">
|
||||||
|
<h2 className="text-2xl font-bold text-teal-800 mb-6">Edit Agen</h2>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="fullName"
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Nama Lengkap *</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Masukkan Nama" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Jabatan */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="position"
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Jabatan *</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Contoh: Branch Manager" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Telepon */}
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="phone"
|
||||||
|
rules={{ required: true }}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>No Telp *</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Contoh: 0812xxxx" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ROLES */}
|
||||||
|
<div>
|
||||||
|
<FormLabel>Pilih Jenis Agen *</FormLabel>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-6 mt-3">
|
||||||
|
{agentTypes.map((role) => (
|
||||||
|
<FormField
|
||||||
|
key={role}
|
||||||
|
control={form.control}
|
||||||
|
name="roles"
|
||||||
|
render={({ field }) => {
|
||||||
|
const selected = field.value || [];
|
||||||
|
return (
|
||||||
|
<FormItem className="flex flex-row items-center space-x-2">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={selected.includes(role)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
const updated = checked
|
||||||
|
? [...selected, role]
|
||||||
|
: selected.filter((i) => i !== role);
|
||||||
|
field.onChange(updated);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="font-normal">{role}</FormLabel>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* FOTO */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<FormLabel>Foto Agen</FormLabel>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
{preview && (
|
||||||
|
<div className="mt-3 relative w-28 h-28 rounded-md overflow-hidden border">
|
||||||
|
<img
|
||||||
|
src={preview}
|
||||||
|
alt="Preview"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleRemoveFile}
|
||||||
|
className="absolute top-1 right-1 bg-red-600 text-white p-1 rounded-full"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="bg-teal-700 hover:bg-teal-800 text-white"
|
||||||
|
onClick={() => document.getElementById("upload-photo")?.click()}
|
||||||
|
>
|
||||||
|
Upload Baru
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="upload-photo"
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* BUTTON */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-5">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="bg-gray-300 text-gray-700 hover:bg-gray-400"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-teal-700 hover:bg-teal-800 text-white py-3"
|
||||||
|
>
|
||||||
|
Simpan Perubahan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,191 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
DialogClose,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { UploadCloud, Check } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import router from "next/router";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import withReactContent from "sweetalert2-react-content";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
|
interface BannerDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSubmit?: (data: any) => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BannerDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onSubmit,
|
||||||
|
onSuccess,
|
||||||
|
}: BannerDialogProps) {
|
||||||
|
const [selectedOrder, setSelectedOrder] = useState<number | null>(1);
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const router = useRouter();
|
||||||
|
const MySwal = withReactContent(Swal);
|
||||||
|
|
||||||
|
const handleFileChange = (e: any) => {
|
||||||
|
const selected = e.target.files[0];
|
||||||
|
if (selected) setFile(selected);
|
||||||
|
};
|
||||||
|
|
||||||
|
const successSubmit = () => {
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Sukses",
|
||||||
|
icon: "success",
|
||||||
|
confirmButtonColor: "#3085d6",
|
||||||
|
confirmButtonText: "OK",
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!title || !file || !selectedOrder) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("title", title);
|
||||||
|
formData.append("position", selectedOrder.toString());
|
||||||
|
formData.append("description", "hardcode description dulu");
|
||||||
|
formData.append("status", "active");
|
||||||
|
formData.append("thumbnail_path", "path-hardcode.png");
|
||||||
|
formData.append("file", file);
|
||||||
|
|
||||||
|
if (onSubmit) {
|
||||||
|
await onSubmit(formData);
|
||||||
|
successSubmit();
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const orderOptions = [
|
||||||
|
{ id: 1, label: "Pertama" },
|
||||||
|
{ id: 2, label: "Kedua" },
|
||||||
|
{ id: 3, label: "Ketiga" },
|
||||||
|
{ id: 4, label: "Keempat" },
|
||||||
|
{ id: 5, label: "Kelima" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-lg p-0 overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<DialogHeader className="bg-[#1F6779] px-6 py-4">
|
||||||
|
<DialogTitle className="text-white text-lg">
|
||||||
|
Tambah Banner
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
{/* Judul Banner */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-gray-700">
|
||||||
|
Judul Banner <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Masukkan judul banner"
|
||||||
|
className="mt-1"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Upload File */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-gray-700">
|
||||||
|
Upload File <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<label
|
||||||
|
htmlFor="uploadFile"
|
||||||
|
className="mt-1 border-2 border-dashed border-gray-300 rounded-xl p-6 flex flex-col items-center justify-center text-gray-500 cursor-pointer hover:border-[#1F6779]/50 transition"
|
||||||
|
>
|
||||||
|
<UploadCloud className="w-10 h-10 text-[#1F6779] mb-2" />
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
Klik untuk upload atau drag & drop
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">PNG, JPG (max 2 MB)</p>
|
||||||
|
<input
|
||||||
|
id="uploadFile"
|
||||||
|
type="file"
|
||||||
|
accept="image/png, image/jpeg"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
{file && (
|
||||||
|
<p className="mt-2 text-xs text-[#1F6779] font-medium">
|
||||||
|
{file.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Urutan Banner */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-gray-700">
|
||||||
|
Urutan Banner <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<div className="grid grid-cols-5 gap-2 mt-2">
|
||||||
|
{orderOptions.map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedOrder(opt.id)}
|
||||||
|
className={cn(
|
||||||
|
"border rounded-lg py-2 flex flex-col items-center justify-center text-sm font-medium transition",
|
||||||
|
selectedOrder === opt.id
|
||||||
|
? "bg-[#1F6779]/20 border-[#1F6779] text-[#1F6779]"
|
||||||
|
: "bg-white border-gray-300 text-gray-600 hover:border-[#1F6779]/50"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{selectedOrder === opt.id && (
|
||||||
|
<Check className="w-4 h-4 text-[#1F6779] mb-1" />
|
||||||
|
)}
|
||||||
|
<span>{opt.id}</span>
|
||||||
|
<span className="text-xs font-normal text-gray-500">
|
||||||
|
{opt.label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<DialogFooter className="bg-gray-100 px-6 py-4 flex justify-end gap-3">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="bg-[#A9C5CC]/40 text-[#1F6779] border-none"
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button
|
||||||
|
className="bg-[#1F6779] text-white hover:bg-[#155864]"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,226 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
DialogClose,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { UploadCloud, X, Check } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import withReactContent from "sweetalert2-react-content";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
interface EditBannerDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onSubmit?: (data: FormData) => void;
|
||||||
|
bannerData?: {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
thumbnail_url?: string;
|
||||||
|
order: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditBannerDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
bannerData,
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (value: boolean) => void;
|
||||||
|
bannerData: any;
|
||||||
|
onSubmit?: (data: FormData, id: number) => void;
|
||||||
|
}) {
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [preview, setPreview] = useState<string | null>(null);
|
||||||
|
const [selectedOrder, setSelectedOrder] = useState<number | null>(1);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const MySwal = withReactContent(Swal);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (bannerData) {
|
||||||
|
setTitle(bannerData.title || "");
|
||||||
|
setPreview(bannerData.thumbnail_url || null);
|
||||||
|
setSelectedOrder(bannerData.position ? Number(bannerData.position) : 1);
|
||||||
|
}
|
||||||
|
}, [bannerData]);
|
||||||
|
|
||||||
|
const handleFileChange = (e: any) => {
|
||||||
|
const selected = e.target.files[0];
|
||||||
|
if (selected) {
|
||||||
|
setFile(selected);
|
||||||
|
setPreview(URL.createObjectURL(selected));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFile = () => {
|
||||||
|
setFile(null);
|
||||||
|
setPreview(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const successSubmit = () => {
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Berhasil diperbarui!",
|
||||||
|
icon: "success",
|
||||||
|
confirmButtonColor: "#3085d6",
|
||||||
|
confirmButtonText: "OK",
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) router.refresh();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!title || !selectedOrder || !bannerData?.id) return;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("title", title);
|
||||||
|
formData.append("position", selectedOrder.toString());
|
||||||
|
formData.append("description", "dummy description");
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
formData.append("file", file);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onSubmit?.(formData, bannerData.id);
|
||||||
|
successSubmit();
|
||||||
|
onOpenChange(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error submit:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const orderOptions = [
|
||||||
|
{ id: 1, label: "Pertama" },
|
||||||
|
{ id: 2, label: "Kedua" },
|
||||||
|
{ id: 3, label: "Ketiga" },
|
||||||
|
{ id: 4, label: "Keempat" },
|
||||||
|
{ id: 5, label: "Kelima" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-lg p-0 overflow-hidden">
|
||||||
|
{/* Header */}
|
||||||
|
<DialogHeader className="bg-[#1F6779] px-6 py-4">
|
||||||
|
<DialogTitle className="text-white text-lg">Edit Banner</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
{/* Judul Banner */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-gray-700">
|
||||||
|
Judul Banner <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
placeholder="Masukkan judul banner"
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Upload File */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-gray-700">Upload File (optional)</Label>
|
||||||
|
<label
|
||||||
|
htmlFor="uploadFileEdit"
|
||||||
|
className="mt-1 border-2 border-dashed border-gray-300 rounded-xl p-6 flex flex-col items-center justify-center text-gray-500 cursor-pointer hover:border-[#1F6779]/50 transition"
|
||||||
|
>
|
||||||
|
<UploadCloud className="w-10 h-10 text-[#1F6779] mb-2" />
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
Klik untuk upload atau drag & drop
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">PNG, JPG (max 2 MB)</p>
|
||||||
|
<input
|
||||||
|
id="uploadFileEdit"
|
||||||
|
type="file"
|
||||||
|
accept="image/png, image/jpeg"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{preview && (
|
||||||
|
<div className="mt-3 relative w-28 h-28 rounded-md overflow-hidden border">
|
||||||
|
<img
|
||||||
|
src={preview}
|
||||||
|
alt="Preview"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleRemoveFile}
|
||||||
|
className="absolute top-1 right-1 bg-red-600 text-white p-1 rounded-full"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Urutan Banner */}
|
||||||
|
<div>
|
||||||
|
<Label className="text-gray-700">
|
||||||
|
Urutan Banner <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<div className="grid grid-cols-5 gap-2 mt-2">
|
||||||
|
{orderOptions.map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedOrder(opt.id)}
|
||||||
|
className={cn(
|
||||||
|
"border rounded-lg py-2 flex flex-col items-center justify-center text-sm font-medium transition",
|
||||||
|
selectedOrder === opt.id
|
||||||
|
? "bg-[#1F6779]/20 border-[#1F6779] text-[#1F6779]"
|
||||||
|
: "bg-white border-gray-300 text-gray-600 hover:border-[#1F6779]/50"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{selectedOrder === opt.id && (
|
||||||
|
<Check className="w-4 h-4 text-[#1F6779] mb-1" />
|
||||||
|
)}
|
||||||
|
<span>{opt.id}</span>
|
||||||
|
<span className="text-xs font-normal text-gray-500">
|
||||||
|
{opt.label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<DialogFooter className="bg-gray-100 px-6 py-4 flex justify-end gap-3">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="bg-[#A9C5CC]/40 text-[#1F6779] border-none"
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<Button
|
||||||
|
className="bg-[#1F6779] text-white hover:bg-[#155864]"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import * as z from "zod";
|
||||||
|
|
||||||
|
import { Upload, Plus, UploadCloud } from "lucide-react";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||||
|
import { Label } from "@radix-ui/react-dropdown-menu";
|
||||||
|
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { createProduct } from "@/service/product";
|
||||||
|
import withReactContent from "sweetalert2-react-content";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
name: z.string().min(1, "Nama produk wajib diisi"),
|
||||||
|
variant: z.string().min(1, "Tipe varian wajib diisi"),
|
||||||
|
price: z.coerce.number().min(1, "Harga produk wajib diisi"),
|
||||||
|
banner: z.instanceof(FileList).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function AddProductForm() {
|
||||||
|
const [colors, setColors] = useState([{ id: 1 }]);
|
||||||
|
const [selectedColor, setSelectedColor] = useState<string | null>(null);
|
||||||
|
const [specs, setSpecs] = useState([{ id: 1 }]);
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const router = useRouter();
|
||||||
|
const MySwal = withReactContent(Swal);
|
||||||
|
|
||||||
|
const handleFileChange = (e: any) => {
|
||||||
|
const selected = e.target.files[0];
|
||||||
|
if (selected) setFile(selected);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddSpec = () => {
|
||||||
|
setSpecs((prev) => [...prev, { id: prev.length + 1 }]);
|
||||||
|
};
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: z.infer<typeof formSchema>) => {
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("title", data.name);
|
||||||
|
formData.append("variant", data.variant);
|
||||||
|
formData.append("price", data.price.toString());
|
||||||
|
// if (data.banner && data.banner.length > 0) {
|
||||||
|
// formData.append("thumbnail_path", data.banner[0]);
|
||||||
|
// }
|
||||||
|
if (file) {
|
||||||
|
formData.append("file", file);
|
||||||
|
}
|
||||||
|
const colorsArray = colors.map((c) => selectedColor || "");
|
||||||
|
formData.append("colors", JSON.stringify(colorsArray));
|
||||||
|
|
||||||
|
const res = await createProduct(formData);
|
||||||
|
|
||||||
|
console.log("API Success:", res);
|
||||||
|
successSubmit("/admin/product");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Submit Error:", error);
|
||||||
|
alert("Gagal mengirim produk");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function successSubmit(redirect: any) {
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Sukses",
|
||||||
|
icon: "success",
|
||||||
|
confirmButtonColor: "#3085d6",
|
||||||
|
confirmButtonText: "OK",
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
router.push(redirect);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddColor = () => {
|
||||||
|
setColors((prev) => [...prev, { id: prev.length + 1 }]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full max-w-full mx-auto shadow-md border-none">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl font-bold text-teal-900">
|
||||||
|
Form Tambah Produk Baru
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||||
|
<div className="grid md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>Nama Produk *</Label>
|
||||||
|
<Input placeholder="Masukkan Nama Produk" {...register("name")} />
|
||||||
|
{errors.name && (
|
||||||
|
<p className="text-sm text-red-500 mt-1">
|
||||||
|
{errors.name.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>Tipe Varian *</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Contoh: AWD, SHS, EV"
|
||||||
|
{...register("variant")}
|
||||||
|
/>
|
||||||
|
{errors.variant && (
|
||||||
|
<p className="text-sm text-red-500 mt-1">
|
||||||
|
{errors.variant.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>Harga Produk *</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Masukkan Harga Produk"
|
||||||
|
{...register("price")}
|
||||||
|
/>
|
||||||
|
{errors.price && (
|
||||||
|
<p className="text-sm text-red-500 mt-1">
|
||||||
|
{errors.price.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-gray-700">
|
||||||
|
Upload Banner <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<label
|
||||||
|
htmlFor="uploadFile"
|
||||||
|
className="mt-1 border-2 border-dashed border-gray-300 rounded-xl p-6 flex flex-col items-center justify-center text-gray-500 cursor-pointer hover:border-[#1F6779]/50 transition"
|
||||||
|
>
|
||||||
|
<UploadCloud className="w-10 h-10 text-[#1F6779] mb-2" />
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
Klik untuk upload atau drag & drop
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400 mt-1">PNG, JPG (max 2 MB)</p>
|
||||||
|
<input
|
||||||
|
id="uploadFile"
|
||||||
|
type="file"
|
||||||
|
accept="image/png, image/jpeg"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
{file && (
|
||||||
|
<p className="mt-2 text-xs text-[#1F6779] font-medium">
|
||||||
|
{file.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Upload Produk */}
|
||||||
|
<div>
|
||||||
|
<Label>Upload Produk *</Label>
|
||||||
|
{colors.map((color, index) => (
|
||||||
|
<div
|
||||||
|
key={color.id}
|
||||||
|
className="border p-4 rounded-lg mt-4 space-y-4"
|
||||||
|
>
|
||||||
|
<Label className="text-sm font-semibold">
|
||||||
|
Pilih Warna {index + 1}
|
||||||
|
</Label>
|
||||||
|
<Input placeholder="Contoh: Silver or #E2E2E2" />
|
||||||
|
|
||||||
|
{/* Pilihan Warna */}
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{[
|
||||||
|
"#1E4E52",
|
||||||
|
"#597E8D",
|
||||||
|
"#6B6B6B",
|
||||||
|
"#BEBEBE",
|
||||||
|
"#E2E2E2",
|
||||||
|
"#F4F4F4",
|
||||||
|
"#FFFFFF",
|
||||||
|
"#F9360A",
|
||||||
|
"#9A2A00",
|
||||||
|
"#7A1400",
|
||||||
|
"#4B0200",
|
||||||
|
"#B48B84",
|
||||||
|
"#FFA598",
|
||||||
|
].map((colorCode) => (
|
||||||
|
<button
|
||||||
|
key={colorCode}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedColor(colorCode)}
|
||||||
|
className={`w-8 h-8 rounded-full border-2 transition ${
|
||||||
|
selectedColor === colorCode
|
||||||
|
? "border-teal-700 scale-110"
|
||||||
|
: "border-gray-200"
|
||||||
|
}`}
|
||||||
|
style={{ backgroundColor: colorCode }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Upload Foto Warna */}
|
||||||
|
<div>
|
||||||
|
<Label>Foto Warna {index + 1}</Label>
|
||||||
|
<div className="border-2 border-dashed rounded-lg flex flex-col items-center justify-center py-6 cursor-pointer hover:bg-gray-50 transition">
|
||||||
|
<Upload className="w-6 h-6 text-gray-400 mb-2" />
|
||||||
|
<p className="text-sm text-gray-500 text-center">
|
||||||
|
Klik untuk upload foto mobil warna ini
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400">PNG, JPG (max 5 MB)</p>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/png,image/jpeg"
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAddColor}
|
||||||
|
className="w-full bg-teal-800 hover:bg-teal-900 text-white mt-4"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-2" /> Tambah Warna Baru
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<Label className="font-semibold text-lg text-teal-900">
|
||||||
|
Spesifikasi Produk <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
{specs.map((spec, index) => (
|
||||||
|
<div key={spec.id} className="mt-4">
|
||||||
|
<Label className="text-sm font-semibold">
|
||||||
|
Judul Spesifikasi {index + 1}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Contoh: Mesin Turbo 1.6L, Interior Premium, Safety Features"
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Label className="text-sm font-semibold mt-4 block">
|
||||||
|
Foto Spesifikasi {index + 1}
|
||||||
|
</Label>
|
||||||
|
<div className="border-2 border-dashed rounded-lg flex flex-col items-center justify-center py-10 cursor-pointer hover:bg-gray-50 transition mt-1">
|
||||||
|
<Upload className="w-8 h-8 text-gray-400 mb-2" />
|
||||||
|
<p className="text-sm text-gray-500 text-center">
|
||||||
|
Klik untuk upload gambar spesifikasi
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400">PNG, JPG (max 5 MB)</p>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/png,image/jpeg"
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAddSpec}
|
||||||
|
className="w-full bg-teal-800 hover:bg-teal-900 text-white py-3 rounded-lg mt-6 flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
Tambahkan Spesifikasi Baru
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className=" bg-teal-800 hover:bg-teal-900 text-white mt-6"
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,336 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Upload, Plus, Settings } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardHeader, CardContent, CardTitle } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
export default function UpdateProductForm() {
|
||||||
|
const [specs, setSpecs] = useState([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Jaecoo 7 SHS Teknologi dan Exterior",
|
||||||
|
images: ["/spec1.jpg", "/spec2.jpg", "/spec3.jpg", "/spec4.jpg"],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
type ColorType = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
preview: string;
|
||||||
|
colorSelected: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [colors, setColors] = useState<ColorType[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "",
|
||||||
|
preview: "/car-1.png",
|
||||||
|
colorSelected: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "",
|
||||||
|
preview: "/car-2.png",
|
||||||
|
colorSelected: null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const palette = [
|
||||||
|
"#1E4E52",
|
||||||
|
"#597E8D",
|
||||||
|
"#6B6B6B",
|
||||||
|
"#BEBEBE",
|
||||||
|
"#E2E2E2",
|
||||||
|
"#F4F4F4",
|
||||||
|
"#FFFFFF",
|
||||||
|
"#F9360A",
|
||||||
|
"#9A2A00",
|
||||||
|
"#7A1400",
|
||||||
|
"#4B0200",
|
||||||
|
"#B48B84",
|
||||||
|
"#FFA598",
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleAddSpec = () => {
|
||||||
|
setSpecs((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
id: prev.length + 1,
|
||||||
|
title: "",
|
||||||
|
images: [],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddColor = () => {
|
||||||
|
setColors((p) => [
|
||||||
|
...p,
|
||||||
|
{
|
||||||
|
id: p.length + 1,
|
||||||
|
name: "",
|
||||||
|
preview: "/car-default.png",
|
||||||
|
colorSelected: null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
|
||||||
|
const [uploadTarget, setUploadTarget] = useState<{
|
||||||
|
type: "spec" | "color";
|
||||||
|
index: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const fileInputId = "file-upload-input";
|
||||||
|
|
||||||
|
const handleFileSelected = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.target.files?.[0];
|
||||||
|
if (!file || !uploadTarget) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const fileUrl = reader.result as string;
|
||||||
|
|
||||||
|
if (uploadTarget.type === "spec") {
|
||||||
|
setSpecs((prev) => {
|
||||||
|
const updated = [...prev];
|
||||||
|
updated[uploadTarget.index].images.push(fileUrl);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadTarget.type === "color") {
|
||||||
|
setColors((prev) => {
|
||||||
|
const updated = [...prev];
|
||||||
|
updated[uploadTarget.index].preview = fileUrl;
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
setIsUploadDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card className="w-full border-none shadow-md">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl font-bold text-teal-900">
|
||||||
|
Edit Produk
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-8">
|
||||||
|
<div className="grid md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>Nama Produk *</Label>
|
||||||
|
<Input defaultValue="JAECOO J7" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>Tipe Varian *</Label>
|
||||||
|
<Input defaultValue="SHS" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>Harga Produk *</Label>
|
||||||
|
<Input defaultValue="RP 599.000.000" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="font-semibold">Warna Produk *</Label>
|
||||||
|
|
||||||
|
{colors.map((item, index) => (
|
||||||
|
<div key={item.id} className="mt-6 border-b pb-6">
|
||||||
|
<Label>Pilih Warna {index + 1}</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Contoh: Silver / #E2E2E2"
|
||||||
|
className="mt-1"
|
||||||
|
defaultValue={item.name}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 mt-3">
|
||||||
|
<div className="w-10 h-10 rounded-full flex items-center justify-center bg-teal-900">
|
||||||
|
<Settings className="w-5 h-5 text-white" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{palette.map((colorCode) => (
|
||||||
|
<button
|
||||||
|
key={colorCode}
|
||||||
|
type="button"
|
||||||
|
style={{ backgroundColor: colorCode }}
|
||||||
|
className={`w-10 h-10 rounded-full border-2 transition ${
|
||||||
|
item.colorSelected === colorCode
|
||||||
|
? "border-teal-700 scale-110"
|
||||||
|
: "border-gray-300"
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
setColors((prev) => {
|
||||||
|
const updated = [...prev];
|
||||||
|
updated[index].colorSelected = colorCode;
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<Label className="font-semibold">
|
||||||
|
Foto Produk Warna {index + 1}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 mt-2">
|
||||||
|
<Image
|
||||||
|
src={item.preview}
|
||||||
|
alt="car color"
|
||||||
|
width={120}
|
||||||
|
height={80}
|
||||||
|
className="object-cover rounded-md border"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="bg-teal-800 hover:bg-teal-900 text-white"
|
||||||
|
onClick={() => {
|
||||||
|
setUploadTarget({ type: "color", index });
|
||||||
|
setIsUploadDialogOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Upload File Baru
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleAddColor}
|
||||||
|
className="w-full bg-teal-800 hover:bg-teal-900 text-white mt-4"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-2" /> Tambah Warna Baru
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-10">
|
||||||
|
<Label className="text-lg font-semibold text-teal-900">
|
||||||
|
Spesifikasi Produk <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
{specs.map((spec, index) => (
|
||||||
|
<div key={spec.id} className="mt-6">
|
||||||
|
<Label className="font-semibold text-sm">
|
||||||
|
Judul Spesifikasi {index + 1}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
defaultValue={spec.title}
|
||||||
|
placeholder="Masukkan Judul Spesifikasi"
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Label className="font-semibold text-sm mt-4 block">
|
||||||
|
Foto Spesifikasi {index + 1}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-4 mt-2">
|
||||||
|
{spec.images.map((img, i) => (
|
||||||
|
<Image
|
||||||
|
key={i}
|
||||||
|
src={img}
|
||||||
|
width={120}
|
||||||
|
height={120}
|
||||||
|
alt="spec"
|
||||||
|
className="rounded-lg border object-cover"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="bg-teal-800 hover:bg-teal-900 text-white"
|
||||||
|
onClick={() => {
|
||||||
|
setUploadTarget({ type: "spec", index });
|
||||||
|
setIsUploadDialogOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Upload File Baru
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="my-6 border-b"></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleAddSpec}
|
||||||
|
className="w-full bg-teal-800 hover:bg-teal-900 text-white flex items-center justify-center gap-2 py-4 rounded-xl"
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
Tambahkan Spesifikasi Baru
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button className=" bg-teal-800 hover:bg-teal-900 text-white py-3">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Dialog open={isUploadDialogOpen} onOpenChange={setIsUploadDialogOpen}>
|
||||||
|
<DialogContent className="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-teal-900 font-semibold">
|
||||||
|
Upload File
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="border-2 border-dashed rounded-xl p-8 text-center cursor-pointer"
|
||||||
|
onClick={() => document.getElementById(fileInputId)?.click()}
|
||||||
|
>
|
||||||
|
<Upload className="w-10 h-10 text-gray-400 mx-auto mb-3" />
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Klik untuk upload atau drag & drop
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400">PNG, JPG (max 2 MB)</p>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id={fileInputId}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileSelected}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="flex gap-3 pt-4">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="bg-slate-200"
|
||||||
|
onClick={() => setIsUploadDialogOpen(false)}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => document.getElementById(fileInputId)?.click()}
|
||||||
|
className="bg-teal-800 hover:bg-teal-900 text-white"
|
||||||
|
>
|
||||||
|
Pilih File
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Upload } from "lucide-react";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { createPromotion } from "@/service/promotion";
|
||||||
|
import withReactContent from "sweetalert2-react-content";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
|
export default function AddPromoForm() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
|
||||||
|
const MySwal = withReactContent(Swal);
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const selected = e.target.files?.[0];
|
||||||
|
if (selected) {
|
||||||
|
setFile(selected);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
alert("File wajib diupload!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("title", title);
|
||||||
|
formData.append("description", title);
|
||||||
|
formData.append("file", file);
|
||||||
|
|
||||||
|
await createPromotion(formData);
|
||||||
|
|
||||||
|
successSubmit("/admin/promotion");
|
||||||
|
} catch (err) {
|
||||||
|
console.error("ERROR CREATE PROMO:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function successSubmit(redirect: string) {
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Sukses",
|
||||||
|
icon: "success",
|
||||||
|
confirmButtonColor: "#3085d6",
|
||||||
|
confirmButtonText: "OK",
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
router.push(redirect);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full max-w-full mx-auto shadow-md border-none p-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-2xl font-bold text-teal-900">
|
||||||
|
Form Tambah Promo
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
<form className="space-y-6" onSubmit={onSubmit}>
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm font-semibold">
|
||||||
|
Judul Promo <span className="text-red-500">*</span>
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Masukkan Judul Promo"
|
||||||
|
className="mt-1"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm font-semibold">Upload File *</Label>
|
||||||
|
|
||||||
|
<label className="border-2 border-dashed rounded-lg flex flex-col items-center justify-center py-16 cursor-pointer hover:bg-gray-50 transition mt-2">
|
||||||
|
<Upload className="w-10 h-10 text-teal-800 mb-3" />
|
||||||
|
<p className="text-base text-gray-600 text-center">
|
||||||
|
Klik untuk upload atau drag & drop
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">PNG, JPG (max 2 MB)</p>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/png,image/jpeg"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{file && (
|
||||||
|
<p className="text-xs text-teal-800 mt-2 font-medium">
|
||||||
|
{file.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="bg-teal-800 hover:bg-teal-900 text-white px-8 py-6 text-lg mt-4"
|
||||||
|
>
|
||||||
|
Tambahkan Promo
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,18 @@ interface RetractingSidebarProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const sidebarSections = [
|
const sidebarSections = [
|
||||||
|
// {
|
||||||
|
// title: "Dashboard",
|
||||||
|
// items: [
|
||||||
|
// {
|
||||||
|
// title: "Dashboard",
|
||||||
|
// icon: () => (
|
||||||
|
// <Icon icon="material-symbols:dashboard" className="text-lg" />
|
||||||
|
// ),
|
||||||
|
// link: "/admin/dashboard",
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
title: "Dashboard",
|
title: "Dashboard",
|
||||||
items: [
|
items: [
|
||||||
|
|
@ -28,20 +40,15 @@ const sidebarSections = [
|
||||||
),
|
),
|
||||||
link: "/admin/dashboard",
|
link: "/admin/dashboard",
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Content Management",
|
|
||||||
items: [
|
|
||||||
{
|
{
|
||||||
title: "Articles",
|
title: "Banner",
|
||||||
icon: () => <Icon icon="ri:article-line" className="text-lg" />,
|
icon: () => <Icon icon="ri:article-line" className="text-lg" />,
|
||||||
link: "/admin/article",
|
link: "/admin/banner",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Categories",
|
title: "Produk",
|
||||||
icon: () => <Icon icon="famicons:list-outline" className="text-lg" />,
|
icon: () => <Icon icon="famicons:car-outline" className="text-lg" />,
|
||||||
link: "/admin/master-category",
|
link: "/admin/product",
|
||||||
},
|
},
|
||||||
// {
|
// {
|
||||||
// title: "Majalah",
|
// title: "Majalah",
|
||||||
|
|
@ -49,28 +56,39 @@ const sidebarSections = [
|
||||||
// link: "/admin/magazine",
|
// link: "/admin/magazine",
|
||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
title: "Advertisements",
|
title: "Agen",
|
||||||
icon: () => <Icon icon="ic:round-ads-click" className="text-lg" />,
|
icon: () => <Icon icon="ic:people-outline" className="text-lg" />,
|
||||||
link: "/admin/advertise",
|
link: "/admin/agent",
|
||||||
},
|
|
||||||
// {
|
|
||||||
// title: "Komentar",
|
|
||||||
// icon: () => <Icon icon="material-symbols:comment-outline-rounded" className="text-lg" />,
|
|
||||||
// link: "/admin/komentar",
|
|
||||||
// },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "System",
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: "Static Pages",
|
|
||||||
icon: () => <Icon icon="fluent-mdl2:page-solid" className="text-lg" />,
|
|
||||||
link: "/admin/static-page",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "User Management",
|
title: "Promo",
|
||||||
icon: () => <Icon icon="ph:users-three-fill" className="text-lg" />,
|
icon: () => <Icon icon="bx:food-menu" className="text-lg" />,
|
||||||
|
link: "/admin/promotion",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Galeri",
|
||||||
|
icon: () => <Icon icon="ion:image-outline" className="text-lg" />,
|
||||||
|
link: "/admin/galery",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Services",
|
||||||
|
icon: () => <Icon icon="et:tools-2" width="18" height="18" />,
|
||||||
|
link: "/admin/services",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Layanan Konsumen",
|
||||||
|
icon: () => (
|
||||||
|
<Icon
|
||||||
|
icon="streamline-ultimate:headphones-customer-support-question"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
link: "/admin/costumer-support",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Manajemen User",
|
||||||
|
icon: () => <Icon icon="mdi:contact" width="18" height="18" />,
|
||||||
link: "/admin/master-user",
|
link: "/admin/master-user",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -192,26 +210,26 @@ const SidebarContent = ({
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<div className="flex flex-col space-y-6">
|
<div className="flex flex-col space-y-6">
|
||||||
<div className="flex items-center justify-between px-4 py-6">
|
<div className="flex items-center justify-between px-4 py-6 bg-[#185567]">
|
||||||
<Link href="/" className="flex items-center space-x-3">
|
<Link href="/" className="flex items-center space-x-3">
|
||||||
<div className="relative">
|
{/* <div className="relative">
|
||||||
<img
|
<img
|
||||||
src="/masjaecoo.png"
|
src="/masjaecoo.png"
|
||||||
className="w-28 h-10 bg-black p-1 dark:bg-transparent"
|
className="w-28 h-10 bg-black p-1 dark:bg-transparent"
|
||||||
/>
|
/>
|
||||||
<div className="absolute opacity-20"></div>
|
<div className="absolute opacity-20"></div>
|
||||||
</div>
|
</div> */}
|
||||||
{open && (
|
{open && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: -10 }}
|
initial={{ opacity: 0, x: -10 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ delay: 0.1 }}
|
transition={{ delay: 0.1 }}
|
||||||
className="flex flex-col"
|
className="flex flex-row"
|
||||||
>
|
>
|
||||||
<span className="text-lg font-bold bg-gradient-to-r from-slate-800 to-slate-600 bg-clip-text text-black dark:text-white">
|
<p className="text-lg font-bold bg-gradient-to-r from-slate-800 to-slate-600 bg-clip-text text-white dark:text-white">
|
||||||
Jaecoo
|
JAECOO <span className="text-lg font-normal">Admin</span>
|
||||||
</span>
|
</p>
|
||||||
<span className="text-xs text-slate-500">Admin Panel</span>
|
{/* <span className="text-xs text-slate-500">Admin Panel</span> */}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -350,7 +368,7 @@ const SidebarContent = ({
|
||||||
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-blue-500 to-purple-500 flex items-center justify-center text-white font-semibold text-sm shadow-lg">
|
<div className="w-10 h-10 rounded-full bg-gradient-to-r from-blue-500 to-purple-500 flex items-center justify-center text-white font-semibold text-sm shadow-lg">
|
||||||
A
|
A
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-green-500 rounded-full border-2 border-white"></div>
|
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-[#1F6779] rounded-full border-2 border-white"></div>
|
||||||
</div>
|
</div>
|
||||||
{open && (
|
{open && (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
import {
|
|
||||||
DashboardCommentIcon,
|
|
||||||
DashboardConnectIcon,
|
|
||||||
DashboardShareIcon,
|
|
||||||
DashboardSpeecIcon,
|
|
||||||
DashboardUserIcon,
|
|
||||||
} from "@/components/icons/dashboard-icon";
|
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import Link from "next/link";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Article } from "@/types/globals";
|
import { Article } from "@/types/globals";
|
||||||
import {
|
import {
|
||||||
|
|
@ -17,13 +9,36 @@ import {
|
||||||
getUserLevelDataStat,
|
getUserLevelDataStat,
|
||||||
} from "@/service/article";
|
} from "@/service/article";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import Image from "next/image";
|
|
||||||
import { convertDateFormat, convertDateFormatNoTime } from "@/utils/global";
|
|
||||||
import "react-datepicker/dist/react-datepicker.css";
|
import "react-datepicker/dist/react-datepicker.css";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import ApexChartColumn from "@/components/main/dashboard/chart/column-chart";
|
|
||||||
import CustomPagination from "@/components/layout/custom-pagination";
|
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
import {
|
||||||
|
Blocks,
|
||||||
|
Check,
|
||||||
|
CheckCircle,
|
||||||
|
CheckCircle2,
|
||||||
|
Eye,
|
||||||
|
Info,
|
||||||
|
TimerIcon,
|
||||||
|
Upload,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
type ArticleData = Article & {
|
type ArticleData = Article & {
|
||||||
no: number;
|
no: number;
|
||||||
|
|
@ -60,11 +75,87 @@ export default function DashboardContainer() {
|
||||||
|
|
||||||
const [startDateValue, setStartDateValue] = useState(new Date());
|
const [startDateValue, setStartDateValue] = useState(new Date());
|
||||||
const [analyticsView, setAnalyticView] = useState<string[]>([]);
|
const [analyticsView, setAnalyticView] = useState<string[]>([]);
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const [selectedItem, setSelectedItem] = useState<any>(null);
|
||||||
const options = [
|
const options = [
|
||||||
{ label: "Comment", value: "comment" },
|
{ label: "Comment", value: "comment" },
|
||||||
{ label: "View", value: "view" },
|
{ label: "View", value: "view" },
|
||||||
{ label: "Share", value: "share" },
|
{ label: "Share", value: "share" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const activities = [
|
||||||
|
{
|
||||||
|
no: 1,
|
||||||
|
tanggal: "10/11/2024",
|
||||||
|
jenis: "Banner",
|
||||||
|
judul: "New Promo JAECOO",
|
||||||
|
status: "Menunggu",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 2,
|
||||||
|
tanggal: "10/11/2024",
|
||||||
|
jenis: "Agen",
|
||||||
|
judul: "Foto Budi Santoso",
|
||||||
|
status: "Disetujui",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 3,
|
||||||
|
tanggal: "09/11/2024",
|
||||||
|
jenis: "Produk",
|
||||||
|
judul: "JAECOO J7 AWD Update",
|
||||||
|
status: "Disetujui",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 4,
|
||||||
|
tanggal: "09/11/2024",
|
||||||
|
jenis: "Dealer",
|
||||||
|
judul: "Dealer Jakarta Selatan",
|
||||||
|
status: "Disetujui",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 5,
|
||||||
|
tanggal: "08/11/2024",
|
||||||
|
jenis: "Dokumen",
|
||||||
|
judul: "Brosur JAECOO J8",
|
||||||
|
status: "Ditolak",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
no: 6,
|
||||||
|
tanggal: "08/11/2024",
|
||||||
|
jenis: "Banner",
|
||||||
|
judul: "Hero Banner Akhir Tahun",
|
||||||
|
status: "Menunggu",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const notifications = [
|
||||||
|
{
|
||||||
|
icon: "✅",
|
||||||
|
text: 'Upload "JAECOO J7 AWD Update" telah disetujui oleh Admin Manager',
|
||||||
|
time: "2 jam yang lalu",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "❌",
|
||||||
|
text: 'Upload "Brosur JAECOO J8" ditolak. Alasan: Resolusi gambar terlalu rendah.',
|
||||||
|
time: "2 jam yang lalu",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "✅",
|
||||||
|
text: 'Update "Dealer Jakarta Selatan" telah disetujui',
|
||||||
|
time: "1 hari yang lalu",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "✅",
|
||||||
|
text: 'Upload "Foto Budi Santoso" telah disetujui',
|
||||||
|
time: "1 hari yang lalu",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "ℹ️",
|
||||||
|
text: "Sistem akan maintenance pada Minggu, 12 November 2024 pukul 00.00 - 04.00 WIB",
|
||||||
|
time: "1 hari yang lalu",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const handleChange = (value: string, checked: boolean) => {
|
const handleChange = (value: string, checked: boolean) => {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
setAnalyticView([...analyticsView, value]);
|
setAnalyticView([...analyticsView, value]);
|
||||||
|
|
@ -179,8 +270,14 @@ export default function DashboardContainer() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
|
<div className="pl-3">
|
||||||
|
<h1 className="text-[#1F6779] text-2xl font-semibold">
|
||||||
|
Dashboard Utama
|
||||||
|
</h1>
|
||||||
|
<p>Ringkasan status aktivitas dan upload anda</p>
|
||||||
|
</div>
|
||||||
{/* Stats Cards */}
|
{/* Stats Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-8 gap-6">
|
||||||
{/* User Profile Card */}
|
{/* User Profile Card */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="col-span-1 md:col-span-2 bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
|
className="col-span-1 md:col-span-2 bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
|
||||||
|
|
@ -190,200 +287,363 @@ export default function DashboardContainer() {
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="text-xl font-bold text-slate-800">{fullname}</h3>
|
<h3 className="text-xl font-bold text-slate-800">
|
||||||
<p className="text-slate-600">{username}</p>
|
Total Upload Hari ini
|
||||||
<div className="flex space-x-6 pt-2">
|
</h3>
|
||||||
|
<p className="text-slate-600 text-lg">2</p>
|
||||||
|
{/* <div className="flex space-x-6 pt-2">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-2xl font-bold text-blue-600">
|
<p className="text-2xl font-bold text-blue-600">
|
||||||
{summary?.totalToday}
|
{summary?.totalToday}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-slate-500">Today</p>
|
<p className="text-sm text-slate-500">2</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
</div> */}
|
||||||
<p className="text-2xl font-bold text-purple-600">
|
|
||||||
{summary?.totalThisWeek}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-slate-500">This Week</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 bg-gradient-to-br from-blue-50 to-purple-50 rounded-xl">
|
<div className="p-3 bg-gradient-to-br from-blue-50 to-purple-50 rounded-xl">
|
||||||
<DashboardUserIcon size={60} className="text-black" />
|
<Upload size={50} className="text-black" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Total Posts */}
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
|
className="col-span-1 md:col-span-2 bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.2 }}
|
transition={{ delay: 0.1 }}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center text-center space-y-3">
|
<div className="flex justify-between items-start">
|
||||||
<div className="p-3 bg-gradient-to-br from-green-50 to-emerald-50 rounded-xl">
|
<div className="space-y-2">
|
||||||
<DashboardSpeecIcon className="text-black" />
|
<h3 className="text-xl font-bold text-slate-800">
|
||||||
|
Menunggu Persetujuan
|
||||||
|
</h3>
|
||||||
|
<p className="text-slate-600 text-lg">2</p>
|
||||||
|
{/* <div className="flex space-x-6 pt-2">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-2xl font-bold text-blue-600">
|
||||||
|
{summary?.totalToday}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-slate-500">2</p>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="p-3 bg-gradient-to-br from-yellow-100 to-yellow-100 rounded-xl">
|
||||||
<p className="text-3xl font-bold text-slate-800">
|
<TimerIcon size={50} className="text-black" />
|
||||||
{summary?.totalAll}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-slate-500">Total Posts</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Total Views */}
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
|
className="col-span-1 md:col-span-2 bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.3 }}
|
transition={{ delay: 0.1 }}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center text-center space-y-3">
|
<div className="flex justify-between items-start">
|
||||||
<div className="p-3 bg-gradient-to-br from-blue-50 to-cyan-50 rounded-xl">
|
<div className="space-y-2">
|
||||||
<DashboardConnectIcon className="text-black" />
|
<h3 className="text-xl font-bold text-slate-800">Disetujui</h3>
|
||||||
|
<p className="text-slate-600 text-lg">2</p>
|
||||||
|
{/* <div className="flex space-x-6 pt-2">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-2xl font-bold text-blue-600">
|
||||||
|
{summary?.totalToday}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-slate-500">2</p>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="p-3 bg-gradient-to-br from-green-100 to-green-100 rounded-xl">
|
||||||
<p className="text-3xl font-bold text-slate-800">
|
<CheckCircle size={50} className="text-black" />
|
||||||
{summary?.totalViews}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-slate-500">Total Views</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Total Shares */}
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
|
className="col-span-1 md:col-span-2 bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.4 }}
|
transition={{ delay: 0.1 }}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center text-center space-y-3">
|
<div className="flex justify-between items-start">
|
||||||
<div className="p-3 bg-gradient-to-br from-purple-50 to-pink-50 rounded-xl">
|
<div className="space-y-2">
|
||||||
<DashboardShareIcon className="text-black" />
|
<h3 className="text-xl font-bold text-slate-800">Ditolak</h3>
|
||||||
|
<p className="text-slate-600 text-lg">2</p>
|
||||||
|
{/* <div className="flex space-x-6 pt-2">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-2xl font-bold text-blue-600">
|
||||||
|
{summary?.totalToday}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-slate-500">2</p>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="p-3 bg-gradient-to-br from-red-100 to-red-100 rounded-xl">
|
||||||
<p className="text-3xl font-bold text-slate-800">
|
<Blocks size={50} className="text-black" />
|
||||||
{summary?.totalShares}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-slate-500">Total Shares</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Total Comments */}
|
|
||||||
<motion.div
|
|
||||||
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6 hover:shadow-xl transition-all duration-300"
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
transition={{ delay: 0.5 }}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center text-center space-y-3">
|
|
||||||
<div className="p-3 bg-gradient-to-br from-orange-50 to-red-50 rounded-xl">
|
|
||||||
<DashboardCommentIcon size={40} className="text-black" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-3xl font-bold text-slate-800">
|
|
||||||
{summary?.totalComments}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-slate-500">Total Comments</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content Section */}
|
{/* Content Section */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
<div className="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-8">
|
||||||
{/* Analytics Chart */}
|
{/* Aktivitas Terakhir */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6"
|
|
||||||
initial={{ opacity: 0, x: -20 }}
|
initial={{ opacity: 0, x: -20 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ delay: 0.6 }}
|
transition={{ delay: 0.5 }}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="bg-white shadow-xl ">
|
||||||
<h3 className="text-lg font-semibold text-slate-800">
|
<p className=" text-lg p-3 bg-cyan-900 text-white rounded-t-lg">
|
||||||
Analytics Overview
|
Aktivitas Terakhir
|
||||||
</h3>
|
</p>
|
||||||
<div className="flex space-x-4">
|
|
||||||
{options.map((option) => (
|
<Table className="p-3">
|
||||||
<label
|
<TableHeader className="bg-gradient-to-r from-[#BCD4DF] to-[#BCD4DF]">
|
||||||
key={option.value}
|
<TableRow>
|
||||||
className="flex items-center space-x-2"
|
<TableHead className="text-[#008080] w-[40px]">No</TableHead>
|
||||||
>
|
<TableHead className="text-[#008080]">Tanggal</TableHead>
|
||||||
<Checkbox
|
<TableHead className="text-[#008080]">Jenis Konten</TableHead>
|
||||||
checked={analyticsView.includes(option.value)}
|
<TableHead className="text-[#008080]">Judul / Nama</TableHead>
|
||||||
onCheckedChange={(checked) =>
|
<TableHead className="text-[#008080]">Status</TableHead>
|
||||||
handleChange(option.value, checked as boolean)
|
<TableHead className="text-[#008080] text-center">
|
||||||
}
|
Aksi
|
||||||
/>
|
</TableHead>
|
||||||
<span className="text-sm text-slate-600">{option.label}</span>
|
</TableRow>
|
||||||
</label>
|
</TableHeader>
|
||||||
))}
|
<TableBody>
|
||||||
|
{activities.map((item) => (
|
||||||
|
<TableRow key={item.no}>
|
||||||
|
<TableCell>{item.no}</TableCell>
|
||||||
|
<TableCell>{item.tanggal}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="bg-slate-100 text-slate-700"
|
||||||
|
>
|
||||||
|
{item.jenis}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="font-medium">{item.judul}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{item.status === "Disetujui" && (
|
||||||
|
<Badge className="bg-green-100 text-green-700">
|
||||||
|
Disetujui
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{item.status === "Menunggu" && (
|
||||||
|
<Badge className="bg-yellow-100 text-yellow-700">
|
||||||
|
Menunggu
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{item.status === "Ditolak" && (
|
||||||
|
<Badge className="bg-red-100 text-red-700">
|
||||||
|
Ditolak
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedItem(item);
|
||||||
|
setDialogOpen(true);
|
||||||
|
}}
|
||||||
|
className="text-blue-600 font-medium text-center cursor-pointer hover:underline"
|
||||||
|
>
|
||||||
|
Lihat
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
|
<DialogContent className="max-w-lg rounded-xl p-0 overflow-hidden">
|
||||||
|
{selectedItem && (
|
||||||
|
<>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-cyan-900 text-white p-4 flex flex-col items-start justify-between">
|
||||||
|
<DialogTitle className="text-lg font-semibold">
|
||||||
|
{selectedItem.judul}
|
||||||
|
</DialogTitle>
|
||||||
|
{selectedItem.status === "Disetujui" && (
|
||||||
|
<Badge className="bg-green-100 text-green-700">
|
||||||
|
Disetujui
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{selectedItem.status === "Menunggu" && (
|
||||||
|
<Badge className="bg-yellow-100 text-yellow-700">
|
||||||
|
Menunggu
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{selectedItem.status === "Ditolak" && (
|
||||||
|
<Badge className="bg-red-100 text-red-700">
|
||||||
|
Ditolak
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<div className="p-5 space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||||
|
<div className="bg-slate-50 p-3 rounded-lg border">
|
||||||
|
<p className="text-slate-500">Tanggal Upload</p>
|
||||||
|
<p className="font-medium">{selectedItem.tanggal}</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-50 p-3 rounded-lg border">
|
||||||
|
<p className="text-slate-500">Ukuran File</p>
|
||||||
|
<p className="font-medium">2.4 MB</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-50 p-3 rounded-lg border">
|
||||||
|
<p className="text-slate-500">Diupload Oleh</p>
|
||||||
|
<p className="font-medium">Operator1</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-50 p-3 rounded-lg border">
|
||||||
|
<p className="text-slate-500">Waktu Upload</p>
|
||||||
|
<p className="font-medium">14:32 WIB</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-slate-700 mb-2">
|
||||||
|
Preview Konten
|
||||||
|
</p>
|
||||||
|
<div className="border rounded-lg p-6 flex flex-col items-center justify-center bg-slate-50">
|
||||||
|
<Eye className="w-10 h-10 text-cyan-800 mb-2" />
|
||||||
|
<p className="font-medium text-cyan-900">
|
||||||
|
Preview File
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-slate-500">
|
||||||
|
File: {selectedItem.judul}.jpg
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-slate-700 mb-1">
|
||||||
|
Deskripsi
|
||||||
|
</p>
|
||||||
|
<div className="border rounded-lg p-3 bg-slate-50">
|
||||||
|
<p className="text-slate-700">
|
||||||
|
Upload {selectedItem.judul}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-slate-700 mb-2">
|
||||||
|
Status Timeline
|
||||||
|
</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-start space-x-2">
|
||||||
|
<CheckCircle2 className="w-5 h-5 text-green-500 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-slate-800">
|
||||||
|
Upload Berhasil
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-slate-500">
|
||||||
|
10/11/2024 • 14:32 WIB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{selectedItem.status === "Disetujui" && (
|
||||||
|
<div className="flex items-start space-x-2">
|
||||||
|
<CheckCircle2 className="w-5 h-5 text-green-500 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-slate-800">
|
||||||
|
Disetujui oleh Approver
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-slate-500">
|
||||||
|
10/11/2024 • 16:45 WIB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<DialogFooter className="bg-slate-100 p-4">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button className="bg-slate-300 text-slate-700 hover:bg-slate-400">
|
||||||
|
Tutup
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogFooter>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
<div className="mt-4 text-right p-3">
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="text-blue-600 font-medium p-3 h-auto"
|
||||||
|
>
|
||||||
|
Lihat Semua Aktivitas →
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-80">
|
|
||||||
<ApexChartColumn
|
|
||||||
type="monthly"
|
|
||||||
date={`${new Date().getMonth() + 1} ${new Date().getFullYear()}`}
|
|
||||||
view={analyticsView}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Recent Articles */}
|
{/* Notifikasi */}
|
||||||
<motion.div
|
<motion.div
|
||||||
className="bg-white rounded-2xl shadow-lg border border-slate-200/60 p-6"
|
|
||||||
initial={{ opacity: 0, x: 20 }}
|
initial={{ opacity: 0, x: 20 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ delay: 0.7 }}
|
transition={{ delay: 0.6 }}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div>
|
||||||
<h3 className="text-lg font-semibold text-slate-800">
|
<p className=" text-lg bg-cyan-900 p-3 rounded-t-lg text-white">
|
||||||
Recent Articles
|
Notifikasi
|
||||||
</h3>
|
</p>
|
||||||
<Link href="/admin/article/create">
|
<ScrollArea className="h-96 pr-2">
|
||||||
<Button className="bg-gradient-to-r from-blue-500 to-purple-500 hover:from-blue-600 hover:to-purple-600 text-white shadow-lg">
|
<div className="">
|
||||||
Create Article
|
{notifications.map((notif, i) => (
|
||||||
</Button>
|
<div
|
||||||
</Link>
|
key={i}
|
||||||
</div>
|
className="flex items-start space-x-3 border p-2 hover:bg-slate-50 transition"
|
||||||
|
>
|
||||||
<div className="space-y-4 max-h-96 overflow-y-auto scrollbar-thin">
|
<div className="text-xl">{notif.icon}</div>
|
||||||
{article?.map((list: any) => (
|
<div className="flex-1">
|
||||||
<motion.div
|
<p className="text-sm text-slate-700 leading-snug">
|
||||||
key={list?.id}
|
{notif.text}
|
||||||
className="flex space-x-4 p-4 rounded-xl hover:bg-slate-50 transition-colors duration-200"
|
</p>
|
||||||
whileHover={{ scale: 1.02 }}
|
<p className="text-xs text-slate-400 mt-1">
|
||||||
transition={{ type: "spring", stiffness: 300 }}
|
{notif.time}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className="w-2 h-2 bg-blue-500 rounded-full mt-2"></span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
<div className="mt-2 text-right">
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="text-blue-600 font-medium p-0 h-auto"
|
||||||
>
|
>
|
||||||
<Image
|
Tandai Semua Dibaca
|
||||||
alt="thumbnail"
|
</Button>
|
||||||
src={list?.thumbnailUrl || `/no-image.jpg`}
|
</div>
|
||||||
width={80}
|
|
||||||
height={80}
|
|
||||||
className="h-20 w-20 object-cover rounded-lg shadow-sm flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h4 className="font-medium text-slate-800 line-clamp-2 mb-1">
|
|
||||||
{list?.title}
|
|
||||||
</h4>
|
|
||||||
<p className="text-sm text-slate-500">
|
|
||||||
{convertDateFormat(list?.createdAt)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
<div className="mt-6 flex justify-center">
|
{/* Informasi Penting */}
|
||||||
<CustomPagination
|
<motion.div
|
||||||
totalPage={totalPage}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
onPageChange={(data) => setPage(data)}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
/>
|
transition={{ delay: 0.7 }}
|
||||||
</div>
|
className="lg:col-span-2"
|
||||||
|
>
|
||||||
|
<Card className="bg-blue-50 border border-blue-200">
|
||||||
|
<CardContent className="flex items-start space-x-3 p-4">
|
||||||
|
<Info className="text-blue-600 w-5 h-5 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="text-blue-800 font-semibold text-sm mb-1">
|
||||||
|
Informasi Penting
|
||||||
|
</h4>
|
||||||
|
<p className="text-blue-700 text-sm">
|
||||||
|
Upload yang berstatus <b>"Menunggu"</b> akan direview oleh
|
||||||
|
Approver. Pastikan semua konten sudah sesuai panduan sebelum
|
||||||
|
upload untuk mempercepat proses approval.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,563 @@
|
||||||
|
"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,
|
||||||
|
getArticlePagination,
|
||||||
|
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";
|
||||||
|
import { EditBannerDialog } from "../form/banner-edit-dialog";
|
||||||
|
import { Eye } from "lucide-react";
|
||||||
|
import AgentDetailDialog from "../dialog/agent-dialog";
|
||||||
|
import { deleteAgent, getAgentData } from "@/service/agent";
|
||||||
|
|
||||||
|
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 AgentTable() {
|
||||||
|
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 [openDetail, setOpenDetail] = useState(false);
|
||||||
|
const [selectedAgent, setSelectedAgent] = useState<any>(null);
|
||||||
|
const [startDateValue, setStartDateValue] = useState({
|
||||||
|
startDate: null,
|
||||||
|
endDate: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initState();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const initState = useCallback(async () => {
|
||||||
|
loading();
|
||||||
|
const req = {
|
||||||
|
limit: showData,
|
||||||
|
page: page,
|
||||||
|
search: search,
|
||||||
|
};
|
||||||
|
const res = await getAgentData(req);
|
||||||
|
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();
|
||||||
|
const resDelete = await deleteAgent(id);
|
||||||
|
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
|
||||||
|
const [openEditDialog, setOpenEditDialog] = useState(false);
|
||||||
|
const [selectedBanner, setSelectedBanner] = useState<any>(null);
|
||||||
|
const [openPreview, setOpenPreview] = useState(false);
|
||||||
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleUpdateBanner = (data: any) => {
|
||||||
|
console.log("Updated banner data:", data);
|
||||||
|
// TODO: panggil API update di sini
|
||||||
|
// lalu refresh tabel
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePreview = (imgUrl: string) => {
|
||||||
|
setPreviewImage(imgUrl);
|
||||||
|
setOpenPreview(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
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":
|
||||||
|
return (
|
||||||
|
// <Chip
|
||||||
|
// className="capitalize "
|
||||||
|
// color={statusColorMap[article.status]}
|
||||||
|
// size="lg"
|
||||||
|
// variant="flat"
|
||||||
|
// >
|
||||||
|
// <div className="flex flex-row items-center gap-2 justify-center">
|
||||||
|
// {article.status}
|
||||||
|
// </div>
|
||||||
|
// </Chip>
|
||||||
|
<p>{article.isPublish ? "Publish" : "Draft"}</p>
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[article, page]
|
||||||
|
);
|
||||||
|
|
||||||
|
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">
|
||||||
|
<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 Product
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<Table className="w-full text-sm">
|
||||||
|
{/* HEADER */}
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-[#BCD4DF] text-[#008080]">
|
||||||
|
<TableHead className="w-[40px] text-center text-[#008080]">
|
||||||
|
NO
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-left text-[#008080]">NAMA</TableHead>
|
||||||
|
<TableHead className="text-left text-[#008080]">
|
||||||
|
JABATAN
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-left text-[#008080]">
|
||||||
|
FOTO PROFIL
|
||||||
|
</TableHead>
|
||||||
|
|
||||||
|
<TableHead className="text-center text-[#008080]">
|
||||||
|
STATUS
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-center text-[#008080]">
|
||||||
|
AKSI
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
|
||||||
|
{/* BODY */}
|
||||||
|
<TableBody>
|
||||||
|
{article.length > 0 ? (
|
||||||
|
article.map((item, index) => (
|
||||||
|
<TableRow
|
||||||
|
key={item.id}
|
||||||
|
className="hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
{/* NO */}
|
||||||
|
<TableCell className="text-center font-medium">
|
||||||
|
{index + 1}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* NAMA */}
|
||||||
|
<TableCell className="font-semibold text-[#0F6C75]">
|
||||||
|
{item.name ?? "—"}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* HARGA PRODUK */}
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{item.job_title}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* BANNER PRODUK */}
|
||||||
|
<TableCell className="text-start">
|
||||||
|
{item.profile_picture_url ? (
|
||||||
|
<img
|
||||||
|
src={item.profile_picture_url}
|
||||||
|
alt={item.title}
|
||||||
|
className="w-[80px] h-[45px] object-cover rounded-md mx-auto"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="text-gray-400 text-xs">No Image</div>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* STATUS */}
|
||||||
|
<TableCell className="text-center">
|
||||||
|
{item.is_active === "true" ? (
|
||||||
|
<span className="bg-green-100 text-green-700 text-xs font-medium px-3 py-1 rounded-full">
|
||||||
|
Published
|
||||||
|
</span>
|
||||||
|
) : item.publishedStatus === "On Schedule" ? (
|
||||||
|
<span className="bg-gray-100 text-gray-600 text-xs font-medium px-3 py-1 rounded-full">
|
||||||
|
On Schedule
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="bg-red-100 text-red-600 text-xs font-medium px-3 py-1 rounded-full">
|
||||||
|
Cancel
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* AKSI */}
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<div className="flex items-center justify-center gap-3">
|
||||||
|
{/* Tombol Lihat */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedAgent({
|
||||||
|
name: item.name,
|
||||||
|
position: item.job_title,
|
||||||
|
phone: item.phone ?? "-",
|
||||||
|
status: item.is_active ? "Aktif" : "Nonaktif",
|
||||||
|
roles: item.agent_type ?? ["-"], // ambil dari API
|
||||||
|
imageUrl: item.thumbnailUrl,
|
||||||
|
});
|
||||||
|
setOpenDetail(true);
|
||||||
|
}}
|
||||||
|
className="text-[#0F6C75]"
|
||||||
|
>
|
||||||
|
<Eye className="w-4 h-4 mr-1" /> Lihat
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Edit */}
|
||||||
|
<Link href={`/admin/agent/update/${item.id}`}>
|
||||||
|
<Button
|
||||||
|
className="text-[#0F6C75] hover:bg-transparent hover:underline p-0"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<CreateIconIon className="w-4 h-4 mr-1" /> Edit
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Tombol Hapus */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDelete(item.id)}
|
||||||
|
className="text-red-600 hover:bg-transparent hover:underline p-0"
|
||||||
|
>
|
||||||
|
Hapus
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={7}
|
||||||
|
className="text-center py-6 text-gray-500"
|
||||||
|
>
|
||||||
|
Tidak ada data untuk ditampilkan.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<AgentDetailDialog
|
||||||
|
open={openDetail}
|
||||||
|
onOpenChange={setOpenDetail}
|
||||||
|
data={selectedAgent}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 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)}
|
||||||
|
>
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h2 className="text-lg font-semibold">JAEC00 J7 AWD</h2>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -19,7 +19,6 @@ import Cookies from "js-cookie";
|
||||||
import {
|
import {
|
||||||
deleteArticle,
|
deleteArticle,
|
||||||
getArticleByCategory,
|
getArticleByCategory,
|
||||||
getArticlePagination,
|
|
||||||
updateIsBannerArticle,
|
updateIsBannerArticle,
|
||||||
} from "@/service/article";
|
} from "@/service/article";
|
||||||
import {
|
import {
|
||||||
|
|
@ -46,6 +45,9 @@ import {
|
||||||
TableCell,
|
TableCell,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import CustomPagination from "../layout/custom-pagination";
|
import CustomPagination from "../layout/custom-pagination";
|
||||||
|
import { EditBannerDialog } from "../form/banner-edit-dialog";
|
||||||
|
import { deleteBanner, getBannerData, updateBanner } from "@/service/banner";
|
||||||
|
import { CheckCheck } from "lucide-react";
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ name: "No", uid: "no" },
|
{ name: "No", uid: "no" },
|
||||||
|
|
@ -106,14 +108,8 @@ export default function ArticleTable() {
|
||||||
limit: showData,
|
limit: showData,
|
||||||
page: page,
|
page: page,
|
||||||
search: search,
|
search: search,
|
||||||
// startDate:
|
|
||||||
// startDateValue.startDate === null ? "" : startDateValue.startDate,
|
|
||||||
// endDate: startDateValue.endDate === null ? "" : startDateValue.endDate,
|
|
||||||
categorySlug: Array.from(selectedCategories).join(","),
|
|
||||||
sort: "desc",
|
|
||||||
sortBy: "created_at",
|
|
||||||
};
|
};
|
||||||
const res = await getArticlePagination(req);
|
const res = await getBannerData(req);
|
||||||
await getTableNumber(parseInt(showData), res.data?.data);
|
await getTableNumber(parseInt(showData), res.data?.data);
|
||||||
setTotalPage(res?.data?.meta?.totalPage);
|
setTotalPage(res?.data?.meta?.totalPage);
|
||||||
close();
|
close();
|
||||||
|
|
@ -136,7 +132,7 @@ export default function ArticleTable() {
|
||||||
|
|
||||||
async function doDelete(id: any) {
|
async function doDelete(id: any) {
|
||||||
// loading();
|
// loading();
|
||||||
const resDelete = await deleteArticle(id);
|
const resDelete = await deleteBanner(id);
|
||||||
|
|
||||||
if (resDelete?.error) {
|
if (resDelete?.error) {
|
||||||
error(resDelete.message);
|
error(resDelete.message);
|
||||||
|
|
@ -171,6 +167,30 @@ export default function ArticleTable() {
|
||||||
initState();
|
initState();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [openEditDialog, setOpenEditDialog] = useState(false);
|
||||||
|
const [selectedBanner, setSelectedBanner] = useState<any>(null);
|
||||||
|
const [openPreview, setOpenPreview] = useState(false);
|
||||||
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
const copyUrlArticle = async (id: number, slug: string) => {
|
const copyUrlArticle = async (id: number, slug: string) => {
|
||||||
const url =
|
const url =
|
||||||
`${window.location.protocol}//${window.location.host}` +
|
`${window.location.protocol}//${window.location.host}` +
|
||||||
|
|
@ -308,139 +328,234 @@ export default function ArticleTable() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="py-3">
|
<div className="py-3">
|
||||||
<div className="flex flex-col items-start rounded-2xl gap-3">
|
<div className="w-full overflow-x-auto rounded-2xl shadow-sm border border-gray-200">
|
||||||
<div className="flex flex-col md:flex-row gap-3 w-full">
|
{/* Header */}
|
||||||
<div className="flex flex-col gap-1 w-full lg:w-1/3">
|
<div className="bg-[#0F6C75] text-white text-lg rounded-t-sm px-6 py-3">
|
||||||
<p className="font-semibold text-sm">Pencarian</p>
|
Daftar Banner
|
||||||
<div className="relative">
|
|
||||||
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground h-4 w-4 pointer-events-none" />
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
placeholder="Cari..."
|
|
||||||
className="pl-9 text-sm bg-muted"
|
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
|
||||||
onKeyUp={handleKeyUp}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1 w-full lg:w-[72px]">
|
|
||||||
<p className="font-semibold text-sm">Data</p>
|
|
||||||
<Select
|
|
||||||
value={showData}
|
|
||||||
onValueChange={(value) => setShowData(value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full text-sm border">
|
|
||||||
<SelectValue placeholder="Select" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="5">5</SelectItem>
|
|
||||||
<SelectItem value="10">10</SelectItem>
|
|
||||||
<SelectItem value="25">25</SelectItem>
|
|
||||||
<SelectItem value="50">50</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1 w-full lg:w-[230px]">
|
|
||||||
<p className="font-semibold text-sm">Kategori</p>
|
|
||||||
<Select
|
|
||||||
value={selectedCategories}
|
|
||||||
onValueChange={(value) => setSelectedCategories(value)}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-full text-sm border">
|
|
||||||
<SelectValue placeholder="Kategori" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{categories
|
|
||||||
?.filter((category: any) => category.slug != null)
|
|
||||||
.map((category: any) => (
|
|
||||||
<SelectItem key={category.slug} value={category.slug}>
|
|
||||||
{category.title}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
{/* <div className="flex flex-col gap-1 w-full lg:w-[240px]">
|
|
||||||
<p className="font-semibold text-sm">Tanggal</p>
|
|
||||||
<Datepicker
|
|
||||||
value={startDateValue}
|
|
||||||
displayFormat="DD/MM/YYYY"
|
|
||||||
onChange={(e: any) => setStartDateValue(e)}
|
|
||||||
inputClassName="z-50 w-full text-sm bg-transparent border-1 border-gray-200 px-2 py-[6px] rounded-xl h-[40px] text-gray-600 dark:text-gray-300"
|
|
||||||
/>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full overflow-x-hidden">
|
|
||||||
<div className="w-full mx-auto overflow-x-hidden">
|
{/* Table */}
|
||||||
<Table className="w-full table-fixed border text-sm">
|
<Table className="w-full text-sm">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow className="bg-[#BCD4DF] text-[#008080]">
|
||||||
{(username === "admin-mabes"
|
<TableHead className="w-[40px] text-[#008080]">NO</TableHead>
|
||||||
? columns
|
<TableHead className="text-[#008080]">JUDUL / NAMA</TableHead>
|
||||||
: columnsOtherRole
|
<TableHead className="text-[#008080] text-center">
|
||||||
).map((column) => (
|
PREVIEW KONTEN
|
||||||
<TableHead
|
</TableHead>
|
||||||
key={column.uid}
|
<TableHead className="text-[#008080] text-center w-[100px]">
|
||||||
className="truncate bg-white dark:bg-black text-black dark:text-white border-b text-md"
|
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)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{column.name}
|
{item.thumbnail_url ? (
|
||||||
</TableHead>
|
<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">
|
||||||
|
{item.status === "Disetujui" ? (
|
||||||
|
<span className="bg-green-100 text-green-700 text-xs px-3 py-1 rounded-full font-medium">
|
||||||
|
Disetujui
|
||||||
|
</span>
|
||||||
|
) : item.status === "Menunggu" ? (
|
||||||
|
<span className="bg-yellow-100 text-yellow-700 text-xs px-3 py-1 rounded-full font-medium">
|
||||||
|
Menunggu
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="bg-red-100 text-red-700 text-xs px-3 py-1 rounded-full font-medium">
|
||||||
|
Ditolak
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</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"
|
||||||
|
// onClick={() => handleEdit(item)}
|
||||||
|
>
|
||||||
|
<CheckCheck className="w-4 h-4 mr-1" />
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
</TableHeader>
|
))
|
||||||
<TableBody>
|
) : (
|
||||||
{article.length > 0 ? (
|
<TableRow>
|
||||||
article.map((item: any) => (
|
<TableCell
|
||||||
<TableRow key={item.id}>
|
colSpan={6}
|
||||||
{(username === "admin-mabes"
|
className="text-center py-6 text-gray-500"
|
||||||
? columns
|
>
|
||||||
: columnsOtherRole
|
Tidak ada data untuk ditampilkan.
|
||||||
).map((column) => (
|
</TableCell>
|
||||||
<TableCell
|
</TableRow>
|
||||||
key={column.uid}
|
)}
|
||||||
className="truncate text-black dark:text-white max-w-[200px]"
|
</TableBody>
|
||||||
>
|
</Table>
|
||||||
{renderCell(item, column.uid)}
|
|
||||||
</TableCell>
|
{/* Footer Pagination */}
|
||||||
))}
|
<div className="flex items-center justify-between px-6 py-3 border-t text-sm text-gray-600">
|
||||||
</TableRow>
|
<p>
|
||||||
))
|
Menampilkan {article.length} dari {article.length} data
|
||||||
) : (
|
</p>
|
||||||
<TableRow>
|
<div className="flex items-center gap-2">
|
||||||
<TableCell
|
<Button
|
||||||
colSpan={columns.length}
|
variant="outline"
|
||||||
className="text-center py-4"
|
size="sm"
|
||||||
>
|
className="rounded-full px-3"
|
||||||
No data to display.
|
disabled={page === 1}
|
||||||
</TableCell>
|
onClick={() => setPage(page - 1)}
|
||||||
</TableRow>
|
>
|
||||||
)}
|
Previous
|
||||||
</TableBody>
|
</Button>
|
||||||
</Table>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="my-2 w-full flex justify-center">
|
|
||||||
{/* <Pagination
|
|
||||||
isCompact
|
|
||||||
showControls
|
|
||||||
showShadow
|
|
||||||
color="primary"
|
|
||||||
classNames={{
|
|
||||||
base: "bg-transparent",
|
|
||||||
wrapper: "bg-transparent",
|
|
||||||
}}
|
|
||||||
page={page}
|
|
||||||
total={totalPage}
|
|
||||||
onChange={(page) => setPage(page)}
|
|
||||||
/> */}
|
|
||||||
<CustomPagination
|
|
||||||
totalPage={totalPage}
|
|
||||||
onPageChange={(data) => setPage(data)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</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"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h2 className="text-lg font-semibold">JAEC00 J7 AWD</h2>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,243 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { Eye, Pencil, Trash2, Calendar, MapPin } from "lucide-react";
|
||||||
|
import {
|
||||||
|
deleteGalery,
|
||||||
|
getGaleryById,
|
||||||
|
getGaleryData,
|
||||||
|
getGaleryFileData,
|
||||||
|
} from "@/service/galery";
|
||||||
|
import { DialogDetailGaleri } from "../dialog/galery-detail-dialog";
|
||||||
|
import { DialogUpdateGaleri } from "../dialog/galery-update-dialog";
|
||||||
|
import { error, success } from "@/config/swal";
|
||||||
|
import withReactContent from "sweetalert2-react-content";
|
||||||
|
import Swal from "sweetalert2";
|
||||||
|
|
||||||
|
export default function Galery() {
|
||||||
|
const MySwal = withReactContent(Swal);
|
||||||
|
const [data, setData] = useState<any[]>([]);
|
||||||
|
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [showData, setShowData] = useState("10");
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [showDetail, setShowDetail] = useState(false);
|
||||||
|
const [detailData, setDetailData] = useState<any>(null);
|
||||||
|
const [showEdit, setShowEdit] = useState(false);
|
||||||
|
const [editData, setEditData] = useState<any>(null);
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const req = {
|
||||||
|
limit: showData,
|
||||||
|
page: page,
|
||||||
|
search: search,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await getGaleryData(req);
|
||||||
|
const list = res?.data?.data ?? [];
|
||||||
|
|
||||||
|
// Ambil gambar yang sesuai gallery_id dan gabungkan ke data galeri
|
||||||
|
const merged = await Promise.all(
|
||||||
|
list.map(async (item: any) => {
|
||||||
|
try {
|
||||||
|
const filesRes = await getGaleryFileData(item.id);
|
||||||
|
const images = filesRes?.data?.data ?? [];
|
||||||
|
|
||||||
|
// Filter file dengan gallery_id sama dengan item.id
|
||||||
|
const filteredImages = images?.filter(
|
||||||
|
(img: any) => img.gallery_id === item.id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ambil gambar pertama sebagai thumbnail
|
||||||
|
const coverImage =
|
||||||
|
filteredImages.length > 0 ? filteredImages[0].image_url : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
image_url: coverImage,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
return { ...item, image_url: null };
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setData(merged);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetch galeri:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [page, showData, search]);
|
||||||
|
|
||||||
|
const openDetail = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const res = await getGaleryById(id);
|
||||||
|
setDetailData(res?.data?.data);
|
||||||
|
setShowDetail(true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error get detail:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEdit = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const res = await getGaleryById(id);
|
||||||
|
setEditData(res?.data?.data);
|
||||||
|
setShowEdit(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error open edit:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function doDelete(id: any) {
|
||||||
|
const resDelete = await deleteGalery(id);
|
||||||
|
|
||||||
|
if (resDelete?.error) {
|
||||||
|
error(resDelete.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
success("Berhasil Hapus");
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||||
|
{data?.map((item: any) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="bg-white shadow-md rounded-2xl overflow-hidden border"
|
||||||
|
>
|
||||||
|
{/* Image */}
|
||||||
|
<div className="relative w-full h-48">
|
||||||
|
{item.image_url ? (
|
||||||
|
<Image
|
||||||
|
src={item.image_url}
|
||||||
|
alt={item.title}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full bg-gray-100 text-gray-400">
|
||||||
|
No Image
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Status Badge */}
|
||||||
|
<span
|
||||||
|
className={`absolute top-3 left-3 text-xs px-3 py-1 rounded-full font-medium
|
||||||
|
${
|
||||||
|
item.is_active
|
||||||
|
? "bg-green-100 text-green-700"
|
||||||
|
: "bg-red-100 text-red-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.is_active ? "Aktif" : "Tidak Aktif"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-4 space-y-3">
|
||||||
|
<h2 className="text-[#1F6779] text-lg font-semibold">
|
||||||
|
{item.title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 text-sm">{item.description ?? "-"}</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-700">
|
||||||
|
<Calendar className="h-4 w-4" />{" "}
|
||||||
|
{new Date(item.created_at).toLocaleDateString("id-ID")}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-sm text-gray-700">
|
||||||
|
<MapPin className="h-4 w-4" /> {item.location ?? "-"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer Actions */}
|
||||||
|
<div className="border-t px-4 py-3 flex items-center justify-between text-sm">
|
||||||
|
<button
|
||||||
|
onClick={() => openDetail(item.id)}
|
||||||
|
className="flex items-center gap-1 text-gray-700 hover:text-[#1F6779] transition"
|
||||||
|
>
|
||||||
|
<Eye className="h-4 w-4" /> Lihat
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => openEdit(item.id)}
|
||||||
|
className="flex items-center gap-1 text-gray-700 hover:text-[#1F6779] transition"
|
||||||
|
>
|
||||||
|
<Pencil className="h-4 w-4" /> Edit
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-1 text-red-600 hover:text-red-700 transition"
|
||||||
|
onClick={() => handleDelete(item.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" /> Hapus
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
<div className="flex items-center justify-between mt-6 text-sm">
|
||||||
|
<p>Menampilkan {data.length} data</p>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
className="px-3 py-1 border rounded-md bg-gray-100 text-gray-700 disabled:opacity-50"
|
||||||
|
disabled={page === 1}
|
||||||
|
onClick={() => setPage((prev) => prev - 1)}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="px-3 py-1 border rounded-md bg-gray-100 text-gray-700"
|
||||||
|
onClick={() => setPage((prev) => prev + 1)}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showDetail && detailData && (
|
||||||
|
<DialogDetailGaleri
|
||||||
|
open={showDetail}
|
||||||
|
onClose={() => setShowDetail(false)}
|
||||||
|
data={detailData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showEdit && editData && (
|
||||||
|
<DialogUpdateGaleri
|
||||||
|
open={showEdit}
|
||||||
|
onClose={() => setShowEdit(false)}
|
||||||
|
data={editData}
|
||||||
|
onUpdated={fetchData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,562 @@
|
||||||
|
"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,
|
||||||
|
getArticlePagination,
|
||||||
|
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";
|
||||||
|
import { EditBannerDialog } from "../form/banner-edit-dialog";
|
||||||
|
import { deleteProduct, getProductPagination } from "@/service/product";
|
||||||
|
import { CheckCheck } from "lucide-react";
|
||||||
|
|
||||||
|
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 ProductTable() {
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
const res = await getProductPagination(req);
|
||||||
|
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();
|
||||||
|
const resDelete = await deleteProduct(id);
|
||||||
|
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
|
||||||
|
const [openEditDialog, setOpenEditDialog] = useState(false);
|
||||||
|
const [selectedBanner, setSelectedBanner] = useState<any>(null);
|
||||||
|
const [openPreview, setOpenPreview] = useState(false);
|
||||||
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleUpdateBanner = (data: any) => {
|
||||||
|
console.log("Updated banner data:", data);
|
||||||
|
// TODO: panggil API update di sini
|
||||||
|
// lalu refresh tabel
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePreview = (imgUrl: string) => {
|
||||||
|
setPreviewImage(imgUrl);
|
||||||
|
setOpenPreview(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
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":
|
||||||
|
return (
|
||||||
|
// <Chip
|
||||||
|
// className="capitalize "
|
||||||
|
// color={statusColorMap[article.status]}
|
||||||
|
// size="lg"
|
||||||
|
// variant="flat"
|
||||||
|
// >
|
||||||
|
// <div className="flex flex-row items-center gap-2 justify-center">
|
||||||
|
// {article.status}
|
||||||
|
// </div>
|
||||||
|
// </Chip>
|
||||||
|
<p>{article.isPublish ? "Publish" : "Draft"}</p>
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[article, page]
|
||||||
|
);
|
||||||
|
|
||||||
|
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">
|
||||||
|
<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 Product
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<Table className="w-full text-sm">
|
||||||
|
{/* HEADER */}
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-[#BCD4DF] text-[#008080]">
|
||||||
|
<TableHead className="w-[40px] text-center text-[#008080]">
|
||||||
|
NO
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-left text-[#008080]">NAMA</TableHead>
|
||||||
|
<TableHead className="text-left text-[#008080]">
|
||||||
|
HARGA PRODUK
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-left text-[#008080]">
|
||||||
|
VARIAN
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-center text-[#008080]">
|
||||||
|
BANNER PRODUK
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-center text-[#008080]">
|
||||||
|
STATUS
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-center text-[#008080]">
|
||||||
|
AKSI
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
|
||||||
|
{/* BODY */}
|
||||||
|
<TableBody>
|
||||||
|
{article.length > 0 ? (
|
||||||
|
article.map((item, index) => (
|
||||||
|
<TableRow
|
||||||
|
key={item.id}
|
||||||
|
className="hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
{/* NO */}
|
||||||
|
<TableCell className="text-center font-medium">
|
||||||
|
{index + 1}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* NAMA */}
|
||||||
|
<TableCell className="font-semibold text-[#0F6C75]">
|
||||||
|
{item.title ?? "—"}
|
||||||
|
<div className="mt-1">
|
||||||
|
{item.isPublish ? (
|
||||||
|
<span className="text-xs bg-blue-100 text-blue-700 px-2 py-[2px] rounded-md">
|
||||||
|
Tampil di Landing
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs bg-gray-100 text-gray-600 px-2 py-[2px] rounded-md">
|
||||||
|
Draft
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* HARGA PRODUK */}
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{item.price
|
||||||
|
? `Rp ${item.price.toLocaleString("id-ID")}`
|
||||||
|
: "Rp 0"}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* VARIAN */}
|
||||||
|
<TableCell>{item.variant ?? "-"}</TableCell>
|
||||||
|
|
||||||
|
{/* BANNER PRODUK */}
|
||||||
|
<TableCell className="text-center">
|
||||||
|
{item.thumbnail_url ? (
|
||||||
|
<img
|
||||||
|
src={item.thumbnail_url}
|
||||||
|
alt={item.title}
|
||||||
|
className="w-[80px] h-[45px] object-cover rounded-md mx-auto"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="text-gray-400 text-xs">No Image</div>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* STATUS */}
|
||||||
|
<TableCell className="text-center">
|
||||||
|
{item.status === "Disetujui" ? (
|
||||||
|
<span className="bg-green-100 text-green-700 text-xs font-medium px-3 py-1 rounded-full">
|
||||||
|
Disetujui
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="bg-gray-100 text-gray-600 text-xs font-medium px-3 py-1 rounded-full">
|
||||||
|
Draft
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* AKSI */}
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<div className="flex items-center justify-center gap-3">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-[#0F6C75] hover:bg-transparent hover:underline p-0"
|
||||||
|
// onClick={() => handleEdit(item)}
|
||||||
|
>
|
||||||
|
<CheckCheck className="w-4 h-4 mr-1" />
|
||||||
|
Approve
|
||||||
|
</Button>
|
||||||
|
<Link href={"/admin/product/update"}>
|
||||||
|
<Button
|
||||||
|
className="text-[#0F6C75] hover:bg-transparent hover:underline p-0"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<CreateIconIon className="w-4 h-4 mr-1" /> Edit
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDelete(item.id)}
|
||||||
|
className="text-red-600 hover:bg-transparent hover:underline p-0"
|
||||||
|
>
|
||||||
|
Hapus
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={7}
|
||||||
|
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)}
|
||||||
|
>
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h2 className="text-lg font-semibold">JAEC00 J7 AWD</h2>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,539 @@
|
||||||
|
"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,
|
||||||
|
getArticlePagination,
|
||||||
|
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";
|
||||||
|
import { EditBannerDialog } from "../form/banner-edit-dialog";
|
||||||
|
import { Eye, Trash2 } from "lucide-react";
|
||||||
|
import AgentDetailDialog from "../dialog/agent-dialog";
|
||||||
|
import PromoDetailDialog from "../dialog/promo-dialog";
|
||||||
|
import { deletePromotion, getPromotionPagination } from "@/service/promotion";
|
||||||
|
|
||||||
|
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 PromotionTable() {
|
||||||
|
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 [openDetail, setOpenDetail] = useState(false);
|
||||||
|
const [selectedPromo, setPromoDetail] = useState<any>(null);
|
||||||
|
const [selectedPromoId, setSelectedPromoId] = useState<number | null>(null);
|
||||||
|
const [startDateValue, setStartDateValue] = useState({
|
||||||
|
startDate: null,
|
||||||
|
endDate: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initState();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const initState = useCallback(async () => {
|
||||||
|
loading();
|
||||||
|
const req = {
|
||||||
|
limit: showData,
|
||||||
|
page: page,
|
||||||
|
search: search,
|
||||||
|
};
|
||||||
|
const res = await getPromotionPagination(req);
|
||||||
|
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();
|
||||||
|
const resDelete = await deletePromotion(id);
|
||||||
|
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
|
||||||
|
const [openEditDialog, setOpenEditDialog] = useState(false);
|
||||||
|
const [selectedBanner, setSelectedBanner] = useState<any>(null);
|
||||||
|
const [openPreview, setOpenPreview] = useState(false);
|
||||||
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleUpdateBanner = (data: any) => {
|
||||||
|
console.log("Updated banner data:", data);
|
||||||
|
// TODO: panggil API update di sini
|
||||||
|
// lalu refresh tabel
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePreview = (imgUrl: string) => {
|
||||||
|
setPreviewImage(imgUrl);
|
||||||
|
setOpenPreview(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
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":
|
||||||
|
return (
|
||||||
|
// <Chip
|
||||||
|
// className="capitalize "
|
||||||
|
// color={statusColorMap[article.status]}
|
||||||
|
// size="lg"
|
||||||
|
// variant="flat"
|
||||||
|
// >
|
||||||
|
// <div className="flex flex-row items-center gap-2 justify-center">
|
||||||
|
// {article.status}
|
||||||
|
// </div>
|
||||||
|
// </Chip>
|
||||||
|
<p>{article.isPublish ? "Publish" : "Draft"}</p>
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[article, page]
|
||||||
|
);
|
||||||
|
|
||||||
|
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">
|
||||||
|
<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 Product
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table */}
|
||||||
|
<Table className="w-full text-sm">
|
||||||
|
{/* HEADER */}
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-[#BCD4DF] text-[#008080]">
|
||||||
|
<TableHead className="w-[40px] text-center text-[#008080]">
|
||||||
|
NO
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-left text-[#008080]">
|
||||||
|
DOKUMEN
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-left text-[#008080]">
|
||||||
|
TANGGAL UPLOAD
|
||||||
|
</TableHead>
|
||||||
|
|
||||||
|
<TableHead className="text-center text-[#008080]">
|
||||||
|
STATUS
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="text-center text-[#008080]">
|
||||||
|
AKSI
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
|
||||||
|
{/* BODY */}
|
||||||
|
<TableBody>
|
||||||
|
{article.length > 0 ? (
|
||||||
|
article.map((item, index) => (
|
||||||
|
<TableRow
|
||||||
|
key={item.id}
|
||||||
|
className="hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
{/* NO */}
|
||||||
|
<TableCell className="text-center font-medium">
|
||||||
|
{index + 1}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* NAMA */}
|
||||||
|
<TableCell className="font-semibold text-[#0F6C75]">
|
||||||
|
{item.title ?? "—"}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* HARGA PRODUK */}
|
||||||
|
<TableCell className="font-medium">
|
||||||
|
{new Date(item.created_at)
|
||||||
|
.toLocaleDateString("id-ID")
|
||||||
|
.replace(/\//g, "-")}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* BANNER PRODUK */}
|
||||||
|
|
||||||
|
{/* STATUS */}
|
||||||
|
<TableCell className="text-center">
|
||||||
|
{item.publishedStatus === "Published" ? (
|
||||||
|
<span className="bg-green-100 text-green-700 text-xs font-medium px-3 py-1 rounded-full">
|
||||||
|
Published
|
||||||
|
</span>
|
||||||
|
) : item.publishedStatus === "On Schedule" ? (
|
||||||
|
<span className="bg-gray-100 text-gray-600 text-xs font-medium px-3 py-1 rounded-full">
|
||||||
|
On Schedule
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="bg-red-100 text-red-600 text-xs font-medium px-3 py-1 rounded-full">
|
||||||
|
Cancel
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
|
||||||
|
{/* AKSI */}
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<div className="flex items-center justify-center gap-3">
|
||||||
|
{/* Tombol Lihat */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedPromoId(item.id); // kirim ID
|
||||||
|
setOpenDetail(true);
|
||||||
|
}}
|
||||||
|
className="text-[#0F6C75]"
|
||||||
|
>
|
||||||
|
<Eye className="w-4 h-4 mr-1" /> Lihat
|
||||||
|
</Button>
|
||||||
|
{/* Tombol Edit */}
|
||||||
|
|
||||||
|
{/* Tombol Hapus */}
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDelete(item.id)}
|
||||||
|
className="text-red-600 hover:bg-transparent hover:underline p-0"
|
||||||
|
>
|
||||||
|
<Trash2 />
|
||||||
|
Hapus
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={7}
|
||||||
|
className="text-center py-6 text-gray-500"
|
||||||
|
>
|
||||||
|
Tidak ada data untuk ditampilkan.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<PromoDetailDialog
|
||||||
|
promoId={selectedPromoId}
|
||||||
|
open={openDetail}
|
||||||
|
onOpenChange={setOpenDetail}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 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)}
|
||||||
|
>
|
||||||
|
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>
|
||||||
|
</div>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h2 className="text-lg font-semibold">JAEC00 J7 AWD</h2>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,168 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
FormProvider,
|
||||||
|
useFormContext,
|
||||||
|
useFormState,
|
||||||
|
type ControllerProps,
|
||||||
|
type FieldPath,
|
||||||
|
type FieldValues,
|
||||||
|
} from "react-hook-form";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
|
const Form = FormProvider;
|
||||||
|
|
||||||
|
type FormFieldContextValue<
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||||
|
> = {
|
||||||
|
name: TName;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||||
|
{} as FormFieldContextValue
|
||||||
|
);
|
||||||
|
|
||||||
|
const FormField = <
|
||||||
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||||
|
>({
|
||||||
|
...props
|
||||||
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
|
return (
|
||||||
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
|
<Controller {...props} />
|
||||||
|
</FormFieldContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useFormField = () => {
|
||||||
|
const fieldContext = React.useContext(FormFieldContext);
|
||||||
|
const itemContext = React.useContext(FormItemContext);
|
||||||
|
const { getFieldState } = useFormContext();
|
||||||
|
const formState = useFormState({ name: fieldContext.name });
|
||||||
|
const fieldState = getFieldState(fieldContext.name, formState);
|
||||||
|
|
||||||
|
if (!fieldContext) {
|
||||||
|
throw new Error("useFormField should be used within <FormField>");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = itemContext;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: fieldContext.name,
|
||||||
|
formItemId: `${id}-form-item`,
|
||||||
|
formDescriptionId: `${id}-form-item-description`,
|
||||||
|
formMessageId: `${id}-form-item-message`,
|
||||||
|
...fieldState,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormItemContextValue = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||||
|
{} as FormItemContextValue
|
||||||
|
);
|
||||||
|
|
||||||
|
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
const id = React.useId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItemContext.Provider value={{ id }}>
|
||||||
|
<div
|
||||||
|
data-slot="form-item"
|
||||||
|
className={cn("grid gap-2", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</FormItemContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
const { error, formItemId } = useFormField();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
data-slot="form-label"
|
||||||
|
data-error={!!error}
|
||||||
|
className={cn("data-[error=true]:text-destructive", className)}
|
||||||
|
htmlFor={formItemId}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||||
|
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||||
|
useFormField();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Slot
|
||||||
|
data-slot="form-control"
|
||||||
|
id={formItemId}
|
||||||
|
aria-describedby={
|
||||||
|
!error
|
||||||
|
? `${formDescriptionId}`
|
||||||
|
: `${formDescriptionId} ${formMessageId}`
|
||||||
|
}
|
||||||
|
aria-invalid={!!error}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
const { formDescriptionId } = useFormField();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="form-description"
|
||||||
|
id={formDescriptionId}
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
const { error, formMessageId } = useFormField();
|
||||||
|
const body = error ? String(error?.message ?? "") : props.children;
|
||||||
|
|
||||||
|
if (!body) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="form-message"
|
||||||
|
id={formMessageId}
|
||||||
|
className={cn("text-destructive text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{body}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
useFormField,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormMessage,
|
||||||
|
FormField,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { PaginationRequest } from "@/types/globals";
|
||||||
|
import {
|
||||||
|
httpDeleteInterceptor,
|
||||||
|
httpGetInterceptor,
|
||||||
|
httpPostInterceptor,
|
||||||
|
httpPutInterceptor,
|
||||||
|
} from "./http-config/http-interceptor-services";
|
||||||
|
import { httpGet } from "./http-config/http-base-services";
|
||||||
|
|
||||||
|
export async function getAgentData(props: PaginationRequest) {
|
||||||
|
const { page, limit, search } = props;
|
||||||
|
return await httpGetInterceptor(`/sales-agents`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAgentById(id: any) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
};
|
||||||
|
return await httpGet(`/sales-agents/${id}`, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAgent(data: any) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "multipart/form-data",
|
||||||
|
};
|
||||||
|
return await httpPostInterceptor(`/sales-agents`, data, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAgent(id: number, data: any) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "multipart/form-data",
|
||||||
|
};
|
||||||
|
return await httpPutInterceptor(`/sales-agents/${id}`, data, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAgent(id: string) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
};
|
||||||
|
return await httpDeleteInterceptor(`sales-agents/${id}`, headers);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { PaginationRequest } from "@/types/globals";
|
||||||
|
import {
|
||||||
|
httpDeleteInterceptor,
|
||||||
|
httpGetInterceptor,
|
||||||
|
httpPostInterceptor,
|
||||||
|
httpPutInterceptor,
|
||||||
|
} from "./http-config/http-interceptor-services";
|
||||||
|
import { httpGet } from "./http-config/http-base-services";
|
||||||
|
|
||||||
|
export async function getBannerData(props: PaginationRequest) {
|
||||||
|
const { page, limit, search } = props;
|
||||||
|
return await httpGetInterceptor(`/banners`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBannerById(id: any) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
};
|
||||||
|
return await httpGet(`/banners/${id}`, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
// export async function createBanner(data: any) {
|
||||||
|
// const pathUrl = `/banners`;
|
||||||
|
// return await httpPostInterceptor(pathUrl, data);
|
||||||
|
// }
|
||||||
|
|
||||||
|
export async function createBanner(data: any) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "multipart/form-data",
|
||||||
|
};
|
||||||
|
return await httpPostInterceptor(`/banners`, data, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateBanner(data: any, id: any) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "multipart/form-data",
|
||||||
|
};
|
||||||
|
return await httpPutInterceptor(`/banners/${id}`, data, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
// export async function updateBanner(data: any, id: any) {
|
||||||
|
// const pathUrl = `/banners/${id}`;
|
||||||
|
// return await httpPutInterceptor(pathUrl, data);
|
||||||
|
// }
|
||||||
|
|
||||||
|
export async function deleteBanner(id: string) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
};
|
||||||
|
return await httpDeleteInterceptor(`banners/${id}`, headers);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { PaginationRequest } from "@/types/globals";
|
||||||
|
import {
|
||||||
|
httpDeleteInterceptor,
|
||||||
|
httpGetInterceptor,
|
||||||
|
httpPostInterceptor,
|
||||||
|
httpPutInterceptor,
|
||||||
|
} from "./http-config/http-interceptor-services";
|
||||||
|
import { httpGet } from "./http-config/http-base-services";
|
||||||
|
|
||||||
|
export async function getGaleryData(props: PaginationRequest) {
|
||||||
|
const { page, limit, search } = props;
|
||||||
|
return await httpGetInterceptor(`/galleries`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getGaleryFileData(props: PaginationRequest) {
|
||||||
|
const { page, limit, search } = props;
|
||||||
|
return await httpGetInterceptor(`/gallery-files`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getGaleryById(id: any) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
};
|
||||||
|
return await httpGet(`/galleries/${id}`, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getGaleryFileName(filename: any) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
};
|
||||||
|
return await httpGet(`/gallery-files/viewer/${filename}`, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createGalery(data: any) {
|
||||||
|
const pathUrl = `/galleries`;
|
||||||
|
return await httpPostInterceptor(pathUrl, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateGalery(id: any, data: any) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "multipart/form-data",
|
||||||
|
};
|
||||||
|
return await httpPutInterceptor(`/galleries/${id}`, data, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUploadGaleryFile(id: any, data: any) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "multipart/form-data",
|
||||||
|
};
|
||||||
|
return await httpPutInterceptor(`/gallery-files/${id}`, data, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function uploadGaleryFile(id: any, data: any) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "multipart/form-data",
|
||||||
|
};
|
||||||
|
return await httpPostInterceptor(`/gallery-files/`, data, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteGalery(id: string) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
};
|
||||||
|
return await httpDeleteInterceptor(`galleries/${id}`, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteGaleryFile(id: any) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
};
|
||||||
|
return await httpDeleteInterceptor(`gallery-files/${id}`, headers);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { PaginationRequest } from "@/types/globals";
|
||||||
|
import {
|
||||||
|
httpDeleteInterceptor,
|
||||||
|
httpGetInterceptor,
|
||||||
|
httpPostInterceptor,
|
||||||
|
httpPutInterceptor,
|
||||||
|
} from "./http-config/http-interceptor-services";
|
||||||
|
|
||||||
|
export async function getProductPagination(props: PaginationRequest) {
|
||||||
|
const { page, limit, search } = props;
|
||||||
|
return await httpGetInterceptor(`/products`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// export async function createProduct(data: any) {
|
||||||
|
// const pathUrl = `/products`;
|
||||||
|
// return await httpPostInterceptor(pathUrl, data);
|
||||||
|
// }
|
||||||
|
|
||||||
|
export async function createProduct(data: any) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "multipart/form-data",
|
||||||
|
};
|
||||||
|
return await httpPostInterceptor(`/products`, data, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateProduct(data: any, id: any) {
|
||||||
|
const pathUrl = `/products/${id}`;
|
||||||
|
return await httpPutInterceptor(pathUrl, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteProduct(id: string) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
};
|
||||||
|
return await httpDeleteInterceptor(`products/${id}`, headers);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { PaginationRequest } from "@/types/globals";
|
||||||
|
import {
|
||||||
|
httpDeleteInterceptor,
|
||||||
|
httpGetInterceptor,
|
||||||
|
httpPostInterceptor,
|
||||||
|
httpPutInterceptor,
|
||||||
|
} from "./http-config/http-interceptor-services";
|
||||||
|
import { httpGet } from "./http-config/http-base-services";
|
||||||
|
|
||||||
|
export async function getPromotionPagination(props: PaginationRequest) {
|
||||||
|
const { page, limit, search } = props;
|
||||||
|
return await httpGetInterceptor(`/promotions`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPromotionById(id: any) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
};
|
||||||
|
return await httpGet(`/promotions/${id}`, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createPromotion(data: any) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "multipart/form-data",
|
||||||
|
};
|
||||||
|
return await httpPostInterceptor(`/promotions`, data, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updatePromotion(data: any, id: any) {
|
||||||
|
const pathUrl = `/promotions/${id}`;
|
||||||
|
return await httpPutInterceptor(pathUrl, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deletePromotion(id: string) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
};
|
||||||
|
return await httpDeleteInterceptor(`promotions/${id}`, headers);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue