From 006799f872339e3cbdba36682e0c00df06debc16 Mon Sep 17 00:00:00 2001 From: Wira Irawan Date: Fri, 8 May 2026 11:25:02 +0700 Subject: [PATCH] Add admin category management --- src/app/admin/categories/page.tsx | 512 ++++++++++++++++++ src/app/admin/layout.tsx | 1 + .../[categoryId]/subcategories/route.ts | 48 ++ src/app/api/admin/categories/route.ts | 40 ++ 4 files changed, 601 insertions(+) create mode 100644 src/app/admin/categories/page.tsx create mode 100644 src/app/api/admin/categories/[categoryId]/subcategories/route.ts create mode 100644 src/app/api/admin/categories/route.ts diff --git a/src/app/admin/categories/page.tsx b/src/app/admin/categories/page.tsx new file mode 100644 index 0000000..5510876 --- /dev/null +++ b/src/app/admin/categories/page.tsx @@ -0,0 +1,512 @@ +"use client"; + +import { useEffect, useState } from "react"; + +interface CategoryRow { + id: string; + name: string; + description: string | null; +} + +interface SubCategoryRow { + id: string; + name: string; + description: string | null; + subCategoryAttributes: string[]; +} + +function getToken() { + if (typeof window === "undefined") return ""; + return sessionStorage.getItem("token") || localStorage.getItem("token") || ""; +} + +function toText(value: unknown) { + return typeof value === "string" ? value : typeof value === "number" ? String(value) : ""; +} + +function parseCategories(payload: unknown): CategoryRow[] { + const rows = + Array.isArray((payload as { rows?: unknown[] })?.rows) ? (payload as { rows: unknown[] }).rows : + Array.isArray((payload as { data?: unknown[] })?.data) ? (payload as { data: unknown[] }).data : + Array.isArray(payload) ? payload : + []; + + return rows + .map((item) => { + const row = item as Record; + const id = toText(row.id); + const name = toText(row.name); + if (!id || !name) return null; + return { + id, + name, + description: toText(row.description) || null, + }; + }) + .filter((item): item is CategoryRow => Boolean(item)); +} + +function parseSubCategories(payload: unknown): SubCategoryRow[] { + const rows = + Array.isArray((payload as { rows?: unknown[] })?.rows) ? (payload as { rows: unknown[] }).rows : + Array.isArray((payload as { data?: unknown[] })?.data) ? (payload as { data: unknown[] }).data : + Array.isArray(payload) ? payload : + []; + + return rows + .map((item) => { + const row = item as Record; + const id = toText(row.id); + const name = toText(row.name); + if (!id || !name) return null; + + const attributes = Array.isArray(row.subCategoryAttributes) + ? row.subCategoryAttributes.map((attr) => toText(attr)).filter(Boolean) + : []; + + return { + id, + name, + description: toText(row.description) || null, + subCategoryAttributes: attributes, + }; + }) + .filter((item): item is SubCategoryRow => Boolean(item)); +} + +export default function AdminCategoriesPage() { + const [categories, setCategories] = useState([]); + const [selectedCategoryId, setSelectedCategoryId] = useState(""); + const [subcategories, setSubcategories] = useState([]); + const [loadingCategories, setLoadingCategories] = useState(true); + const [loadingSubcategories, setLoadingSubcategories] = useState(false); + const [categoryError, setCategoryError] = useState(""); + const [subcategoryError, setSubcategoryError] = useState(""); + const [categoryName, setCategoryName] = useState(""); + const [categoryDescription, setCategoryDescription] = useState(""); + const [subCategoryName, setSubCategoryName] = useState(""); + const [subCategoryDescription, setSubCategoryDescription] = useState(""); + const [attributeInput, setAttributeInput] = useState(""); + const [subCategoryAttributes, setSubCategoryAttributes] = useState([]); + const [savingCategory, setSavingCategory] = useState(false); + const [savingSubcategory, setSavingSubcategory] = useState(false); + + const selectedCategory = + categories.find((category) => category.id === selectedCategoryId) || null; + + useEffect(() => { + async function loadCategories() { + setLoadingCategories(true); + setCategoryError(""); + try { + const res = await fetch("/api/admin/categories?page=0&size=100", { + headers: { "x-auth-token": getToken() }, + }); + const data = await res.json(); + if (!res.ok) { + throw new Error(data?.responseDesc || data?.error || "Gagal memuat category"); + } + const rows = parseCategories(data); + setCategories(rows); + setSelectedCategoryId((current) => current || rows[0]?.id || ""); + } catch (error) { + setCategoryError(error instanceof Error ? error.message : "Gagal memuat category"); + } finally { + setLoadingCategories(false); + } + } + + loadCategories(); + }, []); + + useEffect(() => { + async function loadSubcategories() { + if (!selectedCategoryId) { + setSubcategories([]); + return; + } + + setLoadingSubcategories(true); + setSubcategoryError(""); + try { + const res = await fetch(`/api/admin/categories/${selectedCategoryId}/subcategories?page=0&size=100`, { + headers: { "x-auth-token": getToken() }, + }); + const data = await res.json(); + if (!res.ok) { + throw new Error(data?.responseDesc || data?.error || "Gagal memuat sub-category"); + } + setSubcategories(parseSubCategories(data)); + } catch (error) { + setSubcategoryError(error instanceof Error ? error.message : "Gagal memuat sub-category"); + } finally { + setLoadingSubcategories(false); + } + } + + loadSubcategories(); + }, [selectedCategoryId]); + + function addAttribute() { + const normalized = attributeInput.trim(); + if (!normalized) return; + setSubCategoryAttributes((current) => + current.includes(normalized) ? current : [...current, normalized] + ); + setAttributeInput(""); + } + + async function handleCreateCategory() { + if (!categoryName.trim()) return; + + setSavingCategory(true); + setCategoryError(""); + try { + const res = await fetch("/api/admin/categories", { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-auth-token": getToken(), + }, + body: JSON.stringify({ + name: categoryName.trim(), + description: categoryDescription.trim() || null, + }), + }); + const data = await res.json(); + if (!res.ok) { + throw new Error(data?.responseDesc || data?.error || "Gagal menambah category"); + } + + const newRows = parseCategories(data); + if (newRows[0]) { + setCategories((current) => [newRows[0], ...current]); + setSelectedCategoryId(newRows[0].id); + } else { + const refreshRes = await fetch("/api/admin/categories?page=0&size=100", { + headers: { "x-auth-token": getToken() }, + }); + const refreshData = await refreshRes.json(); + const rows = parseCategories(refreshData); + setCategories(rows); + setSelectedCategoryId(rows[0]?.id || ""); + } + + setCategoryName(""); + setCategoryDescription(""); + } catch (error) { + setCategoryError(error instanceof Error ? error.message : "Gagal menambah category"); + } finally { + setSavingCategory(false); + } + } + + async function handleCreateSubcategory() { + if (!selectedCategoryId || !subCategoryName.trim()) return; + + setSavingSubcategory(true); + setSubcategoryError(""); + try { + const res = await fetch(`/api/admin/categories/${selectedCategoryId}/subcategories`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-auth-token": getToken(), + }, + body: JSON.stringify({ + name: subCategoryName.trim(), + description: subCategoryDescription.trim() || null, + subCategoryAttributes, + }), + }); + const data = await res.json(); + if (!res.ok) { + throw new Error(data?.responseDesc || data?.error || "Gagal menambah sub-category"); + } + + const refreshRes = await fetch(`/api/admin/categories/${selectedCategoryId}/subcategories?page=0&size=100`, { + headers: { "x-auth-token": getToken() }, + }); + const refreshData = await refreshRes.json(); + setSubcategories(parseSubCategories(refreshData)); + + setSubCategoryName(""); + setSubCategoryDescription(""); + setSubCategoryAttributes([]); + setAttributeInput(""); + } catch (error) { + setSubcategoryError(error instanceof Error ? error.message : "Gagal menambah sub-category"); + } finally { + setSavingSubcategory(false); + } + } + + return ( + <> +
+
+

+ Taxonomy Console +

+

+ Category Management +

+

+ Kelola category dan sub-category produk untuk admin panel menggunakan endpoint category dari backend. +

+
+
+
+

Categories

+

{categories.length}

+
+
+

Sub-Categories

+

{subcategories.length}

+
+
+
+ +
+
+
+
+
+

+ Master Categories +

+

Daftar category utama

+
+ + {categories.length} items + +
+ +
+ setCategoryName(event.target.value)} + placeholder="Nama category" + className="w-full rounded-lg border border-surface-container px-4 py-3 text-sm focus:border-primary focus:outline-none" + /> +