feat: add modal popup for product details and enhance status management in ProductList
All checks were successful
Beta CI/CD Pipeline / build-and-test (push) Successful in 14s
Beta CI/CD Pipeline / deploy-beta (push) Successful in 6s

This commit is contained in:
vickytechkey 2026-08-16 15:12:42 +05:30
parent 2d13768234
commit 107a2a1e82
4 changed files with 370 additions and 32 deletions

View file

@ -1,6 +1,38 @@
import React from 'react';
import React, { useState } from 'react';
import StatusBadge from './StatusBadge';
import { API_BASE_URL } from '../config';
import { useAuth } from '../context/AuthContext';
const DocumentViewer = ({ documents, sellerId, refreshDocs }) => {
const { token } = useAuth();
const [previewDoc, setPreviewDoc] = useState(null);
const handleApprove = async (docId) => {
try {
const res = await fetch(`${API_BASE_URL}/sellers/${sellerId}/documents/${docId}/approve/`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!res.ok) throw new Error('Failed to approve document');
alert('Document approved successfully');
if (refreshDocs) refreshDocs();
} catch (err) {
alert(err.message);
}
};
const handleDownload = (doc) => {
const link = document.createElement('a');
link.href = doc.url;
link.download = doc.filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const DocumentViewer = ({ documents, sellerId }) => {
return (
<div className="glass-card" style={{ marginTop: '24px' }}>
<h3 className="section-title">KYC Document Verification</h3>
@ -11,29 +43,60 @@ const DocumentViewer = ({ documents, sellerId }) => {
background: 'rgba(255, 255, 255, 0.02)',
border: '1px solid var(--glass-border)',
borderRadius: 'var(--border-radius)',
padding: '16px'
padding: '16px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
<span style={{ fontWeight: '600' }}>{doc.type}</span>
<span style={{ fontSize: '12px', color: 'var(--text-secondary)' }}>Status: Verified</span>
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<span style={{ fontWeight: '600' }}>{doc.type}</span>
<StatusBadge status={doc.status} />
</div>
<div style={{
height: '180px',
background: 'var(--bg-primary)',
borderRadius: '8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--text-secondary)',
border: '1px dashed var(--glass-border)',
marginBottom: '12px',
fontSize: '13px',
padding: '8px',
textAlign: 'center'
}}>
{doc.filename.match(/\.(jpg|jpeg|png)$/i) ? (
<img src={doc.url} alt={doc.type} style={{ maxWidth: '100%', maxHeight: '100%', borderRadius: '4px', objectFit: 'contain' }} />
) : (
<span>[Document Preview: {doc.filename}]</span>
)}
</div>
</div>
<div style={{
height: '180px',
background: 'var(--bg-primary)',
borderRadius: '8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--text-secondary)',
border: '1px dashed var(--glass-border)',
marginBottom: '12px'
}}>
[Presigned Document Preview: {doc.filename}]
</div>
<div style={{ display: 'flex', gap: '10px' }}>
<a href={doc.url} target="_blank" rel="noreferrer" className="btn btn-secondary" style={{ flex: 1, fontSize: '12px', padding: '8px' }}>
Open Original
</a>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
<button
className="btn btn-primary"
style={{ flex: 1, fontSize: '11px', padding: '6px 10px' }}
onClick={() => handleApprove(doc.id)}
disabled={doc.status?.toLowerCase() === 'verified' || doc.status?.toLowerCase() === 'approved'}
>
{doc.status?.toLowerCase() === 'verified' || doc.status?.toLowerCase() === 'approved' ? 'Approved' : 'Approve'}
</button>
<button
className="btn btn-secondary"
style={{ flex: 1, fontSize: '11px', padding: '6px 10px', background: 'rgba(255,255,255,0.05)', color: '#fff', border: '1px solid var(--glass-border)' }}
onClick={() => setPreviewDoc(doc)}
>
Preview
</button>
<button
className="btn btn-secondary"
style={{ flex: 1, fontSize: '11px', padding: '6px 10px', background: 'rgba(255,255,255,0.05)', color: '#fff', border: '1px solid var(--glass-border)' }}
onClick={() => handleDownload(doc)}
>
Download
</button>
</div>
</div>
))}
@ -41,6 +104,41 @@ const DocumentViewer = ({ documents, sellerId }) => {
) : (
<p style={{ color: 'var(--text-secondary)' }}>No KYC documents submitted yet.</p>
)}
{/* Lightbox / Preview Modal Overlay */}
{previewDoc && (
<div className="modal-overlay" onClick={() => setPreviewDoc(null)}>
<div className="modal-content" style={{ maxWidth: '800px', width: '90%' }} onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2 style={{ fontSize: '20px', fontWeight: '600' }}>Document Preview: {previewDoc.type}</h2>
<button className="modal-close" onClick={() => setPreviewDoc(null)}>&times;</button>
</div>
<div className="modal-body" style={{ display: 'flex', justifyContent: 'center', background: '#000', borderRadius: '8px', padding: '16px', overflow: 'hidden' }}>
{previewDoc.filename.match(/\.(jpg|jpeg|png)$/i) ? (
<img
src={previewDoc.url}
alt={previewDoc.type}
style={{ maxWidth: '100%', maxHeight: '60vh', objectFit: 'contain' }}
/>
) : previewDoc.filename.match(/\.pdf$/i) ? (
<iframe
src={previewDoc.url}
title={previewDoc.type}
style={{ width: '100%', height: '60vh', border: 'none', background: '#fff' }}
/>
) : (
<div style={{ padding: '40px', color: '#fff', textAlign: 'center' }}>
<p style={{ marginBottom: '16px' }}>Preview is not supported directly for this file type ({previewDoc.filename}).</p>
<button className="btn btn-primary" onClick={() => handleDownload(previewDoc)}>Download to View</button>
</div>
)}
</div>
<div className="modal-footer" style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '16px' }}>
<button className="btn btn-secondary" onClick={() => setPreviewDoc(null)}>Close Preview</button>
</div>
</div>
</div>
)}
</div>
);
};

