feat: add modal popup for product details and enhance status management in ProductList
This commit is contained in:
parent
2d13768234
commit
107a2a1e82
4 changed files with 370 additions and 32 deletions
|
|
@ -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)}>×</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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)}>×</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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ const SellerDetail = () => {
|
|||
)}
|
||||
|
||||
{activeTab === 'documents' && (
|
||||
<DocumentViewer documents={documents} sellerId={seller.id} />
|
||||
<DocumentViewer documents={documents} sellerId={seller.id} refreshDocs={fetchSellerData} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue