{product.name}
@@ -421,46 +428,48 @@ export default function AdminReviewDetailPage() {
)}
{/* Action bar */}
-
- {actionSuccess && (
-
- check_circle
- {actionSuccess} Mengalihkan...
-
- )}
- {actionError && !showRejectModal && (
-
- error
- {actionError}
-
- )}
- {acting && (
-
- progress_activity
- Memproses review...
-
- )}
- {!actionSuccess && (
-
-
-
-
- )}
-
+ {!isReadonly ? (
+
+ {actionSuccess && (
+
+ check_circle
+ {actionSuccess} Mengalihkan...
+
+ )}
+ {actionError && !showRejectModal && (
+
+ error
+ {actionError}
+
+ )}
+ {acting && (
+
+ progress_activity
+ Memproses review...
+
+ )}
+ {!actionSuccess && (
+
+
+
+
+ )}
+
+ ) : null}
>
);
diff --git a/src/app/api/admin/products/[productId]/route.ts b/src/app/api/admin/products/[productId]/route.ts
new file mode 100644
index 0000000..c3a7cf6
--- /dev/null
+++ b/src/app/api/admin/products/[productId]/route.ts
@@ -0,0 +1,50 @@
+import { NextRequest, NextResponse } from "next/server";
+import { API_URL, makeHeaders } from "@/lib/api";
+
+export async function GET(
+ req: NextRequest,
+ context: { params: Promise<{ productId: string }> }
+) {
+ const token = req.headers.get("x-auth-token") || "";
+ const { productId } = await context.params;
+
+ const res = await fetch(`${API_URL}/api/v1.0/product/${productId}`, {
+ headers: makeHeaders(token),
+ cache: "no-store",
+ });
+
+ const data = await res.json().catch(() => ({}));
+ return NextResponse.json(data, { status: res.status });
+}
+
+export async function DELETE(
+ req: NextRequest,
+ context: { params: Promise<{ productId: string }> }
+) {
+ const token = req.headers.get("x-auth-token") || "";
+ const { productId } = await context.params;
+
+ const res = await fetch(`${API_URL}/api/v1.0/admin/product/${productId}`, {
+ method: "DELETE",
+ headers: makeHeaders(token),
+ });
+
+ const data = await res.json().catch(() => ({}));
+ return NextResponse.json(data, { status: res.status });
+}
+
+export async function PUT(
+ req: NextRequest,
+ context: { params: Promise<{ productId: string }> }
+) {
+ const token = req.headers.get("x-auth-token") || "";
+ const { productId } = await context.params;
+
+ const res = await fetch(`${API_URL}/api/v1.0/admin/product/${productId}/restore`, {
+ method: "PUT",
+ headers: makeHeaders(token),
+ });
+
+ const data = await res.json().catch(() => ({}));
+ return NextResponse.json(data, { status: res.status });
+}
diff --git a/src/app/api/admin/products/route.ts b/src/app/api/admin/products/route.ts
new file mode 100644
index 0000000..3147727
--- /dev/null
+++ b/src/app/api/admin/products/route.ts
@@ -0,0 +1,22 @@
+import { NextRequest, NextResponse } from "next/server";
+import { API_URL, makeHeaders } from "@/lib/api";
+
+export async function GET(req: NextRequest) {
+ const token = req.headers.get("x-auth-token") || "";
+ const { searchParams } = req.nextUrl;
+ const page = parseInt(searchParams.get("page") || "0", 10) + 1;
+ const size = searchParams.get("size") || "20";
+ const tab = searchParams.get("tab");
+
+ const endpoint =
+ tab === "deleted"
+ ? "/api/v1.0/admin/deleted/product"
+ : "/api/v1.0/product";
+
+ const res = await fetch(`${API_URL}${endpoint}?page=${page}&size=${size}`, {
+ headers: makeHeaders(token),
+ cache: "no-store",
+ });
+ const data = await res.json().catch(() => ({}));
+ return NextResponse.json(data, { status: res.status });
+}
diff --git a/src/app/api/products/[productId]/route.ts b/src/app/api/products/[productId]/route.ts
index 51563e6..f835618 100644
--- a/src/app/api/products/[productId]/route.ts
+++ b/src/app/api/products/[productId]/route.ts
@@ -38,8 +38,25 @@ export async function PUT(
try {
const token = normalizeBearerToken(req.headers.get("x-auth-token") || "");
const { productId } = await context.params;
- const body = await req.json();
const isDraft = req.nextUrl.searchParams.get("draft") === "1";
+ const action = req.nextUrl.searchParams.get("action");
+
+ if (action === "unpublish" || action === "restore") {
+ const endpoint =
+ action === "unpublish"
+ ? `${API_URL}/api/v1.0/seller/product/${productId}/unpublish`
+ : `${API_URL}/api/v1.0/seller/product/${productId}/restore`;
+
+ const res = await fetch(endpoint, {
+ method: "PUT",
+ headers: makeHeaders(token),
+ });
+
+ const data = await res.json().catch(() => ({}));
+ return NextResponse.json(data, { status: res.status });
+ }
+
+ const body = await req.json();
const endpoint = isDraft
? `${API_URL}/api/v1.0/product/draft/${productId}`
diff --git a/src/app/api/products/route.ts b/src/app/api/products/route.ts
index c3d9158..fb24e05 100644
--- a/src/app/api/products/route.ts
+++ b/src/app/api/products/route.ts
@@ -13,6 +13,7 @@ export async function GET(req: NextRequest) {
"local-market": "/api/v1.0/seller/local/product",
"out-of-stock": "/api/v1.0/seller/outofstock/product",
rejected: "/api/v1.0/seller/reject/product",
+ deleted: "/api/v1.0/seller/deleted/product",
};
const endpoint = endpointMap[tab || ""] || "/api/v1.0/seller/product";
diff --git a/src/components/admin-product-submenu-nav.tsx b/src/components/admin-product-submenu-nav.tsx
new file mode 100644
index 0000000..50010b9
--- /dev/null
+++ b/src/components/admin-product-submenu-nav.tsx
@@ -0,0 +1,47 @@
+"use client";
+
+import Link from "next/link";
+import { usePathname, useSearchParams } from "next/navigation";
+
+const adminProductSubmenu = [
+ { label: "All Product", href: "/admin/products" },
+ { label: "Deleted", href: "/admin/products?tab=deleted" },
+];
+
+export function AdminProductSubmenuNav() {
+ const pathname = usePathname();
+ const searchParams = useSearchParams();
+ const currentTab = searchParams.get("tab") ?? "";
+
+ if (pathname !== "/admin/products" && !pathname.startsWith("/admin/products/")) {
+ return null;
+ }
+
+ return (
+
+ {adminProductSubmenu.map((submenu) => {
+ const submenuTab = new URLSearchParams(
+ submenu.href.split("?")[1] || ""
+ ).get("tab") ?? "";
+ const isAllProduct = submenu.href === "/admin/products";
+ const isActive =
+ pathname === "/admin/products" &&
+ (isAllProduct ? currentTab === "" : submenuTab === currentTab);
+
+ return (
+
+ {submenu.label}
+
+ );
+ })}
+
+ );
+}
diff --git a/src/components/product-submenu-nav.tsx b/src/components/product-submenu-nav.tsx
index 70dab17..e98d031 100644
--- a/src/components/product-submenu-nav.tsx
+++ b/src/components/product-submenu-nav.tsx
@@ -12,6 +12,7 @@ const productSubmenu = [
{ label: "Local Market", href: "/products?tab=local-market" },
{ label: "Out Of Stock", href: "/products?tab=out-of-stock" },
{ label: "Rejected", href: "/products?tab=rejected" },
+ { label: "Deleted", href: "/products?tab=deleted" },
];
function ProductSubmenuNavInner() {
diff --git a/src/lib/translations/en.ts b/src/lib/translations/en.ts
index 007a393..0e67a0f 100644
--- a/src/lib/translations/en.ts
+++ b/src/lib/translations/en.ts
@@ -358,6 +358,8 @@ export const en = {
empty: "No products found.",
edit: "Edit",
detail: "Detail",
+ restore: "Restore",
+ unpublish: "Unpublish",
table: {
product: "Product",
price: "Price",
@@ -377,6 +379,16 @@ export const en = {
deleting: "Deleting...",
errorGeneric: "Failed to delete product",
},
+ unpublishDialog: {
+ title: "Unpublish Product?",
+ message: "The product will be removed from the active catalog and can be published again later.",
+ productLabel: "Product to unpublish",
+ cancel: "Cancel",
+ confirm: "Yes, Unpublish",
+ processing: "Processing...",
+ errorGeneric: "Failed to unpublish product",
+ },
+ restoreError: "Failed to restore product",
tabs: {
allProduct: "All Product",
draft: "Draft",
@@ -385,6 +397,7 @@ export const en = {
localMarket: "Local Market",
outOfStock: "Out of Stock",
rejected: "Rejected",
+ deleted: "Deleted",
},
},
productNew: {
diff --git a/src/lib/translations/id.ts b/src/lib/translations/id.ts
index d0d0a4f..2814d74 100644
--- a/src/lib/translations/id.ts
+++ b/src/lib/translations/id.ts
@@ -359,6 +359,8 @@ export const id = {
empty: "Tidak ada produk ditemukan.",
edit: "Edit",
detail: "Detail",
+ restore: "Restore",
+ unpublish: "Unpublish",
table: {
product: "Produk",
price: "Harga",
@@ -378,6 +380,16 @@ export const id = {
deleting: "Menghapus...",
errorGeneric: "Gagal menghapus produk",
},
+ unpublishDialog: {
+ title: "Unpublish Produk?",
+ message: "Produk akan dihapus dari katalog aktif dan bisa dipublish kembali nanti.",
+ productLabel: "Produk yang akan di-unpublish",
+ cancel: "Batal",
+ confirm: "Ya, Unpublish",
+ processing: "Memproses...",
+ errorGeneric: "Gagal unpublish produk",
+ },
+ restoreError: "Gagal restore produk",
tabs: {
allProduct: "Semua Produk",
draft: "Draft",
@@ -386,6 +398,7 @@ export const id = {
localMarket: "Pasar Lokal",
outOfStock: "Habis Stok",
rejected: "Ditolak",
+ deleted: "Deleted",
},
},
productNew: {