fix(crm): resolve product rejection persistence by querying by product pk, fixing use:enhance form unmount, and returning reactive product status
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 1m59s
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 1m59s
This commit is contained in:
parent
07cde931e6
commit
c3a887b192
5 changed files with 133 additions and 77 deletions
|
|
@ -295,21 +295,14 @@ class SellerProductApproveView(APIView):
|
|||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request, pk, product_id):
|
||||
seller = SupplierProfile.objects.filter(pk=pk).first()
|
||||
if not seller:
|
||||
return Response({"detail": "Seller not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
product = Product.objects.filter(
|
||||
models.Q(supplier_id=seller.id) | models.Q(supplier_id=seller.user_id),
|
||||
pk=product_id
|
||||
).first()
|
||||
product = Product.objects.filter(pk=product_id).first()
|
||||
if not product:
|
||||
return Response({"detail": "Product not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
product.status = "approved"
|
||||
product.is_active = True
|
||||
product.rejection_reason = None
|
||||
product.save()
|
||||
product.save(using="product_db")
|
||||
|
||||
return Response({
|
||||
"message": f"Product '{product.title}' has been approved and published to website.",
|
||||
|
|
@ -322,21 +315,14 @@ class SellerProductRejectView(APIView):
|
|||
|
||||
def post(self, request, pk, product_id):
|
||||
reason = request.data.get("reason", "Product does not comply with catalogue quality or listing policies.")
|
||||
seller = SupplierProfile.objects.filter(pk=pk).first()
|
||||
if not seller:
|
||||
return Response({"detail": "Seller not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
product = Product.objects.filter(
|
||||
models.Q(supplier_id=seller.id) | models.Q(supplier_id=seller.user_id),
|
||||
pk=product_id
|
||||
).first()
|
||||
product = Product.objects.filter(pk=product_id).first()
|
||||
if not product:
|
||||
return Response({"detail": "Product not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
product.status = "rejected"
|
||||
product.is_active = False
|
||||
product.rejection_reason = reason
|
||||
product.save()
|
||||
product.save(using="product_db")
|
||||
|
||||
return Response({
|
||||
"message": f"Product '{product.title}' was rejected.",
|
||||
|
|
@ -349,27 +335,13 @@ class SellerProductDetailView(APIView):
|
|||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, pk, product_id):
|
||||
seller = SupplierProfile.objects.filter(pk=pk).first()
|
||||
if not seller:
|
||||
return Response({"detail": "Seller not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
product = Product.objects.filter(
|
||||
models.Q(supplier_id=seller.id) | models.Q(supplier_id=seller.user_id),
|
||||
pk=product_id
|
||||
).first()
|
||||
product = Product.objects.filter(pk=product_id).first()
|
||||
if not product:
|
||||
return Response({"detail": "Product not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
return Response(ProductSerializer(product).data)
|
||||
|
||||
def patch(self, request, pk, product_id):
|
||||
seller = SupplierProfile.objects.filter(pk=pk).first()
|
||||
if not seller:
|
||||
return Response({"detail": "Seller not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
product = Product.objects.filter(
|
||||
models.Q(supplier_id=seller.id) | models.Q(supplier_id=seller.user_id),
|
||||
pk=product_id
|
||||
).first()
|
||||
product = Product.objects.filter(pk=product_id).first()
|
||||
if not product:
|
||||
return Response({"detail": "Product not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
|
@ -397,13 +369,14 @@ class SellerProductDetailView(APIView):
|
|||
"image",
|
||||
"shipping_profile",
|
||||
"package_weight",
|
||||
"processing_days",
|
||||
]
|
||||
|
||||
for field in updatable_fields:
|
||||
if field in request.data:
|
||||
setattr(product, field, request.data[field])
|
||||
|
||||
product.save()
|
||||
product.save(using="product_db")
|
||||
return Response({
|
||||
"message": f"Product '{product.title}' content updated successfully.",
|
||||
"product": ProductSerializer(product).data
|
||||
|
|
@ -414,23 +387,17 @@ class SellerProductToggleView(APIView):
|
|||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request, pk, product_id):
|
||||
seller = SupplierProfile.objects.filter(pk=pk).first()
|
||||
if not seller:
|
||||
return Response({"detail": "Seller not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
product = Product.objects.filter(
|
||||
models.Q(supplier_id=seller.id) | models.Q(supplier_id=seller.user_id),
|
||||
pk=product_id
|
||||
).first()
|
||||
product = Product.objects.filter(pk=product_id).first()
|
||||
if not product:
|
||||
return Response({"detail": "Product not found."}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if "is_active" in request.data:
|
||||
product.is_active = bool(request.data["is_active"])
|
||||
val = request.data["is_active"]
|
||||
product.is_active = val in [True, "true", "True", 1, "1"]
|
||||
if "status" in request.data:
|
||||
product.status = request.data["status"]
|
||||
|
||||
product.save()
|
||||
product.save(using="product_db")
|
||||
return Response({
|
||||
"message": f"Product '{product.title}' updated.",
|
||||
"product": ProductSerializer(product).data
|
||||
|
|
|
|||
|
|
@ -131,12 +131,12 @@ export const actions = {
|
|||
const data = await request.formData();
|
||||
const product_id = data.get('product_id');
|
||||
try {
|
||||
await apiRequest(
|
||||
const res = await apiRequest(
|
||||
`/sellers/${params.id}/products/${product_id}/approve/`,
|
||||
{ method: 'POST' },
|
||||
{ cookies }
|
||||
);
|
||||
return { success: true, message: 'Product approved and published to website' };
|
||||
return { success: true, message: 'Product approved and published to website', product: res.product };
|
||||
} catch (err) {
|
||||
return fail(400, { error: err.message || 'Failed to approve product' });
|
||||
}
|
||||
|
|
@ -147,12 +147,12 @@ export const actions = {
|
|||
const product_id = data.get('product_id');
|
||||
const reason = data.get('reason') || 'Product does not comply with catalogue quality standards.';
|
||||
try {
|
||||
await apiRequest(
|
||||
const res = await apiRequest(
|
||||
`/sellers/${params.id}/products/${product_id}/reject/`,
|
||||
{ method: 'POST', body: { reason: String(reason) } },
|
||||
{ cookies }
|
||||
);
|
||||
return { success: true, message: 'Product rejected' };
|
||||
return { success: true, message: 'Product rejected', product: res.product };
|
||||
} catch (err) {
|
||||
return fail(400, { error: err.message || 'Failed to reject product' });
|
||||
}
|
||||
|
|
@ -175,12 +175,12 @@ export const actions = {
|
|||
}
|
||||
}
|
||||
try {
|
||||
await apiRequest(
|
||||
const res = await apiRequest(
|
||||
`/sellers/${params.id}/products/${product_id}/`,
|
||||
{ method: 'PATCH', body: payload },
|
||||
{ cookies }
|
||||
);
|
||||
return { success: true, message: 'Product content updated successfully in website catalogue' };
|
||||
return { success: true, message: 'Product content updated successfully in website catalogue', product: res.product };
|
||||
} catch (err) {
|
||||
return fail(400, { error: err.message || 'Failed to update product content' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,22 @@
|
|||
let activeTab = $state('overview');
|
||||
let productFilter = $state('all'); // 'all', 'individual', 'bulk', 'pending', 'approved', 'rejected', 'batches'
|
||||
let failedProductImgs = $state(new Set());
|
||||
let localProducts = $state(/** @type {any[]} */ ([]));
|
||||
|
||||
$effect(() => {
|
||||
if (data.products) {
|
||||
localProducts = [...data.products];
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (form?.product) {
|
||||
const idx = localProducts.findIndex((p) => p.id === form.product.id);
|
||||
if (idx !== -1) {
|
||||
localProducts[idx] = { ...localProducts[idx], ...form.product };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Modals state
|
||||
let showRejectModal = $state(false);
|
||||
|
|
@ -154,14 +170,15 @@
|
|||
}
|
||||
|
||||
// Filtered products derived state
|
||||
let filteredProducts = $derived(() => {
|
||||
if (productFilter === 'all') return products;
|
||||
if (productFilter === 'individual') return products.filter((p) => p.upload_source !== 'bulk');
|
||||
if (productFilter === 'bulk') return products.filter((p) => p.upload_source === 'bulk');
|
||||
if (productFilter === 'pending') return products.filter((p) => p.status === 'pending');
|
||||
if (productFilter === 'approved') return products.filter((p) => p.status === 'approved');
|
||||
if (productFilter === 'rejected') return products.filter((p) => p.status === 'rejected');
|
||||
return products;
|
||||
let filteredProducts = $derived.by(() => {
|
||||
const list = localProducts.length > 0 ? localProducts : products;
|
||||
if (productFilter === 'all') return list;
|
||||
if (productFilter === 'individual') return list.filter((p) => p.upload_source !== 'bulk');
|
||||
if (productFilter === 'bulk') return list.filter((p) => p.upload_source === 'bulk');
|
||||
if (productFilter === 'pending') return list.filter((p) => p.status === 'pending');
|
||||
if (productFilter === 'approved') return list.filter((p) => p.status === 'approved');
|
||||
if (productFilter === 'rejected') return list.filter((p) => p.status === 'rejected');
|
||||
return list;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -637,7 +654,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each filteredProducts() as p (p.id)}
|
||||
{#each filteredProducts as p (p.id)}
|
||||
<tr>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
|
|
@ -930,7 +947,16 @@
|
|||
Directly modify product metadata published on the consumer marketplace portal.
|
||||
</p>
|
||||
|
||||
<form method="POST" action="?/updateProduct" use:enhance onsubmit={() => (showEditProductModal = false)}>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/updateProduct"
|
||||
use:enhance={() => {
|
||||
return async ({ update }) => {
|
||||
showEditProductModal = false;
|
||||
await update();
|
||||
};
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="product_id" value={editingProduct.id} />
|
||||
|
||||
<div style="display:flex;flex-direction:column;gap:14px;">
|
||||
|
|
@ -1028,7 +1054,16 @@
|
|||
<div style="padding:10px 14px;background:#F9FAFB;border:1px solid #E5E7EB;border-radius:6px;margin-bottom:16px;font-size:13px;color:#374151;">
|
||||
Rejecting: <strong style="color:#111827;">{rejectingProduct.title}</strong>
|
||||
</div>
|
||||
<form method="POST" action="?/rejectProduct" use:enhance onsubmit={() => (showProductRejectModal = false)}>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/rejectProduct"
|
||||
use:enhance={() => {
|
||||
return async ({ update }) => {
|
||||
showProductRejectModal = false;
|
||||
await update();
|
||||
};
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="product_id" value={rejectingProduct.id} />
|
||||
<label for="product_reject_reason" style="display:block;font-size:13px;font-weight:600;color:#111827;margin-bottom:6px;">
|
||||
Reason for Rejection <span style="color:#DC2626;">*</span>
|
||||
|
|
@ -1059,7 +1094,16 @@
|
|||
<p style="font-size:13px;color:#4B5563;margin-bottom:16px;">
|
||||
Locking disables seller portal login and halts orders without deleting any catalog or financial records.
|
||||
</p>
|
||||
<form method="POST" action="?/lock" use:enhance onsubmit={() => (showLockModal = false)}>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/lock"
|
||||
use:enhance={() => {
|
||||
return async ({ update }) => {
|
||||
showLockModal = false;
|
||||
await update();
|
||||
};
|
||||
}}
|
||||
>
|
||||
<label for="lock_reason_input" style="display:block;font-size:13px;font-weight:600;color:#111827;margin-bottom:6px;">
|
||||
Reason for Locking <span style="color:#DC2626;">*</span>
|
||||
</label>
|
||||
|
|
@ -1089,7 +1133,16 @@
|
|||
<p style="font-size:13px;color:#4B5563;margin-bottom:16px;">
|
||||
State the reason for suspension. Zero data will be deleted from the database.
|
||||
</p>
|
||||
<form method="POST" action="?/suspend" use:enhance onsubmit={() => (showSuspendModal = false)}>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/suspend"
|
||||
use:enhance={() => {
|
||||
return async ({ update }) => {
|
||||
showSuspendModal = false;
|
||||
await update();
|
||||
};
|
||||
}}
|
||||
>
|
||||
<label for="suspend_reason_input" style="display:block;font-size:13px;font-weight:600;color:#111827;margin-bottom:6px;">
|
||||
Reason for Suspension <span style="color:#DC2626;">*</span>
|
||||
</label>
|
||||
|
|
@ -1119,7 +1172,16 @@
|
|||
<div style="padding:10px 14px;background:#F0FDF4;border:1px solid #86EFAC;border-radius:6px;font-size:12px;color:#166534;margin-bottom:14px;">
|
||||
✓ <strong>Zero Data Loss Guarantee:</strong> Closing marks the profile as closed and disables sign-in, while preserving 100% of orders, catalog listings, and wallet settlements for statutory audits.
|
||||
</div>
|
||||
<form method="POST" action="?/close" use:enhance onsubmit={() => (showCloseModal = false)}>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/close"
|
||||
use:enhance={() => {
|
||||
return async ({ update }) => {
|
||||
showCloseModal = false;
|
||||
await update();
|
||||
};
|
||||
}}
|
||||
>
|
||||
<label for="close_reason_input" style="display:block;font-size:13px;font-weight:600;color:#111827;margin-bottom:6px;">
|
||||
Closure Reason <span style="color:#DC2626;">*</span>
|
||||
</label>
|
||||
|
|
@ -1149,7 +1211,16 @@
|
|||
<p style="font-size:13px;color:#4B5563;margin-bottom:16px;">
|
||||
Provide the reason for rejecting this seller application.
|
||||
</p>
|
||||
<form method="POST" action="?/reject" use:enhance onsubmit={() => (showRejectModal = false)}>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/reject"
|
||||
use:enhance={() => {
|
||||
return async ({ update }) => {
|
||||
showRejectModal = false;
|
||||
await update();
|
||||
};
|
||||
}}
|
||||
>
|
||||
<label for="seller_reject_reason_input" style="display:block;font-size:13px;font-weight:600;color:#111827;margin-bottom:6px;">
|
||||
Reason for Rejection <span style="color:#DC2626;">*</span>
|
||||
</label>
|
||||
|
|
@ -1179,7 +1250,16 @@
|
|||
<p style="font-size:13px;color:#4B5563;margin-bottom:16px;">
|
||||
Enter a new secure password for <strong>@{seller.username}</strong> ({seller.email}).
|
||||
</p>
|
||||
<form method="POST" action="?/resetPassword" use:enhance onsubmit={() => (showPasswordModal = false)}>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/resetPassword"
|
||||
use:enhance={() => {
|
||||
return async ({ update }) => {
|
||||
showPasswordModal = false;
|
||||
await update();
|
||||
};
|
||||
}}
|
||||
>
|
||||
<label for="new_password_input" style="display:block;font-size:13px;font-weight:600;color:#111827;margin-bottom:6px;">
|
||||
New Password <span style="color:#DC2626;">*</span>
|
||||
</label>
|
||||
|
|
|
|||
|
|
@ -26,12 +26,12 @@ export async function load({ cookies, params }) {
|
|||
export const actions = {
|
||||
approve: async ({ cookies, params }) => {
|
||||
try {
|
||||
await apiRequest(
|
||||
const res = await apiRequest(
|
||||
`/sellers/${params.id}/products/${params.productId}/approve/`,
|
||||
{ method: 'POST' },
|
||||
{ cookies }
|
||||
);
|
||||
return { success: true, message: 'Product approved and published to customer storefront' };
|
||||
return { success: true, message: 'Product approved and published to customer storefront', product: res.product };
|
||||
} catch (err) {
|
||||
return fail(400, { error: err.message || 'Failed to approve product' });
|
||||
}
|
||||
|
|
@ -41,12 +41,12 @@ export const actions = {
|
|||
const data = await request.formData();
|
||||
const reason = data.get('reason') || 'Product does not comply with catalogue quality standards.';
|
||||
try {
|
||||
await apiRequest(
|
||||
const res = await apiRequest(
|
||||
`/sellers/${params.id}/products/${params.productId}/reject/`,
|
||||
{ method: 'POST', body: { reason: String(reason) } },
|
||||
{ cookies }
|
||||
);
|
||||
return { success: true, message: 'Product rejected' };
|
||||
return { success: true, message: 'Product rejected', product: res.product };
|
||||
} catch (err) {
|
||||
return fail(400, { error: err.message || 'Failed to reject product' });
|
||||
}
|
||||
|
|
@ -56,12 +56,12 @@ export const actions = {
|
|||
const data = await request.formData();
|
||||
const is_active = data.get('is_active') === 'true';
|
||||
try {
|
||||
await apiRequest(
|
||||
const res = await apiRequest(
|
||||
`/sellers/${params.id}/products/${params.productId}/toggle/`,
|
||||
{ method: 'POST', body: { is_active } },
|
||||
{ cookies }
|
||||
);
|
||||
return { success: true, message: 'Product visibility updated' };
|
||||
return { success: true, message: 'Product visibility updated', product: res.product };
|
||||
} catch (err) {
|
||||
return fail(400, { error: err.message || 'Failed to toggle product status' });
|
||||
}
|
||||
|
|
@ -89,12 +89,12 @@ export const actions = {
|
|||
}
|
||||
}
|
||||
try {
|
||||
await apiRequest(
|
||||
const res = await apiRequest(
|
||||
`/sellers/${params.id}/products/${params.productId}/`,
|
||||
{ method: 'PATCH', body: payload },
|
||||
{ cookies }
|
||||
);
|
||||
return { success: true, message: 'Product content updated successfully in website catalogue' };
|
||||
return { success: true, message: 'Product content updated successfully in website catalogue', product: res.product };
|
||||
} catch (err) {
|
||||
return fail(400, { error: err.message || 'Failed to update product content' });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
let { data, form } = $props();
|
||||
|
||||
let seller = $derived(data.seller || {});
|
||||
let product = $derived(data.product || {});
|
||||
let product = $derived(form?.product || data.product || {});
|
||||
|
||||
const CDN_DOMAIN = 'https://dc23efa04hj6d.cloudfront.net';
|
||||
|
||||
|
|
@ -727,7 +727,16 @@
|
|||
Rejecting: <strong style="color:#111827;">{product.title}</strong>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="?/reject" use:enhance onsubmit={() => (showRejectModal = false)}>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/reject"
|
||||
use:enhance={() => {
|
||||
return async ({ update }) => {
|
||||
showRejectModal = false;
|
||||
await update();
|
||||
};
|
||||
}}
|
||||
>
|
||||
<label for="reject_reason_input" style="display:block;font-size:13px;font-weight:600;color:#111827;margin-bottom:6px;">
|
||||
Reason for Rejection <span style="color:#DC2626;">*</span>
|
||||
</label>
|
||||
|
|
|
|||
Loading…
Reference in a new issue