View file

@ -323,3 +323,100 @@ a:hover {
color: var(--text-secondary);
font-size: 14px;
}
/* Modal Popup Styling */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.2s ease-out;
}
.modal-content {
background: var(--bg-secondary);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius);
box-shadow: var(--glass-shadow);
width: 90%;
max-width: 600px;
max-height: 85vh;
overflow-y: auto;
padding: 24px;
animation: slideUp 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--glass-border);
padding-bottom: 16px;
margin-bottom: 16px;
}
.modal-close {
background: transparent;
border: none;
color: var(--text-secondary);
font-size: 24px;
cursor: pointer;
transition: var(--transition);
}
.modal-close:hover {
color: #fff;
}
.modal-section {
margin-bottom: 20px;
}
.modal-section-title {
font-size: 16px;
font-weight: 600;
color: var(--accent-hover);
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.details-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.details-item {
display: flex;
flex-direction: column;
}
.details-label {
font-size: 11px;
color: var(--text-muted);
text-transform: uppercase;
}
.details-value {
font-size: 14px;
color: var(--text-primary);
font-weight: 500;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}

View file

@ -9,6 +9,7 @@ const ProductList = () => {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [selectedProduct, setSelectedProduct] = useState(null);
const fetchProducts = async () => {
try {
@ -109,29 +110,42 @@ const ProductList = () => {
{filteredProducts.map(prod => (
<tr key={prod.id}>
<td>#{prod.id}</td>
<td style={{ fontWeight: '600' }}>{prod.name}</td>
<td
style={{ fontWeight: '600', cursor: 'pointer', color: 'var(--accent-hover)' }}
onClick={() => setSelectedProduct(prod)}
>
{prod.name}
</td>
<td>{prod.supplier_name || 'N/A'}</td>
<td>{parseFloat(prod.price).toLocaleString('en-IN')}</td>
<td>{prod.stock} items</td>
<td><StatusBadge status={prod.status} /></td>
<td>
<div style={{ display: 'flex', gap: '6px' }}>
{prod.status === 'pending_approval' && (
{(prod.status === 'pending' || prod.status === 'pending_approval') && (
<>
<button className="btn btn-primary" style={{ fontSize: '11px', padding: '4px 8px' }} onClick={() => handleStatusChange(prod.id, 'approve')}>
Approve
</button>
<button className="btn btn-danger" style={{ fontSize: '11px', padding: '4px 8px' }} onClick={() => handleStatusChange(prod.id, 'hold')}>
Reject/Hold
<button className="btn btn-secondary" style={{ fontSize: '11px', padding: '4px 8px', background: 'rgba(255,255,255,0.05)', color: '#fff', border: '1px solid var(--glass-border)' }} onClick={() => handleStatusChange(prod.id, 'hold')}>
Hold
</button>
<button className="btn btn-danger" style={{ fontSize: '11px', padding: '4px 8px', background: 'var(--color-danger)' }} onClick={() => handleStatusChange(prod.id, 'reject')}>
Reject
</button>
</>
)}
{prod.status === 'approved' && (
<button className="btn btn-secondary" style={{ fontSize: '11px', padding: '4px 8px' }} onClick={() => handleStatusChange(prod.id, 'hold')}>
Hold Item
</button>
<>
<button className="btn btn-secondary" style={{ fontSize: '11px', padding: '4px 8px' }} onClick={() => handleStatusChange(prod.id, 'hold')}>
Hold Item
</button>
<button className="btn btn-danger" style={{ fontSize: '11px', padding: '4px 8px', background: 'var(--color-danger)' }} onClick={() => handleStatusChange(prod.id, 'reject')}>
Reject
</button>
</>
)}
{prod.status === 'on_hold' && (
{(prod.status === 'hold' || prod.status === 'on_hold' || prod.status === 'rejected') && (
<button className="btn btn-primary" style={{ fontSize: '11px', padding: '4px 8px' }} onClick={() => handleStatusChange(prod.id, 'approve')}>
Activate
</button>
@ -148,6 +162,135 @@ const ProductList = () => {
</div>
)}
</div>
{selectedProduct && (
<div className="modal-overlay" onClick={() => setSelectedProduct(null)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2 style={{ fontSize: '20px', fontWeight: '600' }}>Product & Seller Details</h2>
<button className="modal-close" onClick={() => setSelectedProduct(null)}>&times;</button>
</div>
<div className="modal-body">
<div className="modal-section">
<h3 className="modal-section-title">Product Information</h3>
<div className="details-grid">
<div className="details-item">
<span className="details-label">ID</span>
<span className="details-value">#{selectedProduct.id}</span>
</div>
<div className="details-item">
<span className="details-label">Title</span>
<span className="details-value">{selectedProduct.name}</span>
</div>
<div className="details-item">
<span className="details-label">Category</span>
<span className="details-value">{selectedProduct.category || 'N/A'}</span>
</div>
<div className="details-item">
<span className="details-label">SKU</span>
<span className="details-value">{selectedProduct.sku || 'N/A'}</span>
</div>
<div className="details-item">
<span className="details-label">Price</span>
<span className="details-value">{parseFloat(selectedProduct.price).toLocaleString('en-IN')}</span>
</div>
<div className="details-item">
<span className="details-label">Stock</span>
<span className="details-value">{selectedProduct.stock} items</span>
</div>
<div className="details-item">
<span className="details-label">Status</span>
<span className="details-value"><StatusBadge status={selectedProduct.status} /></span>
</div>
<div className="details-item">
<span className="details-label">Active Listing</span>
<span className="details-value">{selectedProduct.is_active ? 'Yes' : 'No'}</span>
</div>
</div>
{selectedProduct.image && (
<div style={{ marginTop: '12px' }}>
<span className="details-label" style={{ display: 'block', marginBottom: '4px' }}>Product Image</span>
{selectedProduct.image.startsWith('data:image') || selectedProduct.image.startsWith('http') ? (
<img src={selectedProduct.image} alt={selectedProduct.name} style={{ maxWidth: '100%', maxHeight: '150px', borderRadius: '6px', objectFit: 'contain' }} />
) : (
<span className="details-value" style={{ wordBreak: 'break-all', fontSize: '12px', color: 'var(--text-secondary)' }}>{selectedProduct.image}</span>
)}
</div>
)}
</div>
<div className="modal-section" style={{ borderTop: '1px solid var(--glass-border)', paddingTop: '16px' }}>
<h3 className="modal-section-title">Seller / Supplier Information</h3>
<div className="details-grid">
<div className="details-item">
<span className="details-label">Store Name</span>
<span className="details-value">{selectedProduct.supplier_name || 'N/A'}</span>
</div>
<div className="details-item">
<span className="details-label">Account Status</span>
<span className="details-value"><StatusBadge status={selectedProduct.supplier_status} /></span>
</div>
<div className="details-item">
<span className="details-label">Email</span>
<span className="details-value">{selectedProduct.supplier_email || 'N/A'}</span>
</div>
<div className="details-item">
<span className="details-label">Phone</span>
<span className="details-value">{selectedProduct.supplier_phone || 'N/A'}</span>
</div>
<div className="details-item" style={{ gridColumn: 'span 2' }}>
<span className="details-label">GSTIN</span>
<span className="details-value">{selectedProduct.supplier_gstin || 'N/A'}</span>
</div>
</div>
</div>
</div>
<div className="modal-footer" style={{ display: 'flex', justifyContent: 'flex-end', gap: '10px', borderTop: '1px solid var(--glass-border)', paddingTop: '16px', marginTop: '16px' }}>
<button
className="btn btn-primary"
onClick={async () => {
await handleStatusChange(selectedProduct.id, 'approve');
setSelectedProduct(null);
}}
>
Approve
</button>
<button
className="btn btn-secondary"
style={{ background: 'rgba(255,255,255,0.05)', color: '#fff', border: '1px solid var(--glass-border)' }}
onClick={async () => {
await handleStatusChange(selectedProduct.id, 'hold');
setSelectedProduct(null);
}}
>
Hold
</button>
<button
className="btn btn-danger"
style={{ background: 'var(--color-danger)' }}
onClick={async () => {
await handleStatusChange(selectedProduct.id, 'reject');
setSelectedProduct(null);
}}
>
Reject
</button>
<button
className="btn btn-danger"
style={{ background: 'transparent', border: '1px solid var(--color-danger)', color: 'var(--color-danger)' }}
onClick={async () => {
await handleRemove(selectedProduct.id);
setSelectedProduct(null);
}}
>
Remove Product
</button>
</div>
</div>
</div>
)}
</div>
);
};

View file

@ -189,7 +189,7 @@ const SellerDetail = () => {
)}
{activeTab === 'documents' && (
<DocumentViewer documents={documents} sellerId={seller.id} />
<DocumentViewer documents={documents} sellerId={seller.id} refreshDocs={fetchSellerData} />
)}
</div>
);