Add seller deleted/unpublish flow and admin product management
This commit is contained in:
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user