Add seller deleted/unpublish flow and admin product management

This commit is contained in:
2026-05-09 07:56:51 +07:00
parent 37466d42e1
commit cb2a2a9678
13 changed files with 1369 additions and 86 deletions

View File

@ -13,7 +13,8 @@ type TabLabel =
| "International Market"
| "Local Market"
| "Out Of Stock"
| "Rejected";
| "Rejected"
| "Deleted";
interface ProductRow {
id: string;
@ -71,31 +72,49 @@ function tabFromQuery(tab: string | null): TabLabel {
return "Out Of Stock";
case "rejected":
return "Rejected";
case "deleted":
return "Deleted";
default:
return "All Product";
}
}
function DeleteConfirmModal({
function ConfirmActionModal({
product,
onConfirm,
onCancel,
deleting,
processing,
d,
icon,
iconToneClass,
confirmToneClass,
}: {
product: ProductRow;
onConfirm: () => void;
onCancel: () => void;
deleting: boolean;
d: { title: string; message: string; productLabel: string; cancel: string; confirm: string; deleting: string };
processing: boolean;
d: {
title: string;
message: string;
productLabel: string;
cancel: string;
confirm: string;
deleting?: string;
processing?: string;
};
icon: string;
iconToneClass: string;
confirmToneClass: string;
}) {
const processingLabel = d.processing || d.deleting || "Processing...";
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={onCancel} />
<div className="relative w-full max-w-md rounded-2xl bg-surface-container-lowest border border-outline-variant/10 shadow-2xl p-8 space-y-6">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-xl bg-error-container flex items-center justify-center flex-shrink-0">
<span className="material-symbols-outlined text-error text-2xl">delete_forever</span>
<div className={`w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0 ${iconToneClass}`}>
<span className="material-symbols-outlined text-2xl">{icon}</span>
</div>
<div>
<h2 className="text-lg font-black text-on-surface font-headline">{d.title}</h2>
@ -113,7 +132,7 @@ function DeleteConfirmModal({
<button
type="button"
onClick={onCancel}
disabled={deleting}
disabled={processing}
className="flex-1 px-4 py-3 rounded-xl border border-outline-variant/30 text-on-surface font-black text-sm hover:bg-surface-container-low transition-colors disabled:opacity-50"
>
{d.cancel}
@ -121,17 +140,17 @@ function DeleteConfirmModal({
<button
type="button"
onClick={onConfirm}
disabled={deleting}
className="flex-1 px-4 py-3 rounded-xl bg-error text-white font-black text-sm hover:bg-error/90 transition-colors disabled:opacity-60 flex items-center justify-center gap-2"
disabled={processing}
className={`flex-1 px-4 py-3 rounded-xl text-white font-black text-sm transition-colors disabled:opacity-60 flex items-center justify-center gap-2 ${confirmToneClass}`}
>
{deleting ? (
{processing ? (
<>
<span className="material-symbols-outlined text-base animate-spin">progress_activity</span>
{d.deleting}
{processingLabel}
</>
) : (
<>
<span className="material-symbols-outlined text-base">delete</span>
<span className="material-symbols-outlined text-base">{icon}</span>
{d.confirm}
</>
)}
@ -149,6 +168,11 @@ function ProductsPageInner() {
const tab = searchParams.get("tab");
const activeTab = tabFromQuery(tab);
const isInReviewTab = activeTab === "In Review";
const isDeletedTab = activeTab === "Deleted";
const canUnpublish =
activeTab === "All Product" ||
activeTab === "International Market" ||
activeTab === "Local Market";
const [rows, setRows] = useState<ProductRow[]>([]);
const [loading, setLoading] = useState(true);
@ -157,7 +181,9 @@ function ProductsPageInner() {
const [totalPage, setTotalPage] = useState(0);
const [page, setPage] = useState(1);
const [deleteTarget, setDeleteTarget] = useState<ProductRow | null>(null);
const [unpublishTarget, setUnpublishTarget] = useState<ProductRow | null>(null);
const [deleting, setDeleting] = useState(false);
const [unpublishing, setUnpublishing] = useState(false);
// Reset to page 1 when tab changes
useEffect(() => {
@ -213,6 +239,47 @@ function ProductsPageInner() {
}
}
async function handleUnpublish() {
if (!unpublishTarget) return;
setUnpublishing(true);
try {
const res = await fetch(`/api/products/${unpublishTarget.id}?action=unpublish`, {
method: "PUT",
headers: { "x-auth-token": getToken() },
});
const result = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(result?.responseDesc || p.unpublishDialog.errorGeneric);
}
setUnpublishTarget(null);
window.location.reload();
} catch (err) {
alert(err instanceof Error ? err.message : p.unpublishDialog.errorGeneric);
} finally {
setUnpublishing(false);
}
}
async function handleRestore(productId: string) {
try {
const res = await fetch(`/api/products/${productId}?action=restore`, {
method: "PUT",
headers: { "x-auth-token": getToken() },
});
const result = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(result?.responseDesc || p.restoreError);
}
window.location.reload();
} catch (err) {
alert(err instanceof Error ? err.message : p.restoreError);
}
}
const internationalCount = rows.filter(
(row) => row.market === "International"
).length;
@ -444,12 +511,14 @@ function ProductsPageInner() {
<td className="px-6 py-4 text-center">
<div className="flex items-center justify-center gap-3">
{!isInReviewTab ? (
<Link
href={`/products/${product.id}/edit${activeTab === "Draft" ? "?draft=1" : ""}`}
className="rounded-lg bg-primary px-3 py-1 text-[10px] font-bold text-white transition-colors hover:bg-primary/90"
>
{p.edit}
</Link>
!isDeletedTab ? (
<Link
href={`/products/${product.id}/edit${activeTab === "Draft" ? "?draft=1" : ""}`}
className="rounded-lg bg-primary px-3 py-1 text-[10px] font-bold text-white transition-colors hover:bg-primary/90"
>
{p.edit}
</Link>
) : null
) : null}
<Link
href={`/products/${product.id}/detail${activeTab === "Draft" ? "?draft=1" : isInReviewTab ? "?review=1" : ""}`}
@ -457,14 +526,35 @@ function ProductsPageInner() {
>
{p.detail}
</Link>
<button
type="button"
onClick={() => setDeleteTarget(product)}
className="w-7 h-7 flex items-center justify-center rounded-lg text-error hover:bg-error-container transition-colors"
title="Hapus"
>
<span className="material-symbols-outlined text-base">delete</span>
</button>
{isDeletedTab ? (
<button
type="button"
onClick={() => handleRestore(product.id)}
className="rounded-lg bg-tertiary px-3 py-1 text-[10px] font-bold text-white transition-colors hover:bg-tertiary/90"
>
{p.restore}
</button>
) : (
<>
{canUnpublish ? (
<button
type="button"
onClick={() => setUnpublishTarget(product)}
className="rounded-lg bg-secondary px-3 py-1 text-[10px] font-bold text-white transition-colors hover:bg-secondary/90"
>
{p.unpublish}
</button>
) : null}
<button
type="button"
onClick={() => setDeleteTarget(product)}
className="w-7 h-7 flex items-center justify-center rounded-lg text-error hover:bg-error-container transition-colors"
title="Hapus"
>
<span className="material-symbols-outlined text-base">delete</span>
</button>
</>
)}
</div>
</td>
</tr>
@ -505,12 +595,28 @@ function ProductsPageInner() {
</section>
{deleteTarget && (
<DeleteConfirmModal
<ConfirmActionModal
product={deleteTarget}
onConfirm={handleDelete}
onCancel={() => setDeleteTarget(null)}
deleting={deleting}
processing={deleting}
d={p.deleteDialog}
icon="delete"
iconToneClass="bg-error-container text-error"
confirmToneClass="bg-error hover:bg-error/90"
/>
)}
{unpublishTarget && (
<ConfirmActionModal
product={unpublishTarget}
onConfirm={handleUnpublish}
onCancel={() => setUnpublishTarget(null)}
processing={unpublishing}
d={p.unpublishDialog}
icon="visibility_off"
iconToneClass="bg-secondary-container text-on-secondary-container"
confirmToneClass="bg-secondary hover:bg-secondary/90"
/>
)}
</div>