update table agent,banner,promotion,galery,product and approve flow

This commit is contained in:
Anang Yusman 2026-01-20 14:46:45 +08:00
parent 773ed619e8
commit e59452392d
18 changed files with 1198 additions and 177 deletions

View File

@ -8,14 +8,39 @@ import {
DialogFooter,
} from "@/components/ui/dialog";
import Image from "next/image";
import { CheckCircle } from "lucide-react";
import { Check, CheckCheck, CheckCircle, Clock, X } from "lucide-react";
import { useEffect, useState } from "react";
import { getGaleryFileData } from "@/service/galery";
import { approveGalery, getGaleryFileData } from "@/service/galery";
import { convertDateFormat } from "@/utils/global";
import { Button } from "../ui/button";
import { useRouter } from "next/navigation";
import Cookies from "js-cookie";
import { error, loading, success } from "@/config/swal";
export function DialogDetailGaleri({ open, onClose, data }: any) {
const [images, setImages] = useState<any[]>([]);
const [openApproverHistory, setOpenApproverHistory] = useState(false);
const [userLevelId, setUserLevelId] = useState<string | null>(null);
const [openViewDialog, setOpenViewDialog] = useState(false);
const router = useRouter();
// 🔹 Ambil userlevelId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
}, []);
const [openCommentModal, setOpenCommentModal] = useState(false);
const [commentValue, setCommentValue] = useState("");
const handleSubmitComment = async () => {
// await api.post("/banner/comment", {
// bannerId: viewBanner.id,
// comment: commentValue,
// });
setOpenCommentModal(false);
};
const fetchImages = async () => {
try {
const res = await getGaleryFileData(data.id);
@ -44,6 +69,21 @@ export function DialogDetailGaleri({ open, onClose, data }: any) {
setOpenApproverHistory(true);
};
const handleApproveGalery = async (id: number) => {
loading();
const res = await approveGalery(id);
if (res?.error) {
error(res.message || "Gagal menyetujui galeri");
close();
return;
}
close();
success("Galeri berhasil disetujui");
fetchImages(); // refresh table
};
return (
<>
<Dialog open={open} onOpenChange={onClose}>
@ -107,43 +147,59 @@ export function DialogDetailGaleri({ open, onClose, data }: any) {
{/* Timeline */}
<div>
<p className="font-medium text-sm text-gray-700 mb-2">
<h4 className="text-sm font-semibold text-gray-700 mb-3">
Status Timeline
</p>
</h4>
<div className="flex flex-col gap-3">
<div className="flex items-start gap-3">
<CheckCircle className="text-green-600" />
<div className="space-y-4">
<div className="flex gap-3">
<div className="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center">
<Check className="w-4 h-4 text-green-600" />
</div>
<div>
<p className="font-semibold">Diupload Oleh :</p>
<p className="text-gray-500 text-sm">
{new Date(data.created_at).toLocaleString("id-ID")}
<p className="font-medium text-gray-800">
Diupload oleh Operator
</p>
</div>
</div>
<div className="flex items-start gap-3">
<CheckCircle className="text-green-600" />
<div>
<p className="font-semibold">Disetujui Oleh :</p>
<p className="text-gray-500 text-sm">
{new Date(data.created_at).toLocaleString("id-ID")}
<p className="text-sm text-gray-500">
{convertDateFormat(data?.created_at)} WIB
</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 className="flex gap-3">
<div
className={`w-6 h-6 rounded-full flex items-center justify-center ${
data?.status_id === 1
? "bg-yellow-100"
: data?.status_id === 2
? "bg-green-100"
: "bg-red-100"
}`}
>
{data?.status_id === 1 ? (
<Clock className="w-4 h-4 text-yellow-700" />
) : data?.status_id === 2 ? (
<Check className="w-4 h-4 text-green-600" />
) : (
<X className="w-4 h-4 text-red-700" />
)}
</div>
<div>
<p className="font-medium text-gray-800">
{data?.status_id === 1
? "Menunggu disetujui oleh Approver"
: data?.status_id === 2
? "Disetujui oleh Approver"
: "Ditolak oleh Approver"}
</p>
<p className="text-sm text-gray-500">
{convertDateFormat(data?.updated_at)} WIB
</p>
</div>
</div>
<div className="border rounded-lg px-3 py-3">
<p>Comment : </p>
<div className="flex flex-row justify-between">
@ -157,6 +213,63 @@ export function DialogDetailGaleri({ open, onClose, data }: any) {
</div>
</div>
</div>
</div>
{openViewDialog && userLevelId !== "2" && (
<div className="flex justify-between items-center gap-3 px-6 py-4 border-t bg-[#F2F7FA]">
{data.status_id === 1 ? (
<>
<Button
variant="secondary"
className="bg-blue-200 hover:bg-blue-400"
onClick={(e) => {
e.stopPropagation();
setOpenCommentModal(true);
}}
>
Beri Tanggapan
</Button>
<Button
className=" w-[180]"
variant="destructive"
// onClick={(e) => {
// e.stopPropagation();
// handleReject();
// }}
>
Reject
</Button>
{userLevelId === "1" && (
<Button
// variant="ghost"
size="sm"
className="bg-green-600 hover:bg-green-700 text-white w-[180]"
onClick={(e) => {
e.stopPropagation();
handleApproveGalery(data.id);
}}
>
<CheckCheck className="w-4 h-4 mr-1" />
Approve
</Button>
)}
</>
) : (
<Button
variant="secondary"
className="mx-auto"
onClick={(e) => {
e.stopPropagation();
setOpenViewDialog(false);
}}
>
Tutup
</Button>
)}
</div>
)}
</div>
{/* <DialogFooter className="px-6 pb-6">
<button

View File

@ -7,8 +7,20 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { CheckCircle2, FileText } from "lucide-react";
import { getPromotionById } from "@/service/promotion";
import {
Check,
CheckCheck,
CheckCircle2,
Clock,
FileText,
X,
} from "lucide-react";
import { approvePromotion, getPromotionById } from "@/service/promotion";
import { convertDateFormat } from "@/utils/global";
import { Button } from "../ui/button";
import { useRouter } from "next/navigation";
import Cookies from "js-cookie";
import { error, loading, success } from "@/config/swal";
type PromoDetailDialogProps = {
promoId: number | null;
@ -21,10 +33,20 @@ export default function PromoDetailDialog({
open,
onOpenChange,
}: PromoDetailDialogProps) {
const [loading, setLoading] = useState(false);
const [loadingData, setLoadingData] = useState(false);
const [promo, setPromo] = useState<any>(null);
const [openApproverHistory, setOpenApproverHistory] = useState(false);
const [userLevelId, setUserLevelId] = useState<string | null>(null);
const router = useRouter();
// 🔹 Ambil userlevelId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
}, []);
// FORMAT TANGGAL → DD-MM-YYYY
const formatDate = (dateStr: string) => {
const d = new Date(dateStr);
@ -37,17 +59,37 @@ export default function PromoDetailDialog({
const handleOpenApproverHistory = () => {
setOpenApproverHistory(true);
};
const handleApprovePromotion = async (promoId: number) => {
loading();
const res = await approvePromotion(promoId);
if (res?.error) {
error(res.message || "Gagal menyetujui promotion");
close();
return;
}
close();
success("Promotion berhasil disetujui");
// fetchData(); // refresh table
};
useEffect(() => {
if (!promoId || !open) return;
async function fetchData() {
try {
setLoading(true);
setLoadingData(true);
const res = await getPromotionById(promoId);
// Mapping ke format dialog
const mapped = {
id: res?.data?.data?.id,
title: res?.data?.data?.title,
status_id: res?.data?.data?.status_id,
created_at: res?.data?.data?.created_at,
updated_at: res?.data?.data?.updated_at,
fileSize: "Tidak diketahui",
uploadDate: formatDate(res?.data?.data?.created_at),
status: res?.data?.data?.is_active ? "Aktif" : "Nonaktif",
@ -75,19 +117,32 @@ export default function PromoDetailDialog({
} catch (err) {
console.error("ERROR FETCH PROMO:", err);
} finally {
setLoading(false);
setLoadingData(false);
}
}
fetchData();
}, [promoId, open]);
const [openCommentModal, setOpenCommentModal] = useState(false);
const [commentValue, setCommentValue] = useState("");
const [openViewDialog, setOpenViewDialog] = useState(false);
if (!open) return null;
const handleSubmitComment = async () => {
// await api.post("/banner/comment", {
// bannerId: promo.id,
// comment: commentValue,
// });
setOpenCommentModal(false);
};
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="p-0 max-w-xl overflow-hidden rounded-2xl">
<DialogContent className="p-0 max-w-lg overflow-hidden">
{/* HEADER */}
<div className="bg-gradient-to-r from-[#0f6c75] to-[#145f66] text-white px-6 py-5 relative">
<DialogHeader>
@ -107,7 +162,7 @@ export default function PromoDetailDialog({
{/* BODY */}
<div className="p-6">
{loading ? (
{loadingData ? (
<p className="text-Start text-gray-600">Memuat data...</p>
) : promo ? (
<>
@ -133,28 +188,61 @@ export default function PromoDetailDialog({
</div>
{/* TIMELINE */}
<div className="mt-4">
<p className="text-gray-600 mb-3 font-medium">
<div>
<h4 className="text-sm font-semibold text-gray-700 mb-3">
Status Timeline
</p>
</h4>
<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 className="flex gap-3">
<div className="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center">
<Check className="w-4 h-4 text-green-600" />
</div>
<div>
<p className="font-medium">{item.label}</p>
<p className="text-gray-600 text-sm">
{item.date} {item.time} WIB
<p className="font-medium text-gray-800">
Diupload oleh Operator
</p>
<p className="text-sm text-gray-500">
{convertDateFormat(promo?.created_at)} WIB
</p>
</div>
</div>
))}
<div className="flex gap-3">
<div
className={`w-6 h-6 rounded-full flex items-center justify-center ${
promo?.status_id === 1
? "bg-yellow-100"
: promo?.status_id === 2
? "bg-green-100"
: "bg-red-100"
}`}
>
{promo?.status_id === 1 ? (
<Clock className="w-4 h-4 text-yellow-700" />
) : promo?.status_id === 2 ? (
<Check className="w-4 h-4 text-green-600" />
) : (
<X className="w-4 h-4 text-red-700" />
)}
</div>
<div className="border rounded-lg px-3 py-3 mt-3">
<div>
<p className="font-medium text-gray-800">
{promo?.status_id === 1
? "Menunggu disetujui oleh Approver"
: promo?.status_id === 2
? "Disetujui oleh Approver"
: "Ditolak oleh Approver"}
</p>
<p className="text-sm text-gray-500">
{convertDateFormat(promo?.updated_at)} WIB
</p>
</div>
</div>
<div className="border rounded-lg px-3 py-3">
<p>Comment : </p>
<div className="flex flex-row justify-between">
<button
@ -168,11 +256,67 @@ export default function PromoDetailDialog({
</div>
</div>
</div>
</div>
</>
) : (
<p className="text-center text-gray-600">Data tidak ditemukan</p>
)}
</div>
{userLevelId !== "2" && promo && (
<div className="flex justify-between items-center gap-3 px-6 py-4 border-t bg-[#F2F7FA]">
{promo.status_id === 1 ? (
<>
<Button
variant="secondary"
className="bg-blue-200 hover:bg-blue-400"
onClick={(e) => {
e.stopPropagation();
setOpenCommentModal(true);
}}
>
Beri Tanggapan
</Button>
<Button
className=" w-[150]"
variant="destructive"
// onClick={(e) => {
// e.stopPropagation();
// handleReject();
// }}
>
Reject
</Button>
{userLevelId === "1" && (
<Button
// variant="ghost"
size="sm"
className="bg-green-600 hover:bg-green-700 text-white w-[150]"
onClick={(e) => {
e.stopPropagation();
handleApprovePromotion(promo.id);
}}
>
<CheckCheck className="w-4 h-4 mr-1" />
Approve
</Button>
)}
</>
) : (
<Button
variant="secondary"
className="mx-auto"
onClick={(e) => {
e.stopPropagation();
setOpenViewDialog(false);
}}
>
Tutup
</Button>
)}
</div>
)}
{openApproverHistory && (
<div
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4"
@ -271,7 +415,7 @@ export default function PromoDetailDialog({
</div>
</div>
)}
{/* FOOTER */}
{/* FOOTER
<div className="bg-[#E3EFF4] text-center py-3">
<button
onClick={() => onOpenChange(false)}
@ -279,7 +423,7 @@ export default function PromoDetailDialog({
>
Tutup
</button>
</div>
</div> */}
</DialogContent>
</Dialog>
{openApproverHistory && (
@ -380,6 +524,76 @@ export default function PromoDetailDialog({
</div>
</div>
)}
{openCommentModal && promo && (
<div
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4"
onClick={() => setOpenCommentModal(false)}
>
<div
className="bg-white rounded-2xl shadow-2xl max-w-lg w-full overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
{/* HEADER */}
<div className="bg-gradient-to-br from-[#1F6779] to-[#0F6C75] text-white px-6 py-5 relative">
<button
onClick={() => setOpenCommentModal(false)}
className="absolute top-4 right-4 text-white/80 hover:text-white text-xl"
>
</button>
<h2 className="text-lg font-semibold">Beri Tanggapan</h2>
{/* Badge */}
<div className="flex items-center gap-2 mt-3">
<span className="bg-yellow-100 text-yellow-800 text-xs font-medium px-3 py-1 rounded-full">
Menunggu
</span>
<span className="bg-white text-[#0F6C75] text-xs font-medium px-3 py-1 rounded-full">
Banner
</span>
<span className="bg-white/20 text-white text-xs px-2 py-[2px] rounded-full">
{promo.position}
</span>
</div>
</div>
{/* BODY */}
<div className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
Comment
</label>
<textarea
value={commentValue}
onChange={(e) => setCommentValue(e.target.value)}
placeholder="Masukkan komentar"
className="w-full min-h-[100px] border rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-[#0F6C75]"
/>
</div>
</div>
{/* FOOTER */}
<div className="flex justify-between gap-3 px-6 py-4 border-t bg-[#F2F7FA]">
<button
onClick={() => setOpenCommentModal(false)}
className="flex-1 py-2 rounded-xl bg-blue-100 hover:bg-blue-200 text-gray-700 font-medium"
>
Batal
</button>
<button
onClick={handleSubmitComment}
className="flex-1 py-2 rounded-xl bg-[#1F6779] hover:bg-[#0F6C75] text-white font-medium"
>
Submit
</button>
</div>
</div>
</div>
)}
</>
);
}

View File

@ -1,7 +1,15 @@
"use client";
import { useEffect, useState } from "react";
import { Upload, Plus, Settings, CheckCheck } from "lucide-react";
import {
Upload,
Plus,
Settings,
CheckCheck,
Check,
Clock,
X,
} from "lucide-react";
import Image from "next/image";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@ -14,17 +22,18 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { loading } from "@/config/swal";
import { error, loading, success } from "@/config/swal";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useDropzone } from "react-dropzone";
import z from "zod";
import dynamic from "next/dynamic";
import { useParams } from "next/navigation";
import { useParams, useRouter } from "next/navigation";
import Cookies from "js-cookie";
import { getProductDataById } from "@/service/product";
import { getAgentById } from "@/service/agent";
import { approveAgent, getAgentById, updateAgent } from "@/service/agent";
import { Checkbox } from "@/components/ui/checkbox";
import { convertDateFormat } from "@/utils/global";
const ViewEditor = dynamic(
() => {
@ -85,7 +94,25 @@ export default function DetailAgentForm(props: { isDetail: boolean }) {
const id = params?.id;
const [data, setData] = useState<any>(null);
const [openApproverHistory, setOpenApproverHistory] = useState(false);
const [userLevelId, setUserLevelId] = useState<string | null>(null);
const router = useRouter();
const [openCommentModal, setOpenCommentModal] = useState(false);
const [commentValue, setCommentValue] = useState("");
const handleSubmitComment = async () => {
// await api.post("/banner/comment", {
// bannerId: viewBanner.id,
// comment: commentValue,
// });
setOpenCommentModal(false);
};
// 🔹 Ambil userlevelId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
}, []);
useEffect(() => {
fetchData();
}, []);
@ -101,6 +128,41 @@ export default function DetailAgentForm(props: { isDetail: boolean }) {
setOpenApproverHistory(true);
};
// const handleRejectProduct = async (id: number) => {
// loading();
// const payload = {
// status_id: 3, // ✅ number
// };
// const res = await updateAgent(payload, id);
// if (res?.error) {
// error(res.message || "Gagal menolak product");
// close();
// return;
// }
// close();
// success("Product berhasil ditolak");
// fetchData();
// };
const handleApproveAgent = async (id: number) => {
loading();
const res = await approveAgent(id);
if (res?.error) {
error(res.message || "Gagal menyetujui agent");
close();
return;
}
close();
success("Agent berhasil disetujui");
fetchData(); // refresh table
};
return (
<>
<Card className="w-full border-none shadow-md">
@ -155,6 +217,60 @@ export default function DetailAgentForm(props: { isDetail: boolean }) {
/>
</div>
</div>
<div>
<h4 className="text-sm font-semibold text-gray-700 mb-3">
Status Timeline
</h4>
<div className="space-y-4">
<div className="flex gap-3">
<div className="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center">
<Check className="w-4 h-4 text-green-600" />
</div>
<div>
<p className="font-medium text-gray-800">
Diupload oleh Operator
</p>
<p className="text-sm text-gray-500">
{convertDateFormat(data.created_at)} WIB
</p>
</div>
</div>
</div>
<div className="flex gap-3">
<div
className={`w-6 h-6 rounded-full flex items-center justify-center ${
data?.status_id === 1
? "bg-yellow-100"
: data?.status_id === 2
? "bg-green-100"
: "bg-red-100"
}`}
>
{data?.status_id === 1 ? (
<Clock className="w-4 h-4 text-yellow-700" />
) : data?.status_id === 2 ? (
<Check className="w-4 h-4 text-green-600" />
) : (
<X className="w-4 h-4 text-red-700" />
)}
</div>
<div>
<p className="font-medium text-gray-800">
{data?.status_id === 1
? "Menunggu disetujui oleh Approver"
: data?.status_id === 2
? "Disetujui oleh Approver"
: "Ditolak oleh Approver"}
</p>
<p className="text-sm text-gray-500">
{convertDateFormat(data?.updated_at)} WIB
</p>
</div>
</div>
</div>
<div className="border rounded-lg px-3 py-3">
<p>Comment : </p>
<div className="flex flex-row justify-between">
@ -167,6 +283,47 @@ export default function DetailAgentForm(props: { isDetail: boolean }) {
<p>Jaecoo - Approver | 10/11/2026</p>
</div>
</div>
{userLevelId !== "2" && data && (
<div className="flex justify-between items-center gap-3 px-6 py-4 border-t bg-[#F2F7FA] mt-10">
{data.status_id === 1 ? (
<>
<Button
variant="secondary"
className="bg-blue-200 hover:bg-blue-400"
onClick={() => setOpenCommentModal(true)}
>
Beri Tanggapan
</Button>
<Button
variant="destructive"
className="w-[180px]"
// onClick={() => handleRejectProduct(detailData.id)}
>
Reject
</Button>
{userLevelId === "1" && (
<Button
className="bg-green-600 hover:bg-green-700 text-white w-[180px]"
onClick={() => handleApproveAgent(data.id)}
>
<CheckCheck className="w-4 h-4 mr-1" />
Approve
</Button>
)}
</>
) : (
<Button
variant="secondary"
className="mx-auto"
onClick={() => router.back()}
>
Tutup
</Button>
)}
</div>
)}
{/* <Button className=" bg-teal-800 hover:bg-teal-900 text-white py-3">
Submit
</Button> */}

View File

@ -86,7 +86,7 @@ export default function Login() {
url: "https://dev.mikulnews.com/auth",
userId: profile?.data?.data?.id,
},
response?.data?.data?.access_token
response?.data?.data?.access_token,
);
Cookies.set("profile_picture", profile?.data?.data?.profilePictureUrl, {
expires: 1,
@ -103,7 +103,7 @@ export default function Login() {
Cookies.set("username", profile?.data?.data?.username, {
expires: 1,
});
Cookies.set("urie", profile?.data?.data?.roleId, {
Cookies.set("urie", profile?.data?.data?.userRoleId, {
expires: 1,
});
Cookies.set("roleName", profile?.data?.data?.roleName, {

View File

@ -1,7 +1,15 @@
"use client";
import { useEffect, useState } from "react";
import { Upload, Plus, Settings, CheckCheck } from "lucide-react";
import {
Upload,
Plus,
Settings,
CheckCheck,
Clock,
Check,
X,
} from "lucide-react";
import Image from "next/image";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@ -14,15 +22,20 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { loading } from "@/config/swal";
import { error, loading, success } from "@/config/swal";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useDropzone } from "react-dropzone";
import z from "zod";
import dynamic from "next/dynamic";
import { useParams } from "next/navigation";
import { useParams, useRouter } from "next/navigation";
import Cookies from "js-cookie";
import { getProductDataById } from "@/service/product";
import {
approveProduct,
getProductDataById,
updateProduct,
} from "@/service/product";
import { convertDateFormat } from "@/utils/global";
const ViewEditor = dynamic(
() => {
@ -99,6 +112,15 @@ export default function DetailProductForm(props: { isDetail: boolean }) {
const [startDateValue, setStartDateValue] = useState<any>(null);
const [timeValue, setTimeValue] = useState("00:00");
const [openApproverHistory, setOpenApproverHistory] = useState(false);
const [userLevelId, setUserLevelId] = useState<string | null>(null);
const router = useRouter();
// 🔹 Ambil userlevelId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
}, []);
const { getRootProps, getInputProps } = useDropzone({
onDrop: (acceptedFiles) => {
@ -199,6 +221,17 @@ export default function DetailProductForm(props: { isDetail: boolean }) {
};
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
const [openCommentModal, setOpenCommentModal] = useState(false);
const [commentValue, setCommentValue] = useState("");
const handleSubmitComment = async () => {
// await api.post("/banner/comment", {
// bannerId: viewBanner.id,
// comment: commentValue,
// });
setOpenCommentModal(false);
};
const [uploadTarget, setUploadTarget] = useState<{
type: "spec" | "color";
index: number;
@ -268,6 +301,41 @@ export default function DetailProductForm(props: { isDetail: boolean }) {
}
}
const handleRejectProduct = async (id: number) => {
loading();
const payload = {
status_id: 3, // ✅ number
};
const res = await updateProduct(payload, id);
if (res?.error) {
error(res.message || "Gagal menolak product");
close();
return;
}
close();
success("Product berhasil ditolak");
initState();
};
const handleApproveProduct = async (id: number) => {
loading();
const res = await approveProduct(id);
if (res?.error) {
error(res.message || "Gagal menyetujui product");
close();
return;
}
close();
success("Product berhasil disetujui");
initState(); // refresh table
};
const handleOpenApproverHistory = () => {
setOpenApproverHistory(true);
};
@ -517,21 +585,56 @@ export default function DetailProductForm(props: { isDetail: boolean }) {
</Button> */}
</div>
<div>
<p>Status Timeline</p>
<h4 className="text-sm font-semibold text-gray-700 mb-3">
Status Timeline
</h4>
<div className="space-y-4">
<div className="flex gap-3">
<div className="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center">
<CheckCheck className="w-4 h-4 text-green-600" />
<Check className="w-4 h-4 text-green-600" />
</div>
<div>
<p className="font-medium text-gray-800">Diupload oleh</p>
<p className="font-medium text-gray-800">
Diupload oleh Operator
</p>
<p className="text-sm text-gray-500">
{convertDateFormat(detailData?.created_at)} WIB
</p>
</div>
</div>
<div className="flex gap-3 mt-3">
<div className="w-6 h-6 rounded-full bg-green-100 flex items-center justify-center">
<CheckCheck className="w-4 h-4 text-green-600" />
</div>
<div className="flex gap-3">
<div
className={`w-6 h-6 rounded-full flex items-center justify-center ${
detailData?.status_id === 1
? "bg-yellow-100"
: detailData?.status_id === 2
? "bg-green-100"
: "bg-red-100"
}`}
>
{detailData?.status_id === 1 ? (
<Clock className="w-4 h-4 text-yellow-700" />
) : detailData?.status_id === 2 ? (
<Check className="w-4 h-4 text-green-600" />
) : (
<X className="w-4 h-4 text-red-700" />
)}
</div>
<div>
<p className="font-medium text-gray-800">Disetujui oleh</p>
<p className="font-medium text-gray-800">
{detailData?.status_id === 1
? "Menunggu disetujui oleh Approver"
: detailData?.status_id === 2
? "Disetujui oleh Approver"
: "Ditolak oleh Approver"}
</p>
<p className="text-sm text-gray-500">
{convertDateFormat(detailData?.updated_at)} WIB
</p>
</div>
</div>
</div>
@ -547,6 +650,48 @@ export default function DetailProductForm(props: { isDetail: boolean }) {
<p>Jaecoo - Approver | 10/11/2026</p>
</div>
</div>
{userLevelId !== "2" && detailData && (
<div className="flex justify-between items-center gap-3 px-6 py-4 border-t bg-[#F2F7FA] mt-10">
{detailData.status_id === 1 ? (
<>
<Button
variant="secondary"
className="bg-blue-200 hover:bg-blue-400"
onClick={() => setOpenCommentModal(true)}
>
Beri Tanggapan
</Button>
<Button
variant="destructive"
className="w-[180px]"
onClick={() => handleRejectProduct(detailData.id)}
>
Reject
</Button>
{userLevelId === "1" && (
<Button
className="bg-green-600 hover:bg-green-700 text-white w-[180px]"
onClick={() => handleApproveProduct(detailData.id)}
>
<CheckCheck className="w-4 h-4 mr-1" />
Approve
</Button>
)}
</>
) : (
<Button
variant="secondary"
className="mx-auto"
onClick={() => router.back()}
>
Tutup
</Button>
)}
</div>
)}
{/* <Button className=" bg-teal-800 hover:bg-teal-900 text-white py-3">
Submit
</Button> */}
@ -695,6 +840,76 @@ export default function DetailProductForm(props: { isDetail: boolean }) {
</DialogFooter>
</DialogContent>
</Dialog>
{openCommentModal && detailData && (
<div
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 p-4"
onClick={() => setOpenCommentModal(false)}
>
<div
className="bg-white rounded-2xl shadow-2xl max-w-lg w-full overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
{/* HEADER */}
<div className="bg-gradient-to-br from-[#1F6779] to-[#0F6C75] text-white px-6 py-5 relative">
<button
onClick={() => setOpenCommentModal(false)}
className="absolute top-4 right-4 text-white/80 hover:text-white text-xl"
>
</button>
<h2 className="text-lg font-semibold">Beri Tanggapan</h2>
{/* Badge */}
<div className="flex items-center gap-2 mt-3">
<span className="bg-yellow-100 text-yellow-800 text-xs font-medium px-3 py-1 rounded-full">
Menunggu
</span>
<span className="bg-white text-[#0F6C75] text-xs font-medium px-3 py-1 rounded-full">
Banner
</span>
<span className="bg-white/20 text-white text-xs px-2 py-[2px] rounded-full">
{detailData.position}
</span>
</div>
</div>
{/* BODY */}
<div className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-500 mb-2">
Comment
</label>
<textarea
value={commentValue}
onChange={(e) => setCommentValue(e.target.value)}
placeholder="Masukkan komentar"
className="w-full min-h-[100px] border rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-[#0F6C75]"
/>
</div>
</div>
{/* FOOTER */}
<div className="flex justify-between gap-3 px-6 py-4 border-t bg-[#F2F7FA]">
<button
onClick={() => setOpenCommentModal(false)}
className="flex-1 py-2 rounded-xl bg-blue-100 hover:bg-blue-200 text-gray-700 font-medium"
>
Batal
</button>
<button
onClick={handleSubmitComment}
className="flex-1 py-2 rounded-xl bg-[#1F6779] hover:bg-[#0F6C75] text-white font-medium"
>
Submit
</button>
</div>
</div>
</div>
)}
</>
);
}

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 } from "lucide-react";
import { Eye, CheckCheck } from "lucide-react";
import AgentDetailDialog from "../dialog/agent-dialog";
import { deleteAgent, getAgentData } from "@/service/agent";
import { deleteAgent, getAgentData, approveAgent } from "@/service/agent";
const columns = [
{ name: "No", uid: "no" },
@ -94,6 +94,13 @@ export default function AgentTable() {
startDate: null,
endDate: null,
});
const [userLevelId, setUserLevelId] = useState<string | null>(null);
// 🔹 Ambil userlevelId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
}, []);
useEffect(() => {
initState();
@ -180,6 +187,21 @@ export default function AgentTable() {
setOpenPreview(true);
};
const handleApproveAgent = async (id: number) => {
loading();
const res = await approveAgent(id);
if (res?.error) {
error(res.message || "Gagal menyetujui agent");
close();
return;
}
close();
success("Agent berhasil disetujui");
initState(); // refresh table
};
const copyUrlArticle = async (id: number, slug: string) => {
const url =
`${window.location.protocol}//${window.location.host}` +
@ -386,14 +408,29 @@ export default function AgentTable() {
{/* STATUS */}
<TableCell className="text-center">
<span className="bg-yellow-100 text-yellow-800 text-xs font-medium px-3 py-1 rounded-full">
{item.status_id === 1 ? (
<span className="bg-yellow-100 text-yellow-700 text-xs px-3 py-1 rounded-full font-medium">
Menunggu
</span>
) : item.status_id === 2 ? (
<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"}
</span>
)}
</TableCell>
{/* AKSI */}
<TableCell className="text-center">
<div className="flex items-center justify-center gap-3">
{/* Tombol Lihat */}
<Link href={`/admin/agent/detail/${item.id}`}>
<Button
variant="ghost"
@ -414,8 +451,8 @@ export default function AgentTable() {
<Eye className="w-4 h-4 mr-1" /> Lihat
</Button>
</Link>
{/* Tombol Edit */}
{userLevelId !== "1" && (
<Link href={`/admin/agent/update/${item.id}`}>
<Button
className="text-[#0F6C75] hover:bg-transparent hover:underline p-0"
@ -425,7 +462,19 @@ export default function AgentTable() {
<CreateIconIon className="w-4 h-4 mr-1" /> Edit
</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 Hapus */}
<Button
variant="ghost"

View File

@ -46,7 +46,12 @@ import {
} from "@/components/ui/table";
import CustomPagination from "../layout/custom-pagination";
import { EditBannerDialog } from "../form/banner-edit-dialog";
import { deleteBanner, getBannerData, updateBanner } from "@/service/banner";
import {
deleteBanner,
getBannerData,
updateBanner,
approveBanner,
} from "@/service/banner";
import { Check, CheckCheck, Clock, Eye, X } from "lucide-react";
import { useRouter } from "next/navigation";
@ -219,23 +224,38 @@ export default function ArticleTable() {
await updateBanner(formData, id);
};
const handleApprove = async () => {
if (!viewBanner) return;
// const handleApprove = async () => {
// if (!viewBanner) return;
// loading();
// const formData = new FormData();
// formData.append("status", "2"); // APPROVED
// const res = await updateBanner(formData, viewBanner.id);
// if (res?.error) {
// error(res.message);
// return;
// }
// close();
// success("Banner berhasil disetujui");
// setOpenViewDialog(false);
// initState(); // refresh table
// };
const handleApproveBanner = async (id: number) => {
loading();
const formData = new FormData();
formData.append("status", "2"); // APPROVED
const res = await updateBanner(formData, viewBanner.id);
const res = await approveBanner(id);
if (res?.error) {
error(res.message);
error(res.message || "Gagal menyetujui banner");
close();
return;
}
close();
success("Banner berhasil disetujui");
setOpenViewDialog(false);
initState(); // refresh table
};
@ -463,21 +483,21 @@ export default function ArticleTable() {
{/* STATUS */}
<TableCell className="text-center">
{item.status === "1" ? (
{item.status_id === 1 ? (
<span className="bg-yellow-100 text-yellow-700 text-xs px-3 py-1 rounded-full font-medium">
Menunggu
</span>
) : item.status === "2" ? (
) : item.status_id === 2 ? (
<span className="bg-green-100 text-green-700 text-xs px-3 py-1 rounded-full font-medium">
Disetujui
</span>
) : item.status === "3" ? (
) : item.status_id === 3 ? (
<span className="bg-red-100 text-red-700 text-xs px-3 py-1 rounded-full font-medium">
Canceled
Ditolak
</span>
) : (
<span className="bg-gray-100 text-gray-600 text-xs px-3 py-1 rounded-full font-medium">
Tidak Diketahui
{item.status_id || "Tidak Diketahui"}
</span>
)}
</TableCell>
@ -494,7 +514,7 @@ export default function ArticleTable() {
<Eye className="w-4 h-4 mr-1" />
Lihat
</Button>
{userLevelId !== "3" && (
{userLevelId !== "1" && (
<Button
variant="ghost"
size="sm"
@ -505,6 +525,17 @@ 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>
)} */}
<Button
variant="ghost"
size="sm"
@ -647,21 +678,21 @@ export default function ArticleTable() {
<div className="flex items-center gap-2 mt-3">
<span
className={`text-xs font-medium px-3 py-1 rounded-full ${
viewBanner.status === "1"
viewBanner.status_id === 1
? "bg-yellow-100 text-yellow-800"
: viewBanner.status === "2"
: viewBanner.status_id === 2
? "bg-green-100 text-green-800"
: viewBanner.status === "3"
: viewBanner.status_id === 3
? "bg-red-100 text-red-800"
: "bg-gray-100 text-gray-800"
}`}
>
{viewBanner.status === "1"
{viewBanner.status_id === 1
? "Menunggu"
: viewBanner.status === "2"
: viewBanner.status_id === 2
? "Disetujui"
: viewBanner.status === "3"
? "Reject"
: viewBanner.status_id === 3
? "Ditolak"
: "Tidak Diketahui"}
</span>
@ -731,16 +762,16 @@ export default function ArticleTable() {
<div className="flex gap-3">
<div
className={`w-6 h-6 rounded-full flex items-center justify-center ${
viewBanner.status === "1"
viewBanner.status_id === 1
? "bg-yellow-100"
: viewBanner.status === "2"
: viewBanner.status_id === 2
? "bg-green-100"
: "bg-red-100"
}`}
>
{viewBanner.status === "1" ? (
{viewBanner.status_id === 1 ? (
<Clock className="w-4 h-4 text-yellow-700" />
) : viewBanner.status === "2" ? (
) : viewBanner.status_id === 2 ? (
<Check className="w-4 h-4 text-green-600" />
) : (
<X className="w-4 h-4 text-red-700" />
@ -749,9 +780,9 @@ export default function ArticleTable() {
<div>
<p className="font-medium text-gray-800">
{viewBanner.status === "1"
{viewBanner.status_id === 1
? "Menunggu disetujui oleh Approver"
: viewBanner.status === "2"
: viewBanner.status_id === 2
? "Disetujui oleh Approver"
: "Ditolak oleh Approver"}
</p>
@ -781,7 +812,7 @@ export default function ArticleTable() {
{/* FOOTER */}
{userLevelId !== "2" && (
<div className="flex justify-between items-center gap-3 px-6 py-4 border-t bg-[#F2F7FA]">
{viewBanner.status === "1" ? (
{viewBanner.status_id === 1 ? (
<>
<Button
variant="secondary"
@ -795,8 +826,8 @@ export default function ArticleTable() {
</Button>
<Button
className=" w-[180]"
variant="destructive"
className="w-[180]"
onClick={(e) => {
e.stopPropagation();
handleReject();
@ -805,15 +836,20 @@ export default function ArticleTable() {
Reject
</Button>
{userLevelId === "1" && (
<Button
// variant="ghost"
size="sm"
className="bg-green-600 hover:bg-green-700 text-white w-[180]"
onClick={(e) => {
e.stopPropagation();
handleApprove();
handleApproveBanner(viewBanner.id);
}}
>
Approved
<CheckCheck className="w-4 h-4 mr-1" />
Approve
</Button>
)}
</>
) : (
<Button

View File

@ -2,22 +2,32 @@
import { useEffect, useState } from "react";
import Image from "next/image";
import { Eye, Pencil, Trash2, Calendar, MapPin } from "lucide-react";
import {
Eye,
Pencil,
Trash2,
Calendar,
MapPin,
CheckCheck,
} from "lucide-react";
import {
deleteGalery,
getGaleryById,
getGaleryData,
getGaleryFileData,
approveGalery,
} from "@/service/galery";
import { DialogDetailGaleri } from "../dialog/galery-detail-dialog";
import { DialogUpdateGaleri } from "../dialog/galery-update-dialog";
import { error, success } from "@/config/swal";
import { error, success, loading, close } from "@/config/swal";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
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 [page, setPage] = useState(1);
const [showData, setShowData] = useState("10");
@ -47,7 +57,7 @@ export default function Galery() {
// Filter file dengan gallery_id sama dengan item.id
const filteredImages = images?.filter(
(img: any) => img.gallery_id === item.id
(img: any) => img.gallery_id === item.id,
);
// Ambil gambar pertama sebagai thumbnail
@ -61,7 +71,7 @@ export default function Galery() {
} catch (e) {
return { ...item, image_url: null };
}
})
}),
);
setData(merged);
@ -70,6 +80,12 @@ export default function Galery() {
}
};
// 🔹 Ambil userlevelId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
}, []);
useEffect(() => {
fetchData();
}, [page, showData, search]);
@ -120,6 +136,21 @@ export default function Galery() {
});
};
const handleApproveGalery = async (id: number) => {
loading();
const res = await approveGalery(id);
if (res?.error) {
error(res.message || "Gagal menyetujui galeri");
close();
return;
}
close();
success("Galeri berhasil disetujui");
fetchData(); // refresh table
};
return (
<div className="mt-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
@ -147,12 +178,18 @@ export default function Galery() {
<span
className={`absolute top-3 left-3 text-xs px-3 py-1 rounded-full font-medium
${
item.is_active
item.status_id === 2
? "bg-green-100 text-green-700"
: "bg-red-100 text-red-600"
: item.status_id === 1
? "bg-yellow-100 text-yellow-700"
: "bg-gray-100 text-gray-600"
}`}
>
{item.is_active ? "Aktif" : "Tidak Aktif"}
{item.status_id === 2
? "Disetujui"
: item.status_id === 1
? "Menunggu"
: "Tidak Diketahui"}
</span>
</div>
@ -181,13 +218,23 @@ export default function Galery() {
>
<Eye className="h-4 w-4" /> Lihat
</button>
{userLevelId !== "1" && (
<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>
)}
{/* 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>
)} */}
<button
className="flex items-center gap-1 text-red-600 hover:text-red-700 transition"

View File

@ -47,7 +47,11 @@ import {
} from "@/components/ui/table";
import CustomPagination from "../layout/custom-pagination";
import { EditBannerDialog } from "../form/banner-edit-dialog";
import { deleteProduct, getProductPagination } from "@/service/product";
import {
deleteProduct,
getProductPagination,
approveProduct,
} from "@/service/product";
import { CheckCheck, Eye } from "lucide-react";
import { useRouter } from "next/navigation";
@ -194,6 +198,21 @@ export default function ProductTable() {
setOpenPreview(true);
};
const handleApproveProduct = async (id: number) => {
loading();
const res = await approveProduct(id);
if (res?.error) {
error(res.message || "Gagal menyetujui product");
close();
return;
}
close();
success("Product berhasil disetujui");
initState(); // refresh table
};
const copyUrlArticle = async (id: number, slug: string) => {
const url =
`${window.location.protocol}//${window.location.host}` +
@ -425,13 +444,21 @@ export default function ProductTable() {
{/* 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">
{item.status_id === 1 ? (
<span className="bg-yellow-100 text-yellow-700 text-xs px-3 py-1 rounded-full font-medium">
Menunggu
</span>
) : item.status_id === 2 ? (
<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-yellow-100 text-yellow-800 text-xs font-medium px-3 py-1 rounded-full">
Menunggu
<span className="bg-gray-100 text-gray-600 text-xs px-3 py-1 rounded-full font-medium">
{item.status_id || "Tidak Diketahui"}
</span>
)}
</TableCell>
@ -449,7 +476,7 @@ export default function ProductTable() {
Lihat
</Button>
</Link>
{userLevelId !== "3" && (
{userLevelId !== "1" && (
<Link href={"/admin/product/update"}>
<Button
className="text-[#0F6C75] hover:bg-transparent hover:underline p-0"
@ -460,6 +487,17 @@ 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>
)} */}
<Button
variant="ghost"
size="sm"

View File

@ -47,10 +47,14 @@ import {
} from "@/components/ui/table";
import CustomPagination from "../layout/custom-pagination";
import { EditBannerDialog } from "../form/banner-edit-dialog";
import { Eye, Trash2 } from "lucide-react";
import { Eye, Trash2, CheckCheck } from "lucide-react";
import AgentDetailDialog from "../dialog/agent-dialog";
import PromoDetailDialog from "../dialog/promo-dialog";
import { deletePromotion, getPromotionPagination } from "@/service/promotion";
import {
deletePromotion,
getPromotionPagination,
approvePromotion,
} from "@/service/promotion";
const columns = [
{ name: "No", uid: "no" },
@ -96,6 +100,13 @@ export default function PromotionTable() {
startDate: null,
endDate: null,
});
const [userLevelId, setUserLevelId] = useState<string | null>(null);
// 🔹 Ambil userlevelId dari cookies
useEffect(() => {
const ulne = Cookies.get("ulne"); // contoh: "3"
setUserLevelId(ulne ?? null);
}, []);
useEffect(() => {
initState();
@ -182,6 +193,21 @@ export default function PromotionTable() {
setOpenPreview(true);
};
const handleApprovePromotion = async (id: number) => {
loading();
const res = await approvePromotion(id);
if (res?.error) {
error(res.message || "Gagal menyetujui promotion");
close();
return;
}
close();
success("Promotion berhasil disetujui");
initState(); // refresh table
};
const copyUrlArticle = async (id: number, slug: string) => {
const url =
`${window.location.protocol}//${window.location.host}` +
@ -378,9 +404,19 @@ export default function PromotionTable() {
{/* STATUS */}
<TableCell className="text-center">
<span className="bg-yellow-100 text-yellow-800 text-xs font-medium px-3 py-1 rounded-full">
{item.status_id === 1 ? (
<span className="bg-yellow-100 text-yellow-700 text-xs px-3 py-1 rounded-full font-medium">
Menunggu
</span>
) : item.status_id === 2 ? (
<span className="bg-green-100 text-green-700 text-xs px-3 py-1 rounded-full font-medium">
Disetujui
</span>
) : (
<span className="bg-gray-100 text-gray-600 text-xs px-3 py-1 rounded-full font-medium">
{item.status_id || "Tidak Diketahui"}
</span>
)}
</TableCell>
{/* AKSI */}
@ -400,6 +436,18 @@ 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 Hapus */}
<Button
variant="ghost"

61
service/ENV_SETUP.md Normal file
View File

@ -0,0 +1,61 @@
# Environment Setup untuk API Services
## Cara Setup Environment Variables
Buat file `.env.local` di root project dengan konfigurasi berikut:
```env
# API Endpoints
NEXT_PUBLIC_API_BASE_URL=https://jaecookelapagading.com/api
NEXT_PUBLIC_LOCAL_API_URL=http://localhost:8800
NEXT_PUBLIC_CLIENT_KEY=bb65b1ad-e954-4a1a-b4d0-74df5bb0b640
```
## Penjelasan
- `NEXT_PUBLIC_API_BASE_URL`: URL base untuk API (production atau development)
- `NEXT_PUBLIC_LOCAL_API_URL`: URL untuk API lokal (localhost:8800)
- `NEXT_PUBLIC_CLIENT_KEY`: Client key untuk authentikasi API
## Cara Menggunakan
### 1. Untuk memanggil localhost:8800
Ubah `NEXT_PUBLIC_API_BASE_URL` di `.env.local`:
```env
NEXT_PUBLIC_API_BASE_URL=http://localhost:8800
```
### 2. Untuk memanggil API production
```env
NEXT_PUBLIC_API_BASE_URL=https://jaecookelapagading.com/api
```
### 3. Menggunakan service
```typescript
// Tanpa authentication
import { getDataFromLocal, postDataToLocal } from '@/service/local-api-service';
const data = await getDataFromLocal('/endpoint');
// Dengan authentication (menggunakan Bearer token dari cookies)
import { getDataWithAuth, postDataWithAuth } from '@/service/local-api-service';
const data = await getDataWithAuth('/endpoint');
```
## File yang Sudah Diupdate
- `service/http-config/axios-base-instance.ts` - Membaca dari env variable
- `service/http-config/axios-interceptor-instance.ts` - Membaca dari env variable
- `service/local-api-service.ts` - Service helper untuk memanggil API
## Catatan
- Pastikan file `.env.local` tidak di-commit ke repository
- File `.env.local` sudah ada di `.gitignore`
- Restart development server setelah mengubah environment variables

View File

@ -39,3 +39,10 @@ export async function deleteAgent(id: string) {
};
return await httpDeleteInterceptor(`sales-agents/${id}`, headers);
}
export async function approveAgent(id: string | number) {
const headers = {
"content-type": "application/json",
};
return await httpPutInterceptor(`/sales-agents/${id}/approve`, {}, headers);
}

View File

@ -49,3 +49,10 @@ export async function deleteBanner(id: string) {
};
return await httpDeleteInterceptor(`banners/${id}`, headers);
}
export async function approveBanner(id: string | number) {
const headers = {
"content-type": "application/json",
};
return await httpPutInterceptor(`/banners/${id}/approve`, {}, headers);
}

View File

@ -70,3 +70,10 @@ export async function deleteGaleryFile(id: any) {
};
return await httpDeleteInterceptor(`gallery-files/${id}`, headers);
}
export async function approveGalery(id: string | number) {
const headers = {
"content-type": "application/json",
};
return await httpPutInterceptor(`/galleries/${id}/approve`, {}, headers);
}

View File

@ -1,12 +1,16 @@
import axios from "axios";
const baseURL = "https://jaecoocihampelasbdg.com/api";
// Mengambil base URL dari environment variable, default ke API production
const baseURL =
process.env.NEXT_PUBLIC_API_BASE_URL || "https://jaecoocihampelasbdg.com/api";
const clientKey =
process.env.NEXT_PUBLIC_CLIENT_KEY || "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640";
const axiosBaseInstance = axios.create({
baseURL,
headers: {
"Content-Type": "application/json",
"X-Client-Key": "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640",
"X-Client-Key": clientKey,
},
});

View File

@ -2,7 +2,11 @@ import axios from "axios";
import { postSignIn } from "../master-user";
import Cookies from "js-cookie";
const baseURL = "https://jaecoocihampelasbdg.com/api";
// Mengambil base URL dari environment variable, default ke API production
const baseURL =
process.env.NEXT_PUBLIC_API_BASE_URL || "https://jaecoocihampelasbdg.com/api";
const clientKey =
process.env.NEXT_PUBLIC_CLIENT_KEY || "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640";
const refreshToken = Cookies.get("refresh_token");
@ -10,7 +14,7 @@ const axiosInterceptorInstance = axios.create({
baseURL,
headers: {
"Content-Type": "application/json",
"X-Client-Key": "bb65b1ad-e954-4a1a-b4d0-74df5bb0b640",
"X-Client-Key": clientKey,
},
withCredentials: true,
});
@ -28,7 +32,7 @@ axiosInterceptorInstance.interceptors.request.use(
},
(error) => {
return Promise.reject(error);
}
},
);
// Response interceptor
@ -66,7 +70,7 @@ axiosInterceptorInstance.interceptors.response.use(
}
return Promise.reject(error);
}
},
);
export default axiosInterceptorInstance;

View File

@ -42,3 +42,10 @@ export async function deleteProduct(id: string) {
};
return await httpDeleteInterceptor(`products/${id}`, headers);
}
export async function approveProduct(id: string | number) {
const headers = {
"content-type": "application/json",
};
return await httpPutInterceptor(`/products/${id}/approve`, {}, headers);
}

View File

@ -37,3 +37,10 @@ export async function deletePromotion(id: string) {
};
return await httpDeleteInterceptor(`promotions/${id}`, headers);
}
export async function approvePromotion(id: string | number) {
const headers = {
"content-type": "application/json",
};
return await httpPutInterceptor(`/promotions/${id}/approve`, {}, headers);
}