feat: update all approval

This commit is contained in:
hanif salafi 2026-01-20 10:35:33 +07:00
parent 5579ad8c20
commit be02fb7b0e
10 changed files with 393 additions and 82 deletions

View File

@ -47,9 +47,9 @@ import {
} from "@/components/ui/table";
import CustomPagination from "../layout/custom-pagination";
import { EditBannerDialog } from "../form/banner-edit-dialog";
import { Eye, CheckCheck } from "lucide-react";
import { Eye, CheckCheck, X } from "lucide-react";
import AgentDetailDialog from "../dialog/agent-dialog";
import { deleteAgent, getAgentData, approveAgent } from "@/service/agent";
import { deleteAgent, getAgentData, approveAgent, rejectAgent } from "@/service/agent";
const columns = [
{ name: "No", uid: "no" },
@ -94,12 +94,12 @@ export default function AgentTable() {
startDate: null,
endDate: null,
});
const [userLevelId, setUserLevelId] = useState<string | null>(null);
const [userRoleId, setUserRoleId] = useState<string | null>(null);
// 🔹 Ambil userlevelId dari cookies
// 🔹 Ambil userRoleId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
const urie = Cookies.get("urie"); // userRoleId dari cookies
setUserRoleId(urie ?? null);
}, []);
useEffect(() => {
@ -202,6 +202,40 @@ export default function AgentTable() {
initState(); // refresh table
};
const handleRejectAgent = async (id: number) => {
const MySwal = withReactContent(Swal);
const { value: message } = await MySwal.fire({
title: "Tolak Agent",
input: "textarea",
inputLabel: "Alasan penolakan (opsional)",
inputPlaceholder: "Masukkan alasan penolakan...",
inputAttributes: {
"aria-label": "Masukkan alasan penolakan",
},
showCancelButton: true,
confirmButtonText: "Tolak",
cancelButtonText: "Batal",
confirmButtonColor: "#dc2626",
});
if (message === undefined) {
return; // User cancelled
}
loading();
const res = await rejectAgent(id, message || undefined);
if (res?.error) {
error(res.message || "Gagal menolak agent");
close();
return;
}
close();
success("Agent berhasil ditolak");
initState(); // refresh table
};
const copyUrlArticle = async (id: number, slug: string) => {
const url =
`${window.location.protocol}//${window.location.host}` +
@ -416,6 +450,10 @@ export default function AgentTable() {
<span className="bg-green-100 text-green-700 text-xs px-3 py-1 rounded-full font-medium">
Disetujui
</span>
) : item.status_id === 3 ? (
<span className="bg-red-100 text-red-700 text-xs px-3 py-1 rounded-full font-medium">
Ditolak
</span>
) : (
<span className="bg-gray-100 text-gray-600 text-xs px-3 py-1 rounded-full font-medium">
{item.status_id || "Tidak Diketahui"}
@ -458,17 +496,28 @@ export default function AgentTable() {
</Button>
</Link>
{/* Tombol Approve - hanya untuk admin dan status pending */}
{userLevelId === "1" && item.status_id === 1 && (
<Button
variant="ghost"
size="sm"
onClick={() => handleApproveAgent(item.id)}
className="text-green-600 hover:bg-transparent hover:underline p-0"
>
<CheckCheck className="w-4 h-4 mr-1" />
Approve
</Button>
{/* Tombol Approve & Reject - hanya untuk admin dan status pending */}
{userRoleId === "1" && item.status_id === 1 && (
<>
<Button
variant="ghost"
size="sm"
onClick={() => handleApproveAgent(item.id)}
className="text-green-600 hover:bg-transparent hover:underline p-0"
>
<CheckCheck className="w-4 h-4 mr-1" />
Approve
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleRejectAgent(item.id)}
className="text-red-600 hover:bg-transparent hover:underline p-0"
>
<X className="w-4 h-4 mr-1" />
Reject
</Button>
</>
)}
{/* Tombol Hapus */}
<Button

View File

@ -46,7 +46,7 @@ import {
} from "@/components/ui/table";
import CustomPagination from "../layout/custom-pagination";
import { EditBannerDialog } from "../form/banner-edit-dialog";
import { deleteBanner, getBannerData, updateBanner, approveBanner } from "@/service/banner";
import { deleteBanner, getBannerData, updateBanner, approveBanner, rejectBanner, getApprovalHistory } from "@/service/banner";
import { Check, CheckCheck, Clock, Eye, X } from "lucide-react";
import { useRouter } from "next/navigation";
@ -91,14 +91,14 @@ export default function ArticleTable() {
startDate: null,
endDate: null,
});
const [userLevelId, setUserLevelId] = useState<string | null>(null);
const [userRoleId, setUserRoleId] = useState<string | null>(null);
const router = useRouter();
// 🔹 Ambil userlevelId dari cookies
// 🔹 Ambil userRoleId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
const urie = Cookies.get("urie"); // userRoleId dari cookies
setUserRoleId(urie ?? null);
}, []);
useEffect(() => {
@ -254,6 +254,39 @@ export default function ArticleTable() {
initState(); // refresh table
};
const handleRejectBanner = async (id: number) => {
const { value: message } = await MySwal.fire({
title: "Tolak Banner",
input: "textarea",
inputLabel: "Alasan penolakan (opsional)",
inputPlaceholder: "Masukkan alasan penolakan...",
inputAttributes: {
"aria-label": "Masukkan alasan penolakan",
},
showCancelButton: true,
confirmButtonText: "Tolak",
cancelButtonText: "Batal",
confirmButtonColor: "#dc2626",
});
if (message === undefined) {
return; // User cancelled
}
loading();
const res = await rejectBanner(id, message || undefined);
if (res?.error) {
error(res.message || "Gagal menolak banner");
close();
return;
}
close();
success("Banner berhasil ditolak");
initState(); // refresh table
};
const handleReject = async () => {
if (!viewBanner) return;
@ -486,6 +519,10 @@ export default function ArticleTable() {
<span className="bg-green-100 text-green-700 text-xs px-3 py-1 rounded-full font-medium">
Disetujui
</span>
) : item.status_id === 3 ? (
<span className="bg-red-100 text-red-700 text-xs px-3 py-1 rounded-full font-medium">
Ditolak
</span>
) : (
<span className="bg-gray-100 text-gray-600 text-xs px-3 py-1 rounded-full font-medium">
{item.status_id || "Tidak Diketahui"}
@ -505,7 +542,7 @@ export default function ArticleTable() {
<Eye className="w-4 h-4 mr-1" />
Lihat
</Button>
{userLevelId !== "3" && (
{userRoleId !== "3" && (
<Button
variant="ghost"
size="sm"
@ -516,16 +553,27 @@ export default function ArticleTable() {
Edit
</Button>
)}
{userLevelId === "1" && item.status_id === 1 && (
<Button
variant="ghost"
size="sm"
className="text-green-600 hover:bg-transparent hover:underline p-0"
onClick={() => handleApproveBanner(item.id)}
>
<CheckCheck className="w-4 h-4 mr-1" />
Approve
</Button>
{userRoleId === "1" && item.status_id === 1 && (
<>
<Button
variant="ghost"
size="sm"
className="text-green-600 hover:bg-transparent hover:underline p-0"
onClick={() => handleApproveBanner(item.id)}
>
<CheckCheck className="w-4 h-4 mr-1" />
Approve
</Button>
<Button
variant="ghost"
size="sm"
className="text-red-600 hover:bg-transparent hover:underline p-0"
onClick={() => handleRejectBanner(item.id)}
>
<X className="w-4 h-4 mr-1" />
Reject
</Button>
</>
)}
<Button
variant="ghost"
@ -801,7 +849,7 @@ export default function ArticleTable() {
</div>
{/* FOOTER */}
{userLevelId !== "2" && (
{userRoleId !== "2" && (
<div className="flex justify-between items-center gap-3 px-6 py-4 border-t bg-[#F2F7FA]">
{viewBanner.status === "1" ? (
<>

View File

@ -2,13 +2,14 @@
import { useEffect, useState } from "react";
import Image from "next/image";
import { Eye, Pencil, Trash2, Calendar, MapPin, CheckCheck } from "lucide-react";
import { Eye, Pencil, Trash2, Calendar, MapPin, CheckCheck, X } from "lucide-react";
import {
deleteGalery,
getGaleryById,
getGaleryData,
getGaleryFileData,
approveGalery,
rejectGalery,
} from "@/service/galery";
import { DialogDetailGaleri } from "../dialog/galery-detail-dialog";
import { DialogUpdateGaleri } from "../dialog/galery-update-dialog";
@ -20,7 +21,7 @@ import Cookies from "js-cookie";
export default function Galery() {
const MySwal = withReactContent(Swal);
const [data, setData] = useState<any[]>([]);
const [userLevelId, setUserLevelId] = useState<string | null>(null);
const [userRoleId, setUserRoleId] = useState<string | null>(null);
const [page, setPage] = useState(1);
const [showData, setShowData] = useState("10");
@ -73,10 +74,10 @@ export default function Galery() {
}
};
// 🔹 Ambil userlevelId dari cookies
// 🔹 Ambil userRoleId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
const urie = Cookies.get("urie"); // userRoleId dari cookies
setUserRoleId(urie ?? null);
}, []);
useEffect(() => {
@ -144,6 +145,39 @@ export default function Galery() {
fetchData(); // refresh table
};
const handleRejectGalery = async (id: number) => {
const { value: message } = await MySwal.fire({
title: "Tolak Galery",
input: "textarea",
inputLabel: "Alasan penolakan (opsional)",
inputPlaceholder: "Masukkan alasan penolakan...",
inputAttributes: {
"aria-label": "Masukkan alasan penolakan",
},
showCancelButton: true,
confirmButtonText: "Tolak",
cancelButtonText: "Batal",
confirmButtonColor: "#dc2626",
});
if (message === undefined) {
return; // User cancelled
}
loading();
const res = await rejectGalery(id, message || undefined);
if (res?.error) {
error(res.message || "Gagal menolak galery");
close();
return;
}
close();
success("Galery berhasil ditolak");
fetchData(); // refresh table
};
return (
<div className="mt-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
@ -170,11 +204,13 @@ export default function Galery() {
{/* Status Badge */}
<span
className={`absolute top-3 left-3 text-xs px-3 py-1 rounded-full font-medium
${
$ {
item.status_id === 2
? "bg-green-100 text-green-700"
: item.status_id === 1
? "bg-yellow-100 text-yellow-700"
: item.status_id === 3
? "bg-red-100 text-red-700"
: "bg-gray-100 text-gray-600"
}`}
>
@ -182,6 +218,8 @@ export default function Galery() {
? "Disetujui"
: item.status_id === 1
? "Menunggu"
: item.status_id === 3
? "Ditolak"
: "Tidak Diketahui"}
</span>
</div>
@ -219,14 +257,22 @@ export default function Galery() {
<Pencil className="h-4 w-4" /> Edit
</button>
{/* Tombol Approve - hanya untuk admin dan status pending */}
{userLevelId === "1" && item.status_id === 1 && (
<button
className="flex items-center gap-1 text-green-600 hover:text-green-700 transition"
onClick={() => handleApproveGalery(item.id)}
>
<CheckCheck className="h-4 w-4" /> Approve
</button>
{/* Tombol Approve & Reject - hanya untuk admin dan status pending */}
{userRoleId === "1" && item.status_id === 1 && (
<>
<button
className="flex items-center gap-1 text-green-600 hover:text-green-700 transition"
onClick={() => handleApproveGalery(item.id)}
>
<CheckCheck className="h-4 w-4" /> Approve
</button>
<button
className="flex items-center gap-1 text-red-600 hover:text-red-700 transition"
onClick={() => handleRejectGalery(item.id)}
>
<X className="h-4 w-4" /> Reject
</button>
</>
)}
<button

View File

@ -47,8 +47,8 @@ import {
} from "@/components/ui/table";
import CustomPagination from "../layout/custom-pagination";
import { EditBannerDialog } from "../form/banner-edit-dialog";
import { deleteProduct, getProductPagination, approveProduct } from "@/service/product";
import { CheckCheck, Eye } from "lucide-react";
import { deleteProduct, getProductPagination, approveProduct, rejectProduct } from "@/service/product";
import { CheckCheck, Eye, X } from "lucide-react";
import { useRouter } from "next/navigation";
const columns = [
@ -92,14 +92,14 @@ export default function ProductTable() {
startDate: null,
endDate: null,
});
const [userLevelId, setUserLevelId] = useState<string | null>(null);
const [userRoleId, setUserRoleId] = useState<string | null>(null);
const router = useRouter();
// 🔹 Ambil userlevelId dari cookies
// 🔹 Ambil userRoleId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
const urie = Cookies.get("urie"); // userRoleId dari cookies
setUserRoleId(urie ?? null);
}, []);
useEffect(() => {
@ -209,6 +209,40 @@ export default function ProductTable() {
initState(); // refresh table
};
const handleRejectProduct = async (id: number) => {
const MySwal = withReactContent(Swal);
const { value: message } = await MySwal.fire({
title: "Tolak Product",
input: "textarea",
inputLabel: "Alasan penolakan (opsional)",
inputPlaceholder: "Masukkan alasan penolakan...",
inputAttributes: {
"aria-label": "Masukkan alasan penolakan",
},
showCancelButton: true,
confirmButtonText: "Tolak",
cancelButtonText: "Batal",
confirmButtonColor: "#dc2626",
});
if (message === undefined) {
return; // User cancelled
}
loading();
const res = await rejectProduct(id, message || undefined);
if (res?.error) {
error(res.message || "Gagal menolak product");
close();
return;
}
close();
success("Product berhasil ditolak");
initState(); // refresh table
};
const copyUrlArticle = async (id: number, slug: string) => {
const url =
`${window.location.protocol}//${window.location.host}` +
@ -448,6 +482,10 @@ export default function ProductTable() {
<span className="bg-green-100 text-green-700 text-xs px-3 py-1 rounded-full font-medium">
Disetujui
</span>
) : item.status_id === 3 ? (
<span className="bg-red-100 text-red-700 text-xs px-3 py-1 rounded-full font-medium">
Ditolak
</span>
) : (
<span className="bg-gray-100 text-gray-600 text-xs px-3 py-1 rounded-full font-medium">
{item.status_id || "Tidak Diketahui"}
@ -468,7 +506,7 @@ export default function ProductTable() {
Lihat
</Button>
</Link>
{userLevelId !== "3" && (
{userRoleId !== "3" && (
<Link href={"/admin/product/update"}>
<Button
className="text-[#0F6C75] hover:bg-transparent hover:underline p-0"
@ -479,16 +517,27 @@ export default function ProductTable() {
</Button>
</Link>
)}
{userLevelId === "1" && item.status_id === 1 && (
<Button
variant="ghost"
size="sm"
onClick={() => handleApproveProduct(item.id)}
className="text-green-600 hover:bg-transparent hover:underline p-0"
>
<CheckCheck className="w-4 h-4 mr-1" />
Approve
</Button>
{userRoleId === "1" && item.status_id === 1 && (
<>
<Button
variant="ghost"
size="sm"
onClick={() => handleApproveProduct(item.id)}
className="text-green-600 hover:bg-transparent hover:underline p-0"
>
<CheckCheck className="w-4 h-4 mr-1" />
Approve
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleRejectProduct(item.id)}
className="text-red-600 hover:bg-transparent hover:underline p-0"
>
<X className="w-4 h-4 mr-1" />
Reject
</Button>
</>
)}
<Button
variant="ghost"

View File

@ -47,10 +47,10 @@ import {
} from "@/components/ui/table";
import CustomPagination from "../layout/custom-pagination";
import { EditBannerDialog } from "../form/banner-edit-dialog";
import { Eye, Trash2, CheckCheck } from "lucide-react";
import { Eye, Trash2, CheckCheck, X } from "lucide-react";
import AgentDetailDialog from "../dialog/agent-dialog";
import PromoDetailDialog from "../dialog/promo-dialog";
import { deletePromotion, getPromotionPagination, approvePromotion } from "@/service/promotion";
import { deletePromotion, getPromotionPagination, approvePromotion, rejectPromotion } from "@/service/promotion";
const columns = [
{ name: "No", uid: "no" },
@ -96,12 +96,12 @@ export default function PromotionTable() {
startDate: null,
endDate: null,
});
const [userLevelId, setUserLevelId] = useState<string | null>(null);
const [userRoleId, setUserRoleId] = useState<string | null>(null);
// 🔹 Ambil userlevelId dari cookies
// 🔹 Ambil userRoleId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
const urie = Cookies.get("urie"); // userRoleId dari cookies
setUserRoleId(urie ?? null);
}, []);
useEffect(() => {
@ -204,6 +204,40 @@ export default function PromotionTable() {
initState(); // refresh table
};
const handleRejectPromotion = async (id: number) => {
const MySwal = withReactContent(Swal);
const { value: message } = await MySwal.fire({
title: "Tolak Promotion",
input: "textarea",
inputLabel: "Alasan penolakan (opsional)",
inputPlaceholder: "Masukkan alasan penolakan...",
inputAttributes: {
"aria-label": "Masukkan alasan penolakan",
},
showCancelButton: true,
confirmButtonText: "Tolak",
cancelButtonText: "Batal",
confirmButtonColor: "#dc2626",
});
if (message === undefined) {
return; // User cancelled
}
loading();
const res = await rejectPromotion(id, message || undefined);
if (res?.error) {
error(res.message || "Gagal menolak promotion");
close();
return;
}
close();
success("Promotion berhasil ditolak");
initState(); // refresh table
};
const copyUrlArticle = async (id: number, slug: string) => {
const url =
`${window.location.protocol}//${window.location.host}` +
@ -408,6 +442,10 @@ export default function PromotionTable() {
<span className="bg-green-100 text-green-700 text-xs px-3 py-1 rounded-full font-medium">
Disetujui
</span>
) : item.status_id === 3 ? (
<span className="bg-red-100 text-red-700 text-xs px-3 py-1 rounded-full font-medium">
Ditolak
</span>
) : (
<span className="bg-gray-100 text-gray-600 text-xs px-3 py-1 rounded-full font-medium">
{item.status_id || "Tidak Diketahui"}
@ -432,17 +470,28 @@ export default function PromotionTable() {
</Button>
{/* Tombol Edit */}
{/* Tombol Approve - hanya untuk admin dan status pending */}
{userLevelId === "1" && item.status_id === 1 && (
<Button
variant="ghost"
size="sm"
onClick={() => handleApprovePromotion(item.id)}
className="text-green-600 hover:bg-transparent hover:underline p-0"
>
<CheckCheck className="w-4 h-4 mr-1" />
Approve
</Button>
{/* Tombol Approve & Reject - hanya untuk admin dan status pending */}
{userRoleId === "1" && item.status_id === 1 && (
<>
<Button
variant="ghost"
size="sm"
onClick={() => handleApprovePromotion(item.id)}
className="text-green-600 hover:bg-transparent hover:underline p-0"
>
<CheckCheck className="w-4 h-4 mr-1" />
Approve
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleRejectPromotion(item.id)}
className="text-red-600 hover:bg-transparent hover:underline p-0"
>
<X className="w-4 h-4 mr-1" />
Reject
</Button>
</>
)}
{/* Tombol Hapus */}
<Button

View File

@ -46,3 +46,17 @@ export async function approveAgent(id: string | number) {
};
return await httpPutInterceptor(`/sales-agents/${id}/approve`, {}, headers);
}
export async function rejectAgent(id: string | number, message?: string) {
const headers = {
"content-type": "application/json",
};
return await httpPutInterceptor(`/sales-agents/${id}/reject`, { message }, headers);
}
export async function getApprovalHistory(moduleType: string, moduleId: string | number) {
const headers = {
"content-type": "application/json",
};
return await httpGetInterceptor(`/approval-histories/${moduleType}/${moduleId}`, headers);
}

View File

@ -56,3 +56,17 @@ export async function approveBanner(id: string | number) {
};
return await httpPutInterceptor(`/banners/${id}/approve`, {}, headers);
}
export async function rejectBanner(id: string | number, message?: string) {
const headers = {
"content-type": "application/json",
};
return await httpPutInterceptor(`/banners/${id}/reject`, { message }, headers);
}
export async function getApprovalHistory(moduleType: string, moduleId: string | number) {
const headers = {
"content-type": "application/json",
};
return await httpGetInterceptor(`/approval-histories/${moduleType}/${moduleId}`, headers);
}

View File

@ -77,3 +77,17 @@ export async function approveGalery(id: string | number) {
};
return await httpPutInterceptor(`/galleries/${id}/approve`, {}, headers);
}
export async function rejectGalery(id: string | number, message?: string) {
const headers = {
"content-type": "application/json",
};
return await httpPutInterceptor(`/galleries/${id}/reject`, { message }, headers);
}
export async function getApprovalHistory(moduleType: string, moduleId: string | number) {
const headers = {
"content-type": "application/json",
};
return await httpGetInterceptor(`/approval-histories/${moduleType}/${moduleId}`, headers);
}

View File

@ -49,3 +49,17 @@ export async function approveProduct(id: string | number) {
};
return await httpPutInterceptor(`/products/${id}/approve`, {}, headers);
}
export async function rejectProduct(id: string | number, message?: string) {
const headers = {
"content-type": "application/json",
};
return await httpPutInterceptor(`/products/${id}/reject`, { message }, headers);
}
export async function getApprovalHistory(moduleType: string, moduleId: string | number) {
const headers = {
"content-type": "application/json",
};
return await httpGetInterceptor(`/approval-histories/${moduleType}/${moduleId}`, headers);
}

View File

@ -44,3 +44,17 @@ export async function approvePromotion(id: string | number) {
};
return await httpPutInterceptor(`/promotions/${id}/approve`, {}, headers);
}
export async function rejectPromotion(id: string | number, message?: string) {
const headers = {
"content-type": "application/json",
};
return await httpPutInterceptor(`/promotions/${id}/reject`, { message }, headers);
}
export async function getApprovalHistory(moduleType: string, moduleId: string | number) {
const headers = {
"content-type": "application/json",
};
return await httpGetInterceptor(`/approval-histories/${moduleType}/${moduleId}`, headers);
}