diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx index 6714245..416c802 100644 --- a/src/app/(dashboard)/dashboard/page.tsx +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -1,52 +1,94 @@ "use client"; +import { useEffect, useState } from "react"; import { useLanguage } from "@/lib/i18n-context"; -const recentOrders = [ - { - id: "ORD-001", - product: "Titan Minimalist V2", - sku: "TM-9920", - customer: "Sarah Jenkins", - location: "London, UK", - date: "Oct 24, 2023", - amount: "$249.00", - status: "Processing", - statusColor: "text-primary bg-primary/10", - }, - { - id: "ORD-002", - product: "Sonic Wave Pro", - sku: "SW-1021", - customer: "Marcus Thorne", - location: "Berlin, DE", - date: "Oct 23, 2023", - amount: "$499.00", - status: "Shipped", - statusColor: "text-secondary bg-secondary/10", - }, - { - id: "ORD-003", - product: "Pulse Runner X", - sku: "PR-8821", - customer: "Elena Rodriguez", - location: "Madrid, ES", - date: "Oct 23, 2023", - amount: "$125.00", - status: "Delivered", - statusColor: "text-tertiary bg-tertiary/10", - }, -]; +interface AnalyticsPoint { + label: string; + value: number; +} -const barHeights = [40, 65, 50, 85, 70, 60, 95, 45, 55, 80]; +interface DashboardOrder { + id: string; + product: string; + sku: string; + customer: string; + location: string; + date: string; + amount: number; + status: string; +} + +interface SellerDashboardPayload { + metrics: { + totalProducts: number; + soldProducts: number; + refundProducts: number; + internationalProducts: number; + localProducts: number; + }; + analytics: AnalyticsPoint[]; + recentOrders: DashboardOrder[]; +} + +function getToken() { + if (typeof window === "undefined") return ""; + return sessionStorage.getItem("token") || localStorage.getItem("token") || ""; +} + +function formatCurrency(value: number) { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + maximumFractionDigits: 2, + }).format(value || 0); +} + +function statusColor(status: string) { + const normalized = status.toLowerCase(); + if (normalized.includes("ship")) return "text-secondary bg-secondary/10"; + if (normalized.includes("deliver")) return "text-tertiary bg-tertiary/10"; + if (normalized.includes("cancel") || normalized.includes("reject")) return "text-error bg-error/10"; + return "text-primary bg-primary/10"; +} export default function DashboardPage() { const { t } = useLanguage(); const d = t.dashboard.overview; + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + async function load() { + setLoading(true); + setError(""); + try { + const res = await fetch("/api/dashboard/seller", { + headers: { "x-auth-token": getToken() }, + }); + const result = await res.json(); + if (!res.ok) { + throw new Error(result?.responseDesc || result?.error || "Failed to load dashboard"); + } + setData(result); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to load dashboard"); + } finally { + setLoading(false); + } + } + + load(); + }, []); + + const analytics = data?.analytics || []; + const maxAnalytics = Math.max(...analytics.map((item) => item.value), 1); + const totalRevenue = (data?.recentOrders || []).reduce((sum, item) => sum + item.amount, 0); + const totalOrders = data?.recentOrders.length || 0; return (
- {/* Hero Title */}

{d.title} @@ -54,88 +96,93 @@ export default function DashboardPage() {

{d.subtitle}

- {/* Stats Grid */}
- {/* Total Products */}

{d.totalProducts}

-

1,284

+

+ {loading ? "—" : (data?.metrics.totalProducts ?? 0).toLocaleString("en-US")} +

trending_up - +12.5% {d.vsLastMonth} + {loading ? "Loading..." : `${data?.metrics.internationalProducts ?? 0} international`}
- {/* Total Buyers */}

{d.totalBuyers}

-

42,502

+

+ {loading ? "—" : (data?.metrics.soldProducts ?? 0).toLocaleString("en-US")} +

group - {d.globalReach} + {loading ? "Loading..." : `${data?.metrics.localProducts ?? 0} local`}
- {/* Refunds */}

{d.refunds}

-

142

+

+ {loading ? "—" : (data?.metrics.refundProducts ?? 0).toLocaleString("en-US")} +

history - 0.3% {d.returnRate} + {loading ? "Loading..." : `${totalOrders} recent orders`}
- {/* Analytics Row */}
- {/* Orders Analytics Chart */}

{d.ordersAnalytics}

{d.ordersSubtitle}

- +
+ {d.last30Days} +
- {/* Bar Chart */}
- {barHeights.map((height, i) => ( -
- ))} + {loading ? ( +
+ Loading analytics... +
+ ) : analytics.length === 0 ? ( +
+ No analytics available +
+ ) : ( + analytics.map((item, i) => ( +
+ )) + )}
- {d.wk} 1 - {d.wk} 2 - {d.wk} 3 - {d.wk} 4 + {(analytics.length ? analytics : [{ label: `${d.wk} 1`, value: 0 }]).slice(0, 4).map((item) => ( + {item.label} + ))}
- {/* Earnings */}

{d.earnings}

- - {/* Donut Chart Placeholder */}
- $84.2k + + {loading ? "—" : formatCurrency(totalRevenue)} + {d.grossRevenue} @@ -151,31 +200,29 @@ export default function DashboardPage() {
- {/* Legend */}
{[ - { color: "bg-primary", label: d.directSales, pct: "65%" }, - { color: "bg-secondary", label: d.retailPartners, pct: "25%" }, - { color: "bg-tertiary", label: d.affiliates, pct: "10%" }, + { color: "bg-primary", label: d.directSales, value: `${data?.metrics.totalProducts ?? 0}` }, + { color: "bg-secondary", label: d.retailPartners, value: `${data?.metrics.soldProducts ?? 0}` }, + { color: "bg-tertiary", label: d.affiliates, value: `${data?.metrics.refundProducts ?? 0}` }, ].map((item) => (
{item.label}
- {item.pct} + {item.value}
))}
- {/* Recent Orders Table */}

{d.recentOrders}

@@ -192,7 +239,7 @@ export default function DashboardPage() { - {recentOrders.map((order) => ( + {(data?.recentOrders || []).map((order) => (
@@ -201,7 +248,7 @@ export default function DashboardPage() {

{order.product}

-

SKU: {order.sku}

+

SKU: {order.sku || "—"}

@@ -210,9 +257,9 @@ export default function DashboardPage() {

{order.location}

{order.date} - {order.amount} + {formatCurrency(order.amount)} - + {order.status} @@ -223,10 +270,23 @@ export default function DashboardPage() { ))} + {!loading && !error && (data?.recentOrders || []).length === 0 ? ( + + + No recent orders available. + + + ) : null}
+ + {error ? ( +
+ {error} +
+ ) : null}
); } diff --git a/src/app/(dashboard)/products/[productId]/detail/page.tsx b/src/app/(dashboard)/products/[productId]/detail/page.tsx index 590c014..d831450 100644 --- a/src/app/(dashboard)/products/[productId]/detail/page.tsx +++ b/src/app/(dashboard)/products/[productId]/detail/page.tsx @@ -130,6 +130,7 @@ function ProductDetailPageInner() { const params = useParams<{ productId: string }>(); const searchParams = useSearchParams(); const isDraft = searchParams.get("draft") === "1"; + const isReview = searchParams.get("review") === "1"; const [product, setProduct] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); @@ -137,7 +138,11 @@ function ProductDetailPageInner() { useEffect(() => { if (!params.productId) return; - const url = `/api/products/${params.productId}${isDraft ? "?draft=1" : ""}`; + const requestParams = new URLSearchParams(); + if (isDraft) requestParams.set("draft", "1"); + if (isReview) requestParams.set("review", "1"); + const query = requestParams.toString(); + const url = `/api/products/${params.productId}${query ? `?${query}` : ""}`; fetch(url, { headers: { "x-auth-token": getToken() } }) .then((r) => r.json()) .then((j) => { @@ -146,7 +151,7 @@ function ProductDetailPageInner() { }) .catch(() => setError(errorLoadText)) .finally(() => setLoading(false)); - }, [errorLoadText, params.productId, isDraft]); + }, [errorLoadText, params.productId, isDraft, isReview]); if (loading) { return ( @@ -185,6 +190,7 @@ function ProductDetailPageInner() { .filter(isNonEmptyString) : []), ]; + const isReviewProduct = isReview || product.state === "REVIEW"; return (
@@ -202,13 +208,15 @@ function ProductDetailPageInner() {

{product.name || d.title}

{product.state || "DRAFT"}

- - edit - {d.editProduct} - + {!isReviewProduct ? ( + + edit + {d.editProduct} + + ) : null}
@@ -493,13 +501,15 @@ function ProductDetailPageInner() { arrow_back Kembali - - edit - Edit Produk - + {!isReviewProduct ? ( + + edit + Edit Produk + + ) : null} diff --git a/src/app/(dashboard)/products/[productId]/edit/page.tsx b/src/app/(dashboard)/products/[productId]/edit/page.tsx index 4d0dbf6..604f6c4 100644 --- a/src/app/(dashboard)/products/[productId]/edit/page.tsx +++ b/src/app/(dashboard)/products/[productId]/edit/page.tsx @@ -150,8 +150,10 @@ interface ApiModel extends ApiMeasurement { } interface ApiProductImage { + id?: string | number | null; sequence?: number | null; imageId?: string | number | null; + image?: string | number | null; } interface ApiProductFile { @@ -415,7 +417,7 @@ function apiToEditState(data: ApiProduct): EditState { (a: ApiProductImage, b: ApiProductImage) => (a.sequence ?? 0) - (b.sequence ?? 0) ) - .map((img: ApiProductImage) => toStr(img.imageId)) + .map((img: ApiProductImage) => toStr(img.imageId ?? img.image)) : [], keywords: Array.isArray(data?.productKeyWords) ? data.productKeyWords.filter(Boolean) : [], features: Array.isArray(data?.productFeatures) ? data.productFeatures.filter(Boolean) : [], diff --git a/src/app/(dashboard)/products/page.tsx b/src/app/(dashboard)/products/page.tsx index 1fd34cf..f48b176 100644 --- a/src/app/(dashboard)/products/page.tsx +++ b/src/app/(dashboard)/products/page.tsx @@ -148,6 +148,7 @@ function ProductsPageInner() { const searchParams = useSearchParams(); const tab = searchParams.get("tab"); const activeTab = tabFromQuery(tab); + const isInReviewTab = activeTab === "In Review"; const [rows, setRows] = useState([]); const [loading, setLoading] = useState(true); @@ -442,14 +443,16 @@ function ProductsPageInner() {
+ {!isInReviewTab ? ( + + {p.edit} + + ) : null} - {p.edit} - - {p.detail} diff --git a/src/app/admin/categories/page.tsx b/src/app/admin/categories/page.tsx index 5510876..117b87b 100644 --- a/src/app/admin/categories/page.tsx +++ b/src/app/admin/categories/page.tsx @@ -15,6 +15,18 @@ interface SubCategoryRow { subCategoryAttributes: string[]; } +type CategoryFormState = { + name: string; + description: string; +}; + +type SubCategoryFormState = { + name: string; + description: string; + attributes: string[]; + attributeInput: string; +}; + function getToken() { if (typeof window === "undefined") return ""; return sessionStorage.getItem("token") || localStorage.getItem("token") || ""; @@ -26,10 +38,13 @@ function toText(value: unknown) { 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 : - []; + 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) => { @@ -48,10 +63,13 @@ function parseCategories(payload: unknown): CategoryRow[] { 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 : - []; + 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) => { @@ -61,7 +79,18 @@ function parseSubCategories(payload: unknown): SubCategoryRow[] { if (!id || !name) return null; const attributes = Array.isArray(row.subCategoryAttributes) - ? row.subCategoryAttributes.map((attr) => toText(attr)).filter(Boolean) + ? row.subCategoryAttributes + .map((attr) => { + if (typeof attr === "string" || typeof attr === "number") { + return toText(attr); + } + if (attr && typeof attr === "object") { + const record = attr as Record; + return toText(record.paramName) || toText(record.name) || toText(record.value); + } + return ""; + }) + .filter(Boolean) : []; return { @@ -74,6 +103,14 @@ function parseSubCategories(payload: unknown): SubCategoryRow[] { .filter((item): item is SubCategoryRow => Boolean(item)); } +function emptyCategoryForm(): CategoryFormState { + return { name: "", description: "" }; +} + +function emptySubCategoryForm(): SubCategoryFormState { + return { name: "", description: "", attributes: [], attributeInput: "" }; +} + export default function AdminCategoriesPage() { const [categories, setCategories] = useState([]); const [selectedCategoryId, setSelectedCategoryId] = useState(""); @@ -82,165 +119,269 @@ export default function AdminCategoriesPage() { 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 [categoryForm, setCategoryForm] = useState(emptyCategoryForm); + const [subCategoryForm, setSubCategoryForm] = useState(emptySubCategoryForm); + const [editingCategoryId, setEditingCategoryId] = useState(""); + const [editingSubCategoryId, setEditingSubCategoryId] = useState(""); const [savingCategory, setSavingCategory] = useState(false); const [savingSubcategory, setSavingSubcategory] = useState(false); + const [deletingCategoryId, setDeletingCategoryId] = useState(""); + const [deletingSubCategoryId, setDeletingSubCategoryId] = useState(""); 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); + async function loadCategories(preferredCategoryId?: string) { + 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) => { + if (preferredCategoryId && rows.some((item) => item.id === preferredCategoryId)) { + return preferredCategoryId; + } + if (current && rows.some((item) => item.id === current)) { + return current; + } + return rows[0]?.id || ""; + }); + } catch (error) { + setCategoryError(error instanceof Error ? error.message : "Gagal memuat category"); + } finally { + setLoadingCategories(false); + } + } + + async function loadSubcategories(categoryId: string) { + if (!categoryId) { + setSubcategories([]); + return; } + setLoadingSubcategories(true); + setSubcategoryError(""); + try { + const res = await fetch(`/api/admin/categories/${categoryId}/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); + } + } + + useEffect(() => { 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); - } + if (!selectedCategoryId) { + setSubcategories([]); + return; } - - loadSubcategories(); + loadSubcategories(selectedCategoryId); }, [selectedCategoryId]); - function addAttribute() { - const normalized = attributeInput.trim(); - if (!normalized) return; - setSubCategoryAttributes((current) => - current.includes(normalized) ? current : [...current, normalized] - ); - setAttributeInput(""); + function resetCategoryEditor() { + setEditingCategoryId(""); + setCategoryForm(emptyCategoryForm); } - async function handleCreateCategory() { - if (!categoryName.trim()) return; + function resetSubCategoryEditor() { + setEditingSubCategoryId(""); + setSubCategoryForm(emptySubCategoryForm); + } + + function startEditCategory(category: CategoryRow) { + setEditingCategoryId(category.id); + setCategoryForm({ + name: category.name, + description: category.description || "", + }); + } + + function startEditSubCategory(subcategory: SubCategoryRow) { + setEditingSubCategoryId(subcategory.id); + setSubCategoryForm({ + name: subcategory.name, + description: subcategory.description || "", + attributes: [...subcategory.subCategoryAttributes], + attributeInput: "", + }); + } + + function addAttribute() { + const normalized = subCategoryForm.attributeInput.trim(); + if (!normalized) return; + setSubCategoryForm((current) => ({ + ...current, + attributes: current.attributes.includes(normalized) + ? current.attributes + : [...current.attributes, normalized], + attributeInput: "", + })); + } + + function removeAttribute(attribute: string) { + setSubCategoryForm((current) => ({ + ...current, + attributes: current.attributes.filter((item) => item !== attribute), + })); + } + + async function handleSaveCategory() { + if (!categoryForm.name.trim()) return; setSavingCategory(true); setCategoryError(""); try { - const res = await fetch("/api/admin/categories", { - method: "POST", + const isEditing = Boolean(editingCategoryId); + const url = isEditing + ? `/api/admin/categories/${editingCategoryId}` + : "/api/admin/categories"; + const method = isEditing ? "PUT" : "POST"; + + const res = await fetch(url, { + method, headers: { "Content-Type": "application/json", "x-auth-token": getToken(), }, body: JSON.stringify({ - name: categoryName.trim(), - description: categoryDescription.trim() || null, + name: categoryForm.name.trim(), + description: categoryForm.description.trim() || null, }), }); const data = await res.json(); if (!res.ok) { - throw new Error(data?.responseDesc || data?.error || "Gagal menambah category"); + throw new Error( + data?.responseDesc || + data?.error || + (isEditing ? "Gagal mengubah category" : "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(""); + await loadCategories(isEditing ? editingCategoryId : undefined); + resetCategoryEditor(); } catch (error) { - setCategoryError(error instanceof Error ? error.message : "Gagal menambah category"); + setCategoryError(error instanceof Error ? error.message : "Gagal menyimpan category"); } finally { setSavingCategory(false); } } - async function handleCreateSubcategory() { - if (!selectedCategoryId || !subCategoryName.trim()) return; + async function handleDeleteCategory(category: CategoryRow) { + const confirmed = window.confirm(`Hapus category "${category.name}"?`); + if (!confirmed) return; + + setDeletingCategoryId(category.id); + setCategoryError(""); + try { + const res = await fetch(`/api/admin/categories/${category.id}`, { + method: "DELETE", + headers: { "x-auth-token": getToken() }, + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + throw new Error(data?.responseDesc || data?.error || "Gagal menghapus category"); + } + + if (editingCategoryId === category.id) { + resetCategoryEditor(); + } + + await loadCategories(category.id === selectedCategoryId ? undefined : selectedCategoryId); + } catch (error) { + setCategoryError(error instanceof Error ? error.message : "Gagal menghapus category"); + } finally { + setDeletingCategoryId(""); + } + } + + async function handleSaveSubCategory() { + if (!selectedCategoryId || !subCategoryForm.name.trim()) return; setSavingSubcategory(true); setSubcategoryError(""); try { - const res = await fetch(`/api/admin/categories/${selectedCategoryId}/subcategories`, { - method: "POST", + const isEditing = Boolean(editingSubCategoryId); + const url = isEditing + ? `/api/admin/subcategories/${editingSubCategoryId}` + : `/api/admin/categories/${selectedCategoryId}/subcategories`; + const method = isEditing ? "PUT" : "POST"; + + const res = await fetch(url, { + method, headers: { "Content-Type": "application/json", "x-auth-token": getToken(), }, body: JSON.stringify({ - name: subCategoryName.trim(), - description: subCategoryDescription.trim() || null, - subCategoryAttributes, + name: subCategoryForm.name.trim(), + description: subCategoryForm.description.trim() || null, + subCategoryAttributes: subCategoryForm.attributes, }), }); const data = await res.json(); if (!res.ok) { - throw new Error(data?.responseDesc || data?.error || "Gagal menambah sub-category"); + throw new Error( + data?.responseDesc || + data?.error || + (isEditing ? "Gagal mengubah sub-category" : "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(""); + await loadSubcategories(selectedCategoryId); + resetSubCategoryEditor(); } catch (error) { - setSubcategoryError(error instanceof Error ? error.message : "Gagal menambah sub-category"); + setSubcategoryError(error instanceof Error ? error.message : "Gagal menyimpan sub-category"); } finally { setSavingSubcategory(false); } } + async function handleDeleteSubCategory(subcategory: SubCategoryRow) { + const confirmed = window.confirm(`Hapus sub-category "${subcategory.name}"?`); + if (!confirmed) return; + + setDeletingSubCategoryId(subcategory.id); + setSubcategoryError(""); + try { + const res = await fetch(`/api/admin/subcategories/${subcategory.id}`, { + method: "DELETE", + headers: { "x-auth-token": getToken() }, + }); + const data = await res.json().catch(() => ({})); + if (!res.ok) { + throw new Error(data?.responseDesc || data?.error || "Gagal menghapus sub-category"); + } + + if (editingSubCategoryId === subcategory.id) { + resetSubCategoryEditor(); + } + + await loadSubcategories(selectedCategoryId); + } catch (error) { + setSubcategoryError(error instanceof Error ? error.message : "Gagal menghapus sub-category"); + } finally { + setDeletingSubCategoryId(""); + } + } + return ( <>
@@ -252,7 +393,7 @@ export default function AdminCategoriesPage() { Category Management

- Kelola category dan sub-category produk untuk admin panel menggunakan endpoint category dari backend. + Kelola category dan sub-category produk langsung dari admin panel dengan data real dari backend.

@@ -283,35 +424,57 @@ export default function AdminCategoriesPage() {
+
+

+ {editingCategoryId ? "Edit Category" : "Add Category"} +

+ {editingCategoryId ? ( + + ) : null} +
setCategoryName(event.target.value)} + value={categoryForm.name} + onChange={(event) => setCategoryForm((current) => ({ ...current, name: 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" />