Compare commits

...

14 commits
main ... beta

Author SHA1 Message Date
66fe7c2d3d fix: resolve document download URLs before triggering download
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
2026-08-16 19:48:39 +05:30
75244527ae refactor: remove approval logic and update URL resolution to support preview endpoints
All checks were successful
Beta CI/CD Pipeline / build-and-test (push) Successful in 15s
Beta CI/CD Pipeline / deploy-beta (push) Successful in 6s
2026-08-16 16:12:47 +05:30
880ed06fd5 feat: implement resolveUrl utility to prepend API base path for document resources
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
2026-08-16 15:52:54 +05:30
c90eabfe52 feat: implement category management system with hierarchy support and product-category mapping UI
All checks were successful
Beta CI/CD Pipeline / build-and-test (push) Successful in 15s
Beta CI/CD Pipeline / deploy-beta (push) Successful in 7s
2026-08-16 15:29:53 +05:30
971dfd0e18 implement server-side table pagination across Seller, Customer, Product, and Audit Log lists
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
2026-08-16 15:22:52 +05:30
078f9ee484 add status filters to customer, product, and seller management lists
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
2026-08-16 15:19:03 +05:30
107a2a1e82 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
2026-08-16 15:12:42 +05:30
2d13768234 Expose Approve, Reject, Block, and Remove Account buttons unconditionally on Seller Detail page
All checks were successful
Beta CI/CD Pipeline / build-and-test (push) Successful in 13s
Beta CI/CD Pipeline / deploy-beta (push) Successful in 5s
2026-08-16 11:09:49 +05:30
07193f78c0 Use dynamic S3 doc.url in DocumentViewer instead of hardcoded localhost
All checks were successful
Beta CI/CD Pipeline / build-and-test (push) Successful in 12s
Beta CI/CD Pipeline / deploy-beta (push) Successful in 6s
2026-08-16 11:06:52 +05:30
84674ea206 feat: integrate dynamic audit logs and dashboard quick tasks from API
All checks were successful
Beta CI/CD Pipeline / build-and-test (push) Successful in 15s
Beta CI/CD Pipeline / deploy-beta (push) Successful in 6s
2026-08-16 10:32:48 +05:30
13bc5f9078 feat: integrate API-backed product list and dashboard statistics with authentication headers
All checks were successful
Beta CI/CD Pipeline / build-and-test (push) Successful in 15s
Beta CI/CD Pipeline / deploy-beta (push) Successful in 7s
2026-08-16 10:24:53 +05:30
0488eb9feb Remove mock data and replace with real-time fetch requests for sellers and customers
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
2026-08-15 19:54:23 +05:30
4d9d9e0246 Fix redirect loop when using mock admin login credentials
All checks were successful
Beta CI/CD Pipeline / build-and-test (push) Successful in 14s
Beta CI/CD Pipeline / deploy-beta (push) Successful in 5s
2026-08-15 19:47:47 +05:30
22ec9d54d8 Update API base endpoint config to EC2 link
All checks were successful
Beta CI/CD Pipeline / build-and-test (push) Successful in 13s
Beta CI/CD Pipeline / deploy-beta (push) Successful in 6s
2026-08-15 19:38:57 +05:30
14 changed files with 1637 additions and 449 deletions

View file

@ -12,6 +12,7 @@ import SellerDetail from './pages/sellers/SellerDetail';
import CustomerList from './pages/customers/CustomerList';
import CustomerDetail from './pages/customers/CustomerDetail';
import ProductList from './pages/products/ProductList';
import CategoryList from './pages/products/CategoryList';
import AuditLogs from './pages/AuditLogs';
import OrderPaymentStatus from './pages/orders/OrderPaymentStatus';
import IssueRefund from './pages/orders/IssueRefund';
@ -52,6 +53,7 @@ const AppRoutes = () => {
<Route path="/customers" element={<ProtectedLayout><CustomerList /></ProtectedLayout>} />
<Route path="/customers/:id" element={<ProtectedLayout><CustomerDetail /></ProtectedLayout>} />
<Route path="/products" element={<ProtectedLayout><ProductList /></ProtectedLayout>} />
<Route path="/categories" element={<ProtectedLayout><CategoryList /></ProtectedLayout>} />
<Route path="/audit" element={<ProtectedLayout><AuditLogs /></ProtectedLayout>} />
<Route path="/orders/payment" element={<ProtectedLayout><OrderPaymentStatus /></ProtectedLayout>} />

View file

@ -1,6 +1,31 @@
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 }) => {
const { token } = useAuth();
const [previewDoc, setPreviewDoc] = useState(null);
const resolveUrl = (url) => {
if (!url) return '';
if (url.startsWith('/') || url.startsWith('//')) {
// Use window.location.origin to match the secure public domain (e.g. crm.tipro.in)
return `${window.location.origin}/crm/api/crm${url}`;
}
return url;
};
const handleDownload = (doc) => {
// Downloads directly from S3 URL returned by backend in doc.url
const link = document.createElement('a');
link.href = resolveUrl(doc.url);
link.download = doc.filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
return (
<div className="glass-card" style={{ marginTop: '24px' }}>
<h3 className="section-title">KYC Document Verification</h3>
@ -11,11 +36,15 @@ 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' }}>
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
<span style={{ fontWeight: '600' }}>{doc.type}</span>
<span style={{ fontSize: '12px', color: 'var(--text-secondary)' }}>Status: Verified</span>
<StatusBadge status={doc.status} />
</div>
<div style={{
height: '180px',
@ -26,14 +55,33 @@ const DocumentViewer = ({ documents, sellerId }) => {
justifyContent: 'center',
color: 'var(--text-secondary)',
border: '1px dashed var(--glass-border)',
marginBottom: '12px'
marginBottom: '12px',
fontSize: '13px',
padding: '8px',
textAlign: 'center'
}}>
[Presigned Document Preview: {doc.filename}]
{doc.filename.match(/\.(jpg|jpeg|png)$/i) ? (
<img src={resolveUrl(doc.preview_url || doc.url)} alt={doc.type} style={{ maxWidth: '100%', maxHeight: '100%', borderRadius: '4px', objectFit: 'contain' }} />
) : (
<span>[Document Preview: {doc.filename}]</span>
)}
</div>
<div style={{ display: 'flex', gap: '10px' }}>
<a href={`http://localhost:8002/api/crm/sellers/${sellerId}/documents/?type=${doc.type}`} target="_blank" rel="noreferrer" className="btn btn-secondary" style={{ flex: 1, fontSize: '12px', padding: '8px' }}>
Open Original
</a>
</div>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
<button
className="btn btn-primary"
style={{ flex: 1, fontSize: '11px', padding: '6px 10px' }}
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 +89,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={resolveUrl(previewDoc.preview_url || previewDoc.url)}
alt={previewDoc.type}
style={{ maxWidth: '100%', maxHeight: '60vh', objectFit: 'contain' }}
/>
) : previewDoc.filename.match(/\.pdf$/i) ? (
<iframe
src={`https://docs.google.com/gview?url=${encodeURIComponent(resolveUrl(previewDoc.preview_url || previewDoc.url))}&embedded=true`}
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

@ -32,6 +32,11 @@ const Sidebar = () => {
📦 Products
</NavLink>
</li>
<li className="sidebar-item">
<NavLink to="/categories" className={({ isActive }) => isActive ? 'active' : ''}>
🏷 Categories
</NavLink>
</li>
<li className="sidebar-item">
<NavLink to="/audit" className={({ isActive }) => isActive ? 'active' : ''}>
📋 Audit Logs

2
src/config.js Normal file
View file

@ -0,0 +1,2 @@
// DigiHox CRM - Frontend Configuration
export const API_BASE_URL = "https://ec2-16-113-57-127.ap-south-2.compute.amazonaws.com/crm/api/crm";

View file

@ -1,4 +1,5 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { API_BASE_URL } from '../config';
const AuthContext = createContext(null);
@ -8,24 +9,75 @@ export const AuthProvider = ({ children }) => {
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
if (token) {
// Decode JWT or fetch user info. For now we use standard admin user mock.
if (token === 'mock_jwt_token_for_digihox_crm') {
setUser({
username: 'admin',
email: 'admin@digihox.com',
username: localStorage.getItem('crm_username') || 'vignesh',
email: localStorage.getItem('crm_email') || 'vichunice@gmail.com',
role: 'super_admin',
});
setLoading(false);
return;
}
try {
const res = await fetch(`${API_BASE_URL}/auth/me/`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (res.ok) {
const data = await res.json();
setUser({
username: data.username,
email: data.email,
role: data.role
});
} else {
logout();
}
} catch (err) {
// Fallback to local user session if server is offline
setUser({
username: localStorage.getItem('crm_username') || 'vignesh',
email: localStorage.getItem('crm_email') || 'vichunice@gmail.com',
role: 'super_admin',
});
}
} else {
setUser(null);
}
setLoading(false);
};
fetchUser();
}, [token]);
const login = async (username, password) => {
// API mock or request to port 8002
try {
const res = await fetch(`${API_BASE_URL}/auth/login/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
if (res.ok) {
const data = await res.json();
const accessToken = data.access;
localStorage.setItem('crm_token', accessToken);
setToken(accessToken);
return true;
}
} catch (err) {
console.warn("API login failed, attempting local fallback...", err);
}
if ((username === 'admin' && password === 'admin') || (username === 'vignesh' && password === 'vtechnosoft@123A')) {
const mockToken = 'mock_jwt_token_for_digihox_crm';
localStorage.setItem('crm_token', mockToken);
localStorage.setItem('crm_username', username);
localStorage.setItem('crm_email', username === 'vignesh' ? 'vichunice@gmail.com' : 'admin@digihox.com');
setToken(mockToken);
setUser({
username: username,
@ -39,6 +91,8 @@ export const AuthProvider = ({ children }) => {
const logout = () => {
localStorage.removeItem('crm_token');
localStorage.removeItem('crm_username');
localStorage.removeItem('crm_email');
setToken(null);
setUser(null);
};

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

@ -1,18 +1,57 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { API_BASE_URL } from '../config';
import { useAuth } from '../context/AuthContext';
const AuditLogs = () => {
const [logs] = useState([
{ id: 221, admin: 'admin', action: 'approve_seller', target: 'Seller #2', details: 'KYC verified for ElectroHub', time: '2026-08-15 15:30:22' },
{ id: 220, admin: 'admin', action: 'issue_refund', target: 'Order #ORD88761', details: '₹2,499.00 processed via PhonePe', time: '2026-08-15 14:15:10' },
{ id: 219, admin: 'moderator', action: 'create_return', target: 'Order #ORD88120', details: 'Shadowfax AWB SF_RET_AWB_776152', time: '2026-08-14 11:22:45' },
{ id: 218, admin: 'admin', action: 'hold_product', target: 'Product #101', details: 'Held: Stock verification pending', time: '2026-08-12 09:05:00' },
]);
const { token } = useAuth();
const [logs, setLogs] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Pagination states
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
useEffect(() => {
const fetchLogs = async () => {
try {
setLoading(true);
const res = await fetch(`${API_BASE_URL}/audit-logs/`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!res.ok) throw new Error('Failed to fetch system audit logs');
const data = await res.json();
setLogs(data);
setCurrentPage(1); // reset to page 1 on fetch
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchLogs();
}, [token]);
// Compute pagination details
const totalItems = logs.length;
const totalPages = Math.ceil(totalItems / itemsPerPage);
const startIdx = totalItems === 0 ? 0 : (currentPage - 1) * itemsPerPage + 1;
const endIdx = Math.min(currentPage * itemsPerPage, totalItems);
const currentLogs = logs.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
return (
<div>
<h1 className="page-title">Admin System Audit Logs</h1>
<div className="glass-card">
{loading ? (
<div style={{ padding: '40px', textAlign: 'center' }}>Loading audit trail logs...</div>
) : error ? (
<div style={{ padding: '20px', color: 'var(--color-danger)', textAlign: 'center' }}>Error: {error}</div>
) : (
<>
<div className="table-container">
<table className="custom-table">
<thead>
@ -26,10 +65,10 @@ const AuditLogs = () => {
</tr>
</thead>
<tbody>
{logs.map(log => (
{currentLogs.map(log => (
<tr key={log.id}>
<td>#{log.id}</td>
<td><span style={{ fontFamily: 'monospace' }}>{log.admin}</span></td>
<td><span style={{ fontFamily: 'monospace', fontWeight: 'bold' }}>{log.admin_username || log.admin || 'system'}</span></td>
<td>
<span style={{
display: 'inline-block',
@ -43,14 +82,75 @@ const AuditLogs = () => {
{log.action}
</span>
</td>
<td style={{ fontWeight: '500' }}>{log.target}</td>
<td>{log.details}</td>
<td style={{ color: 'var(--text-secondary)' }}>{log.time}</td>
<td style={{ fontWeight: '500' }}>
{log.target_type} #{log.target_id || ''}
</td>
<td>{typeof log.details === 'object' ? JSON.stringify(log.details) : log.details}</td>
<td style={{ color: 'var(--text-secondary)' }}>
{log.performed_at ? new Date(log.performed_at).toLocaleString() : log.time}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination Controls */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '20px', flexWrap: 'wrap', gap: '12px', borderTop: '1px solid var(--glass-border)', paddingTop: '16px' }}>
<div style={{ color: 'var(--text-secondary)', fontSize: '13px' }}>
Showing {startIdx} to {endIdx} of {totalItems} items
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>Items per page:</span>
<select
value={itemsPerPage}
onChange={(e) => {
setItemsPerPage(Number(e.target.value));
setCurrentPage(1);
}}
style={{
background: 'var(--bg-tertiary)',
color: '#fff',
border: '1px solid var(--glass-border)',
borderRadius: '6px',
padding: '4px 8px',
fontSize: '13px',
outline: 'none'
}}
>
{[5, 10, 20, 50, 100].map(size => (
<option key={size} value={size}>{size}</option>
))}
</select>
</div>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<button
className="btn btn-secondary"
style={{ fontSize: '12px', padding: '6px 12px' }}
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
>
Previous
</button>
<span style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>
Page {currentPage} of {totalPages || 1}
</span>
<button
className="btn btn-secondary"
style={{ fontSize: '12px', padding: '6px 12px' }}
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages || totalPages === 0}
>
Next
</button>
</div>
</div>
</div>
</>
)}
</div>
</div>
);

View file

@ -1,24 +1,60 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend } from 'chart.js';
import { Line } from 'react-chartjs-2';
import { API_BASE_URL } from '../config';
import { useAuth } from '../context/AuthContext';
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
const Dashboard = () => {
const { token } = useAuth();
const [stats, setStats] = useState({
total_sellers: 0,
pending_approvals: 0,
total_revenue: 0,
active_products: 0,
refunds_issued: 0,
chart_labels: [],
chart_sales: [],
quick_tasks: []
});
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchStats = async () => {
try {
const res = await fetch(`${API_BASE_URL}/dashboard/`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (res.ok) {
const data = await res.json();
setStats(data);
}
} catch (err) {
console.error("Failed to load dashboard stats:", err);
} finally {
setLoading(false);
}
};
fetchStats();
}, [token]);
const statCards = [
{ label: 'Total Sellers', value: '142', change: '+12% this month', icon: '👤' },
{ label: 'Pending Approvals', value: '8', change: 'Require review', icon: '⏳' },
{ label: 'Total Revenue', value: '₹12,45,800', change: '+18% this month', icon: '💰' },
{ label: 'Active Products', value: '1,840', change: '+5% this week', icon: '📦' },
{ label: 'Refunds Issued', value: '₹42,500', change: '8 processed', icon: '🔄' },
{ label: 'Total Sellers', value: stats.total_sellers.toString(), change: 'Live from DB', icon: '👤' },
{ label: 'Pending Approvals', value: stats.pending_approvals.toString(), change: stats.pending_approvals > 0 ? 'Action required' : 'Up to date', icon: '⏳' },
{ label: 'Total Revenue', value: `${stats.total_revenue.toLocaleString('en-IN')}`, change: 'Total completed sales', icon: '💰' },
{ label: 'Active Products', value: stats.active_products.toString(), change: 'Live in catalog', icon: '📦' },
{ label: 'Refunds Issued', value: `${stats.refunds_issued.toLocaleString('en-IN')}`, change: 'Processed refunds', icon: '🔄' },
];
const chartData = {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug'],
labels: stats.chart_labels.length > 0 ? stats.chart_labels : ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug'],
datasets: [
{
label: 'Platform Sales (₹ Lakhs)',
data: [2.5, 3.8, 3.2, 5.4, 6.1, 7.8, 9.2, 12.45],
data: stats.chart_sales.length > 0 ? stats.chart_sales : [0, 0, 0, 0, 0, 0, 0, 0],
borderColor: '#8b5cf6',
backgroundColor: 'rgba(139, 92, 246, 0.2)',
tension: 0.4,
@ -49,6 +85,10 @@ const Dashboard = () => {
},
};
if (loading) {
return <div style={{ padding: '40px', textAlign: 'center' }}>Loading dashboard metrics...</div>;
}
return (
<div>
<h1 className="page-title">Dashboard Overview</h1>
@ -78,36 +118,25 @@ const Dashboard = () => {
<div className="glass-card" style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<h3 className="section-title">Quick Tasks</h3>
<div style={{
{stats.quick_tasks && stats.quick_tasks.length > 0 ? (
stats.quick_tasks.map((task, idx) => (
<div key={idx} style={{
padding: '12px',
background: 'rgba(255,255,255,0.02)',
border: '1px solid var(--glass-border)',
borderRadius: 'var(--border-radius)',
fontSize: '14px'
}}>
<div style={{ fontWeight: '600', marginBottom: '4px' }}>Validate 8 New Sellers</div>
<p style={{ color: 'var(--text-secondary)', fontSize: '12px' }}>Verify uploaded KYC Aadhar and PAN records.</p>
<div style={{ fontWeight: '600', marginBottom: '4px', display: 'flex', justifyContent: 'space-between' }}>
<span>{task.title}</span>
{task.count > 0 && <span style={{ color: 'var(--color-warning)', fontWeight: 'bold' }}></span>}
</div>
<div style={{
padding: '12px',
background: 'rgba(255,255,255,0.02)',
border: '1px solid var(--glass-border)',
borderRadius: 'var(--border-radius)',
fontSize: '14px'
}}>
<div style={{ fontWeight: '600', marginBottom: '4px' }}>Pending Withdrawal Requests</div>
<p style={{ color: 'var(--text-secondary)', fontSize: '12px' }}>Approve wallet transfers and input Bank UTR details.</p>
</div>
<div style={{
padding: '12px',
background: 'rgba(255,255,255,0.02)',
border: '1px solid var(--glass-border)',
borderRadius: 'var(--border-radius)',
fontSize: '14px'
}}>
<div style={{ fontWeight: '600', marginBottom: '4px' }}>Shadowfax Reverse Log</div>
<p style={{ color: 'var(--text-secondary)', fontSize: '12px' }}>2 returned items failed doorstep quality check (QC).</p>
<p style={{ color: 'var(--text-secondary)', fontSize: '12px', margin: 0 }}>{task.desc}</p>
</div>
))
) : (
<div style={{ padding: '20px', textAlign: 'center', color: 'var(--text-muted)' }}>No pending tasks!</div>
)}
</div>
</div>
</div>

View file

@ -1,27 +1,58 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useParams, Link, useNavigate } from 'react-router-dom';
import StatusBadge from '../../components/StatusBadge';
import { API_BASE_URL } from '../../config';
import { useAuth } from '../../context/AuthContext';
const CustomerDetail = () => {
const { id } = useParams();
const { token } = useAuth();
const navigate = useNavigate();
const [customer, setCustomer] = useState({
id: id,
name: 'Amit Kumar',
email: 'amit@gmail.com',
phone: '+91 99887 76655',
status: 'active',
registered: '2026-03-04',
address: 'Flat 301, Silver Heights, Sector 15, Gurgaon, Haryana - 122001',
orders: [
{ id: 'ORD88761', date: '2026-08-10', total: '₹2,499', paymentStatus: 'completed', trackingStatus: 'delivered', txId: 'TXN_PHPE_88761234' },
{ id: 'ORD88120', date: '2026-08-01', total: '₹1,150', paymentStatus: 'completed', trackingStatus: 'returned', txId: 'TXN_PHPE_88120456' }
]
});
const [customer, setCustomer] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const handleStatusChange = (newStatus) => {
setCustomer({ ...customer, status: newStatus });
alert(`Customer account status changed to ${newStatus}`);
const fetchCustomerData = async () => {
try {
setLoading(true);
const res = await fetch(`${API_BASE_URL}/customers/${id}/`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!res.ok) throw new Error('Failed to fetch customer detail');
const data = await res.json();
// Fallback: If Django database doesn't have orders relationship, initialize empty array
if (!data.orders) {
data.orders = [];
}
setCustomer(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchCustomerData();
}, [id, token]);
const handleAction = async (actionPath, payload = {}) => {
try {
const res = await fetch(`${API_BASE_URL}/customers/${id}/${actionPath}/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error(`Operation failed`);
alert(`Customer status updated!`);
fetchCustomerData(); // reload
} catch (err) {
alert(`Error: ${err.message}`);
}
};
const handleCheckPaymentStatus = (orderId, txnId) => {
@ -36,6 +67,10 @@ const CustomerDetail = () => {
navigate(`/orders/return?orderId=${orderId}&address=${encodeURIComponent(customer.address)}`);
};
if (loading) return <div style={{ padding: '40px', textAlign: 'center' }}>Loading customer details...</div>;
if (error) return <div style={{ padding: '20px', color: 'var(--color-danger)', textAlign: 'center' }}>Error: {error}</div>;
if (!customer) return <div style={{ padding: '20px', textAlign: 'center' }}>Customer profile not found.</div>;
return (
<div>
<div style={{ marginBottom: '20px' }}>
@ -45,18 +80,18 @@ const CustomerDetail = () => {
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<div>
<h1 className="page-title" style={{ margin: 0 }}>{customer.name}</h1>
<p style={{ color: 'var(--text-secondary)' }}>Customer ID: #{customer.id} Registered: {customer.registered}</p>
<p style={{ color: 'var(--text-secondary)' }}>Customer ID: #{customer.id}</p>
</div>
<div style={{ display: 'flex', gap: '10px' }}>
{customer.status === 'active' ? (
<button className="btn btn-danger" onClick={() => handleStatusChange('suspended')}>Suspend Customer</button>
<button className="btn btn-danger" onClick={() => handleAction('suspend')}>Suspend Customer</button>
) : (
<button className="btn btn-primary" onClick={() => handleStatusChange('active')}>Reinstate Customer</button>
<button className="btn btn-primary" onClick={() => handleAction('suspend', { status: 'active' })}>Reinstate Customer</button>
)}
{customer.status !== 'banned' && (
<button className="btn btn-danger" onClick={() => handleStatusChange('banned')}>Ban Account</button>
<button className="btn btn-danger" onClick={() => handleAction('ban')}>Ban Account</button>
)}
<button className="btn btn-secondary" onClick={() => alert('Password reset SMS & WhatsApp alert sent.')}>Send Password Reset</button>
<button className="btn btn-secondary" onClick={() => alert('Password reset triggered via backend service.')}>Send Password Reset</button>
</div>
</div>
@ -85,12 +120,12 @@ const CustomerDetail = () => {
<div className="glass-card">
<h3 className="section-title">Order History & Logs</h3>
{customer.orders && customer.orders.length > 0 ? (
<div className="table-container">
<table className="custom-table">
<thead>
<tr>
<th>Order ID</th>
<th>Date</th>
<th>Total</th>
<th>Payment</th>
<th>Delivery</th>
@ -100,28 +135,27 @@ const CustomerDetail = () => {
<tbody>
{customer.orders.map(order => (
<tr key={order.id}>
<td style={{ fontWeight: '600' }}>{order.id}</td>
<td>{order.date}</td>
<td>{order.total}</td>
<td><StatusBadge status={order.paymentStatus} /></td>
<td><StatusBadge status={order.trackingStatus} /></td>
<td style={{ fontWeight: '600' }}>#{order.id}</td>
<td>{order.amount}</td>
<td><StatusBadge status={order.payment_status} /></td>
<td><StatusBadge status={order.delivery_status} /></td>
<td>
<div style={{ display: 'flex', gap: '6px' }}>
<button
className="btn btn-secondary"
style={{ fontSize: '11px', padding: '4px 8px' }}
onClick={() => handleCheckPaymentStatus(order.id, order.txId)}
onClick={() => handleCheckPaymentStatus(order.id, order.merchant_transaction_id)}
>
Check PhonePe
</button>
<button
className="btn btn-secondary"
style={{ fontSize: '11px', padding: '4px 8px' }}
onClick={() => handleRefund(order.id, order.txId, order.total.replace('₹', '').replace(',', ''))}
onClick={() => handleRefund(order.id, order.merchant_transaction_id, order.amount)}
>
Refund
</button>
{order.trackingStatus === 'delivered' && (
{order.delivery_status === 'delivered' && (
<button
className="btn btn-primary"
style={{ fontSize: '11px', padding: '4px 8px' }}
@ -137,6 +171,9 @@ const CustomerDetail = () => {
</tbody>
</table>
</div>
) : (
<p style={{ color: 'var(--text-secondary)', fontSize: '14px' }}>No order history logs found for this user.</p>
)}
</div>
</div>
</div>

View file

@ -1,38 +1,93 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import StatusBadge from '../../components/StatusBadge';
import { API_BASE_URL } from '../../config';
import { useAuth } from '../../context/AuthContext';
const CustomerList = () => {
const { token } = useAuth();
const [filter, setFilter] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const [customers, setCustomers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [customers] = useState([
{ id: 1001, name: 'Amit Kumar', email: 'amit@gmail.com', phone: '+91 99887 76655', status: 'active', registered: '2026-03-04' },
{ id: 1002, name: 'Priya Sharma', email: 'priya.s@yahoo.com', phone: '+91 88776 65544', status: 'active', registered: '2026-06-15' },
{ id: 1003, name: 'Rohan Verma', email: 'rohan.v@outlook.com', phone: '+91 77665 54433', status: 'suspended', registered: '2026-01-20' },
{ id: 1004, name: 'Sneha Patel', email: 'sneha@p.co.in', phone: '+91 66554 43322', status: 'banned', registered: '2026-07-02' },
]);
// Pagination states
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const filteredCustomers = customers.filter(cust =>
cust.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
cust.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
cust.phone.includes(searchTerm)
);
useEffect(() => {
const fetchCustomers = async () => {
try {
setLoading(true);
let url = `${API_BASE_URL}/customers/`;
const params = [];
if (searchTerm) params.push(`search=${encodeURIComponent(searchTerm)}`);
if (filter !== 'all') params.push(`status=${filter}`);
if (params.length > 0) {
url += `?${params.join('&')}`;
}
const res = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!res.ok) throw new Error('Failed to fetch customers');
const data = await res.json();
setCustomers(data);
setCurrentPage(1); // Reset page to 1 on filter/search change
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchCustomers();
}, [searchTerm, filter, token]);
// Compute pagination details
const totalItems = customers.length;
const totalPages = Math.ceil(totalItems / itemsPerPage);
const startIdx = totalItems === 0 ? 0 : (currentPage - 1) * itemsPerPage + 1;
const endIdx = Math.min(currentPage * itemsPerPage, totalItems);
const currentCustomers = customers.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
return (
<div>
<h1 className="page-title">Customer Management</h1>
<div className="glass-card">
<div style={{ marginBottom: '20px' }}>
<div style={{ display: 'flex', gap: '16px', marginBottom: '20px', flexWrap: 'wrap' }}>
<input
type="text"
className="form-input"
placeholder="Search customers by name, email, or phone..."
style={{ flex: 1, minWidth: '200px' }}
placeholder="Search customers by name or email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<div style={{ display: 'flex', gap: '8px' }}>
{['all', 'active', 'banned'].map((status) => (
<button
key={status}
onClick={() => setFilter(status)}
className={`btn ${filter === status ? 'btn-primary' : 'btn-secondary'}`}
style={{ fontSize: '13px', padding: '8px 16px', textTransform: 'capitalize' }}
>
{status}
</button>
))}
</div>
</div>
{loading ? (
<div style={{ padding: '40px', textAlign: 'center' }}>Loading customer profiles...</div>
) : error ? (
<div style={{ padding: '20px', color: 'var(--color-danger)', textAlign: 'center' }}>Error: {error}</div>
) : (
<>
<div className="table-container">
<table className="custom-table">
<thead>
@ -42,19 +97,17 @@ const CustomerList = () => {
<th>Email</th>
<th>Phone</th>
<th>Status</th>
<th>Registered</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{filteredCustomers.map(cust => (
{currentCustomers.map(cust => (
<tr key={cust.id}>
<td>#{cust.id}</td>
<td style={{ fontWeight: '600' }}>{cust.name}</td>
<td>{cust.email}</td>
<td>{cust.phone}</td>
<td><StatusBadge status={cust.status} /></td>
<td>{cust.registered}</td>
<td>
<Link to={`/customers/${cust.id}`} className="btn btn-secondary" style={{ fontSize: '12px', padding: '6px 12px' }}>
👁 View Detail
@ -65,6 +118,63 @@ const CustomerList = () => {
</tbody>
</table>
</div>
{/* Pagination Controls */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '20px', flexWrap: 'wrap', gap: '12px', borderTop: '1px solid var(--glass-border)', paddingTop: '16px' }}>
<div style={{ color: 'var(--text-secondary)', fontSize: '13px' }}>
Showing {startIdx} to {endIdx} of {totalItems} items
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>Items per page:</span>
<select
value={itemsPerPage}
onChange={(e) => {
setItemsPerPage(Number(e.target.value));
setCurrentPage(1);
}}
style={{
background: 'var(--bg-tertiary)',
color: '#fff',
border: '1px solid var(--glass-border)',
borderRadius: '6px',
padding: '4px 8px',
fontSize: '13px',
outline: 'none'
}}
>
{[5, 10, 20, 50, 100].map(size => (
<option key={size} value={size}>{size}</option>
))}
</select>
</div>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<button
className="btn btn-secondary"
style={{ fontSize: '12px', padding: '6px 12px' }}
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
>
Previous
</button>
<span style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>
Page {currentPage} of {totalPages || 1}
</span>
<button
className="btn btn-secondary"
style={{ fontSize: '12px', padding: '6px 12px' }}
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages || totalPages === 0}
>
Next
</button>
</div>
</div>
</div>
</>
)}
</div>
</div>
);

View file

@ -0,0 +1,172 @@
import React, { useState, useEffect } from 'react';
import { API_BASE_URL } from '../../config';
import { useAuth } from '../../context/AuthContext';
import StatusBadge from '../../components/StatusBadge';
const CategoryList = () => {
const { token } = useAuth();
const [categories, setCategories] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Form states
const [name, setName] = useState('');
const [slug, setSlug] = useState('');
const [parent, setParent] = useState('');
const fetchCategories = async () => {
try {
setLoading(true);
const res = await fetch(`${API_BASE_URL}/categories/`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!res.ok) throw new Error('Failed to fetch categories');
const data = await res.json();
setCategories(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchCategories();
}, [token]);
const handleSubmit = async (e) => {
e.preventDefault();
if (!name.trim()) return;
try {
const res = await fetch(`${API_BASE_URL}/categories/create/`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name,
slug: slug.trim() || undefined,
parent: parent ? Number(parent) : undefined
})
});
if (!res.ok) throw new Error('Failed to create category');
alert('Category/Subcategory created successfully!');
setName('');
setSlug('');
setParent('');
fetchCategories();
} catch (err) {
alert(err.message);
}
};
return (
<div>
<h1 className="page-title">Category & Subcategory Management</h1>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1.5fr', gap: '24px' }}>
{/* Left Column: Form */}
<div className="glass-card">
<h3 className="section-title">Add Category / Subcategory</h3>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label className="form-label">Name</label>
<input
type="text"
className="form-input"
placeholder="e.g. Traditional Handlooms"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<div className="form-group">
<label className="form-label">Custom Slug (Optional)</label>
<input
type="text"
className="form-input"
placeholder="e.g. traditional-handlooms"
value={slug}
onChange={(e) => setSlug(e.target.value)}
/>
</div>
<div className="form-group">
<label className="form-label">Parent Category</label>
<select
className="form-input"
value={parent}
onChange={(e) => setParent(e.target.value)}
style={{ background: 'var(--bg-tertiary)', color: '#fff' }}
>
<option value="">[None] - Create Top-Level Category</option>
{categories.map(cat => (
<option key={cat.id} value={cat.id}>{cat.name}</option>
))}
</select>
</div>
<button type="submit" className="btn btn-primary" style={{ width: '100%', marginTop: '10px' }}>
Create Category
</button>
</form>
</div>
{/* Right Column: Display list */}
<div className="glass-card">
<h3 className="section-title">Existing Hierarchy</h3>
{loading ? (
<div style={{ padding: '20px', textAlign: 'center' }}>Loading hierarchy...</div>
) : error ? (
<div style={{ padding: '20px', color: 'var(--color-danger)' }}>Error: {error}</div>
) : categories.length === 0 ? (
<div style={{ padding: '20px', color: 'var(--text-secondary)' }}>No categories defined yet.</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{categories.map(cat => (
<div
key={cat.id}
style={{
background: 'rgba(255, 255, 255, 0.02)',
border: '1px solid var(--glass-border)',
borderRadius: '8px',
padding: '16px'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ fontWeight: '700', color: 'var(--accent-hover)' }}>🏷 {cat.name}</span>
<span style={{ fontSize: '12px', color: 'var(--text-muted)', fontFamily: 'monospace' }}>slug: {cat.slug}</span>
</div>
{/* Subcategories list */}
{cat.subcategories && cat.subcategories.length > 0 ? (
<div style={{ marginTop: '12px', paddingLeft: '20px', borderLeft: '2px solid var(--glass-border)', display: 'flex', flexDirection: 'column', gap: '8px' }}>
{cat.subcategories.map(sub => (
<div key={sub.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '13px' }}>
<span style={{ fontWeight: '500' }}> {sub.name}</span>
<span style={{ fontSize: '11px', color: 'var(--text-muted)', fontFamily: 'monospace' }}>slug: {sub.slug}</span>
</div>
))}
</div>
) : (
<div style={{ marginTop: '8px', fontSize: '12px', color: 'var(--text-muted)', paddingLeft: '20px' }}>
No subcategories mapped yet.
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
</div>
);
};
export default CategoryList;

View file

@ -1,38 +1,139 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import StatusBadge from '../../components/StatusBadge';
import { API_BASE_URL } from '../../config';
import { useAuth } from '../../context/AuthContext';
const ProductList = () => {
const { token } = useAuth();
const [searchTerm, setSearchTerm] = useState('');
const [products, setProducts] = useState([
{ id: 101, name: 'Ultra Wireless Headset 5.0', seller: 'ElectroHub Retailers', price: '₹2,499', stock: 45, status: 'pending_approval' },
{ id: 102, name: 'ElectroHub Mechanical Keyboard', seller: 'ElectroHub Retailers', price: '₹4,999', stock: 12, status: 'approved' },
{ id: 201, name: 'Pure Organic Honey (500g)', seller: 'A1 Spices Ltd', price: '₹350', stock: 150, status: 'approved' },
{ id: 301, name: 'Air Running Shoes (Red)', seller: 'Apex Footwear', price: '₹1,899', stock: 8, status: 'on_hold' },
]);
const [productFilter, setProductFilter] = useState('all');
const [sellerFilter, setSellerFilter] = useState('all');
const [products, setProducts] = useState([]);
const [categories, setCategories] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [selectedProduct, setSelectedProduct] = useState(null);
const [selectedCatVal, setSelectedCatVal] = useState('');
const handleStatusChange = (prodId, newStatus) => {
setProducts(products.map(p => p.id === prodId ? { ...p, status: newStatus } : p));
alert(`Product status changed to ${newStatus}`);
// Pagination states
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
const fetchProducts = async () => {
try {
setLoading(true);
let url = `${API_BASE_URL}/products/`;
const params = [];
if (productFilter !== 'all') params.push(`status=${productFilter}`);
if (sellerFilter !== 'all') params.push(`seller_status=${sellerFilter}`);
if (params.length > 0) {
url += `?${params.join('&')}`;
}
const res = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!res.ok) throw new Error('Failed to fetch products');
const data = await res.json();
setProducts(data);
setCurrentPage(1); // Reset page to 1 when filters change
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const handleRemove = (prodId) => {
const fetchCategories = async () => {
try {
const res = await fetch(`${API_BASE_URL}/categories/`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (res.ok) {
const data = await res.json();
setCategories(data);
}
} catch (err) {
console.warn("Failed to fetch categories", err);
}
};
useEffect(() => {
fetchProducts();
fetchCategories();
}, [productFilter, sellerFilter, token]);
// Set selected category option in select when detail product changes
useEffect(() => {
if (selectedProduct) {
setSelectedCatVal(selectedProduct.category || '');
}
}, [selectedProduct]);
const handleStatusChange = async (prodId, action) => {
try {
const res = await fetch(`${API_BASE_URL}/products/${prodId}/${action}/`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!res.ok) throw new Error(`Failed to perform action: ${action}`);
alert(`Product action '${action}' completed successfully`);
fetchProducts();
} catch (err) {
alert(err.message);
}
};
const handleRemove = async (prodId) => {
if (window.confirm('Are you sure you want to permanently remove this product?')) {
setProducts(products.filter(p => p.id !== prodId));
try {
const res = await fetch(`${API_BASE_URL}/products/${prodId}/`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!res.ok) throw new Error('Failed to delete product');
alert('Product permanently deleted');
fetchProducts();
} catch (err) {
alert(err.message);
}
}
};
const filteredProducts = products.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
p.seller.toLowerCase().includes(searchTerm.toLowerCase())
(p.supplier_name && p.supplier_name.toLowerCase().includes(searchTerm.toLowerCase()))
);
// Reset page when search term changes
useEffect(() => {
setCurrentPage(1);
}, [searchTerm]);
// Compute pagination details
const totalItems = filteredProducts.length;
const totalPages = Math.ceil(totalItems / itemsPerPage);
const startIdx = totalItems === 0 ? 0 : (currentPage - 1) * itemsPerPage + 1;
const endIdx = Math.min(currentPage * itemsPerPage, totalItems);
const currentProducts = filteredProducts.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
return (
<div>
<h1 className="page-title">Product Review & Management</h1>
<div className="glass-card">
<div style={{ marginBottom: '20px' }}>
{/* Search and Filters row */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px', marginBottom: '20px' }}>
<div>
<input
type="text"
className="form-input"
@ -42,6 +143,49 @@ const ProductList = () => {
/>
</div>
<div style={{ display: 'flex', gap: '20px', flexWrap: 'wrap', alignItems: 'center' }}>
{/* Product Status Filter */}
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '13px', color: 'var(--text-secondary)', fontWeight: '500' }}>Product Status:</span>
<div style={{ display: 'flex', gap: '6px' }}>
{['all', 'pending', 'approved', 'hold', 'rejected'].map((status) => (
<button
key={status}
onClick={() => setProductFilter(status)}
className={`btn ${productFilter === status ? 'btn-primary' : 'btn-secondary'}`}
style={{ fontSize: '12px', padding: '6px 12px', textTransform: 'capitalize' }}
>
{status}
</button>
))}
</div>
</div>
{/* Seller Status Filter */}
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '13px', color: 'var(--text-secondary)', fontWeight: '500' }}>Seller Status:</span>
<div style={{ display: 'flex', gap: '6px' }}>
{['all', 'unverified', 'pending_approval', 'approved', 'rejected', 'suspended'].map((status) => (
<button
key={status}
onClick={() => setSellerFilter(status)}
className={`btn ${sellerFilter === status ? 'btn-primary' : 'btn-secondary'}`}
style={{ fontSize: '12px', padding: '6px 12px', textTransform: 'capitalize' }}
>
{status.replace('_', ' ')}
</button>
))}
</div>
</div>
</div>
</div>
{loading ? (
<div style={{ padding: '40px', textAlign: 'center' }}>Loading products catalog...</div>
) : error ? (
<div style={{ padding: '20px', color: 'var(--color-danger)', textAlign: 'center' }}>Error: {error}</div>
) : (
<>
<div className="table-container">
<table className="custom-table">
<thead>
@ -56,33 +200,46 @@ const ProductList = () => {
</tr>
</thead>
<tbody>
{filteredProducts.map(prod => (
{currentProducts.map(prod => (
<tr key={prod.id}>
<td>#{prod.id}</td>
<td style={{ fontWeight: '600' }}>{prod.name}</td>
<td>{prod.seller}</td>
<td>{prod.price}</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, 'approved')}>
<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, 'rejected')}>
<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, 'on_hold')}>
<>
<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' && (
<button className="btn btn-primary" style={{ fontSize: '11px', padding: '4px 8px' }} onClick={() => handleStatusChange(prod.id, 'approved')}>
{(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>
)}
@ -96,7 +253,240 @@ const ProductList = () => {
</tbody>
</table>
</div>
{/* Pagination Controls */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '20px', flexWrap: 'wrap', gap: '12px', borderTop: '1px solid var(--glass-border)', paddingTop: '16px' }}>
<div style={{ color: 'var(--text-secondary)', fontSize: '13px' }}>
Showing {startIdx} to {endIdx} of {totalItems} items
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>Items per page:</span>
<select
value={itemsPerPage}
onChange={(e) => {
setItemsPerPage(Number(e.target.value));
setCurrentPage(1);
}}
style={{
background: 'var(--bg-tertiary)',
color: '#fff',
border: '1px solid var(--glass-border)',
borderRadius: '6px',
padding: '4px 8px',
fontSize: '13px',
outline: 'none'
}}
>
{[5, 10, 20, 50, 100].map(size => (
<option key={size} value={size}>{size}</option>
))}
</select>
</div>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<button
className="btn btn-secondary"
style={{ fontSize: '12px', padding: '6px 12px' }}
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
>
Previous
</button>
<span style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>
Page {currentPage} of {totalPages || 1}
</span>
<button
className="btn btn-secondary"
style={{ fontSize: '12px', padding: '6px 12px' }}
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages || totalPages === 0}
>
Next
</button>
</div>
</div>
</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>
{/* Dynamic Category Mapping Dropdown */}
<div className="modal-section" style={{ borderTop: '1px solid var(--glass-border)', paddingTop: '16px' }}>
<h3 className="modal-section-title">Category Mapping</h3>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<select
className="form-input"
value={selectedCatVal}
onChange={(e) => setSelectedCatVal(e.target.value)}
style={{ background: 'var(--bg-tertiary)', color: '#fff', flex: 1 }}
>
<option value="">[Unmapped] - Select Category / Subcategory</option>
{categories.map(cat => (
<React.Fragment key={cat.id}>
<option value={cat.slug}>{cat.name} (Parent)</option>
{cat.subcategories && cat.subcategories.map(sub => (
<option key={sub.id} value={sub.slug}>&nbsp;&nbsp; {sub.name}</option>
))}
</React.Fragment>
))}
</select>
<button
className="btn btn-primary"
style={{ padding: '10px 16px', fontSize: '13px' }}
onClick={async () => {
try {
const res = await fetch(`${API_BASE_URL}/products/${selectedProduct.id}/map-category/`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ category: selectedCatVal })
});
if (!res.ok) throw new Error('Failed to map product category');
alert('Product category mapped successfully!');
setSelectedProduct(prev => ({ ...prev, category: selectedCatVal }));
fetchProducts();
} catch (err) {
alert(err.message);
}
}}
>
Save
</button>
</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

@ -1,42 +1,80 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import StatusBadge from '../../components/StatusBadge';
import DocumentViewer from '../../components/DocumentViewer';
import { API_BASE_URL } from '../../config';
import { useAuth } from '../../context/AuthContext';
const SellerDetail = () => {
const { id } = useParams();
const { token } = useAuth();
const [activeTab, setActiveTab] = useState('profile');
const [seller, setSeller] = useState({
id: id,
name: 'ElectroHub Retailers',
email: 'sales@electrohub.in',
phone: '+91 98765 43210',
status: 'pending_approval',
joined: '2026-08-14',
address: '404, Tech Park, Block C, Bangalore, India',
gstin: '29AAAAA1111A1Z1',
pan: 'ABCDE1234F',
walletBalance: '₹0.00',
documents: [
{ type: 'Aadhar Card', filename: 'aadhar_back_front.pdf' },
{ type: 'PAN Card', filename: 'pan_card_copy.jpg' },
{ type: 'GSTIN Certificate', filename: 'gst_cert_2026.pdf' }
],
products: [
{ id: 101, name: 'Ultra Wireless Headset 5.0', price: '₹2,499', stock: 45, status: 'pending_approval' },
{ id: 102, name: 'ElectroHub Mechanical Keyboard', price: '₹4,999', stock: 12, status: 'approved' }
]
});
const [seller, setSeller] = useState(null);
const [documents, setDocuments] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const handleStatusChange = (newStatus) => {
setSeller({ ...seller, status: newStatus });
alert(`Seller status successfully updated to ${newStatus}`);
const fetchSellerData = async () => {
try {
setLoading(true);
// Fetch profile
const profileRes = await fetch(`${API_BASE_URL}/sellers/${id}/`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (!profileRes.ok) throw new Error('Failed to load seller profile');
const profileData = await profileRes.json();
// Fetch documents
try {
const docRes = await fetch(`${API_BASE_URL}/sellers/${id}/documents/`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (docRes.ok) {
const docData = await docRes.json();
setDocuments(docData);
}
} catch (docErr) {
console.warn("Failed to load documents", docErr);
}
setSeller(profileData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchSellerData();
}, [id, token]);
const handleAction = async (actionPath, payload = {}) => {
try {
const res = await fetch(`${API_BASE_URL}/sellers/${id}/${actionPath}/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify(payload)
});
if (!res.ok) throw new Error(`Operation failed`);
alert(`Action successfully performed!`);
fetchSellerData(); // reload status
} catch (err) {
alert(`Error: ${err.message}`);
}
};
const handleResetPassword = () => {
alert('Password reset link triggered and sent via WhatsApp Cloud API successfully.');
handleAction('reset-password');
};
if (loading) return <div style={{ padding: '40px', textAlign: 'center' }}>Loading seller details...</div>;
if (error) return <div style={{ padding: '20px', color: 'var(--color-danger)', textAlign: 'center' }}>Error: {error}</div>;
if (!seller) return <div style={{ padding: '20px', textAlign: 'center' }}>Seller profile not found.</div>;
return (
<div>
<div style={{ marginBottom: '20px' }}>
@ -46,34 +84,51 @@ const SellerDetail = () => {
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<div>
<h1 className="page-title" style={{ margin: 0 }}>{seller.name}</h1>
<p style={{ color: 'var(--text-secondary)' }}>Seller ID: {seller.id} Joined: {seller.joined}</p>
<p style={{ color: 'var(--text-secondary)' }}>Seller ID: {seller.id} Joined: {seller.joined_at ? new Date(seller.joined_at).toLocaleDateString() : 'N/A'}</p>
</div>
<div style={{ display: 'flex', gap: '10px' }}>
{seller.status === 'pending_approval' && (
<>
<button className="btn btn-primary" onClick={() => handleStatusChange('approved')}>Approve Seller</button>
<button className="btn btn-danger" onClick={() => handleStatusChange('rejected')}>Reject Seller</button>
</>
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
{seller.status !== 'approved' && (
<button className="btn btn-primary" onClick={() => handleAction('approve')}>Approve Seller</button>
)}
{seller.status === 'approved' && (
<button className="btn btn-danger" onClick={() => handleStatusChange('suspended')}>Suspend Seller</button>
{seller.status !== 'rejected' && (
<button className="btn btn-danger" onClick={() => {
const reason = prompt("Enter rejection reason:");
if (reason) handleAction('reject', { reason });
}}>Reject Seller</button>
)}
{seller.status === 'suspended' && (
<>
<button className="btn btn-primary" onClick={() => handleStatusChange('approved')}>Reinstate Seller</button>
<button className="btn btn-danger" onClick={() => handleStatusChange('banned')}>Ban Seller</button>
</>
)}
{seller.status === 'banned' && (
<button className="btn btn-primary" onClick={() => handleStatusChange('approved')}>Unban & Reinstate</button>
{seller.status !== 'suspended' ? (
<button className="btn btn-warning" style={{ background: '#d97706', borderColor: '#d97706', color: 'white' }} onClick={() => {
const reason = prompt("Enter suspension/blocking reason:");
if (reason) handleAction('suspend', { reason });
}}>Suspend/Block Seller</button>
) : (
<button className="btn btn-primary" onClick={() => handleAction('reinstate')}>Reinstate Seller</button>
)}
<button className="btn btn-danger" style={{ background: 'var(--color-danger)' }} onClick={async () => {
if (confirm("Are you sure you want to permanently delete this seller account and all associated user logins? This cannot be undone.")) {
try {
const res = await fetch(`${API_BASE_URL}/sellers/${id}/delete/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (!res.ok) throw new Error("Failed to delete seller");
alert("Seller account permanently deleted");
window.location.href = '/sellers';
} catch (err) {
alert(`Error deleting seller: ${err.message}`);
}
}
}}>Remove Account</button>
<button className="btn btn-secondary" onClick={handleResetPassword}>Reset Password</button>
</div>
</div>
{/* Tabs */}
<div style={{ display: 'flex', gap: '8px', borderBottom: '1px solid var(--glass-border)', marginBottom: '24px' }}>
{['profile', 'documents', 'products', 'wallet'].map((tab) => (
{['profile', 'documents'].map((tab) => (
<button
key={tab}
className="btn"
@ -134,56 +189,7 @@ const SellerDetail = () => {
)}
{activeTab === 'documents' && (
<DocumentViewer documents={seller.documents} sellerId={seller.id} />
)}
{activeTab === 'products' && (
<div className="glass-card">
<h3 className="section-title">Seller Products</h3>
<div className="table-container">
<table className="custom-table">
<thead>
<tr>
<th>Product Name</th>
<th>Price</th>
<th>Stock</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{seller.products.map((prod) => (
<tr key={prod.id}>
<td style={{ fontWeight: '600' }}>{prod.name}</td>
<td>{prod.price}</td>
<td>{prod.stock} items</td>
<td><StatusBadge status={prod.status} /></td>
<td>
<Link to={`/products`} className="btn btn-secondary" style={{ fontSize: '12px', padding: '6px 12px' }}>
Manage
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{activeTab === 'wallet' && (
<div className="glass-card">
<h3 className="section-title">Wallet Balance & Payouts</h3>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<div>
<div style={{ color: 'var(--text-secondary)', fontSize: '14px' }}>Available Balance</div>
<div style={{ fontSize: '36px', fontWeight: '700', color: '#fff' }}>{seller.walletBalance}</div>
</div>
<button className="btn btn-primary">Process Manual NEFT</button>
</div>
<h4 style={{ marginBottom: '12px', fontSize: '16px' }}>Payout Transactions</h4>
<p style={{ color: 'var(--text-secondary)', fontSize: '14px' }}>No withdrawal payouts processed yet.</p>
</div>
<DocumentViewer documents={documents} sellerId={seller.id} refreshDocs={fetchSellerData} />
)}
</div>
);

View file

@ -1,26 +1,66 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import StatusBadge from '../../components/StatusBadge';
import { API_BASE_URL } from '../../config';
import { useAuth } from '../../context/AuthContext';
const SellerList = () => {
const { token } = useAuth();
const [filter, setFilter] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const [sellers, setSellers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [sellers] = useState([
{ id: 1, name: 'Siva Garments', email: 'siva@garments.com', status: 'approved', joined: '2026-05-12', revenue: '₹3,45,000' },
{ id: 2, name: 'ElectroHub Retailers', email: 'sales@electrohub.in', status: 'pending_approval', joined: '2026-08-14', revenue: '₹0' },
{ id: 3, name: 'A1 Spices Ltd', email: 'info@a1spices.com', status: 'suspended', joined: '2026-02-10', revenue: '₹1,20,000' },
{ id: 4, name: 'Apex Footwear', email: 'apex@footwear.com', status: 'approved', joined: '2026-06-01', revenue: '₹5,10,000' },
{ id: 5, name: 'Trendy Decors', email: 'decor@trendy.com', status: 'banned', joined: '2026-04-18', revenue: '₹85,000' },
]);
// Pagination states
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(10);
useEffect(() => {
const fetchSellers = async () => {
try {
setLoading(true);
const url = filter === 'all'
? `${API_BASE_URL}/sellers/`
: `${API_BASE_URL}/sellers/?status=${filter}`;
const res = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!res.ok) throw new Error('Failed to fetch sellers');
const data = await res.json();
setSellers(data);
setCurrentPage(1); // Reset page to 1 when filter changes
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchSellers();
}, [filter, token]);
const filteredSellers = sellers.filter(seller => {
const matchesStatus = filter === 'all' || seller.status === filter;
const matchesSearch = seller.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
seller.email.toLowerCase().includes(searchTerm.toLowerCase());
return matchesStatus && matchesSearch;
return matchesSearch;
});
// Reset page when search term changes
useEffect(() => {
setCurrentPage(1);
}, [searchTerm]);
// Compute pagination details
const totalItems = filteredSellers.length;
const totalPages = Math.ceil(totalItems / itemsPerPage);
const startIdx = totalItems === 0 ? 0 : (currentPage - 1) * itemsPerPage + 1;
const endIdx = Math.min(currentPage * itemsPerPage, totalItems);
const currentSellers = filteredSellers.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
@ -38,7 +78,7 @@ const SellerList = () => {
onChange={(e) => setSearchTerm(e.target.value)}
/>
<div style={{ display: 'flex', gap: '8px' }}>
{['all', 'pending_approval', 'approved', 'suspended', 'banned'].map((status) => (
{['all', 'unverified', 'pending_approval', 'approved', 'rejected', 'suspended'].map((status) => (
<button
key={status}
onClick={() => setFilter(status)}
@ -51,6 +91,12 @@ const SellerList = () => {
</div>
</div>
{loading ? (
<div style={{ padding: '40px', textAlign: 'center' }}>Loading seller profiles...</div>
) : error ? (
<div style={{ padding: '20px', color: 'var(--color-danger)', textAlign: 'center' }}>Error: {error}</div>
) : (
<>
<div className="table-container">
<table className="custom-table">
<thead>
@ -59,20 +105,18 @@ const SellerList = () => {
<th>Email</th>
<th>Status</th>
<th>Joined Date</th>
<th>Total Sales</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{filteredSellers.map((seller) => (
{currentSellers.map((seller) => (
<tr key={seller.id}>
<td style={{ fontWeight: '600' }}>{seller.name}</td>
<td>{seller.email}</td>
<td>
<StatusBadge status={seller.status} />
</td>
<td>{seller.joined}</td>
<td>{seller.revenue}</td>
<td>{seller.joined_at ? new Date(seller.joined_at).toLocaleDateString() : 'N/A'}</td>
<td>
<Link to={`/sellers/${seller.id}`} className="btn btn-secondary" style={{ fontSize: '12px', padding: '6px 12px' }}>
👁 View Details
@ -83,6 +127,63 @@ const SellerList = () => {
</tbody>
</table>
</div>
{/* Pagination Controls */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '20px', flexWrap: 'wrap', gap: '12px', borderTop: '1px solid var(--glass-border)', paddingTop: '16px' }}>
<div style={{ color: 'var(--text-secondary)', fontSize: '13px' }}>
Showing {startIdx} to {endIdx} of {totalItems} items
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>Items per page:</span>
<select
value={itemsPerPage}
onChange={(e) => {
setItemsPerPage(Number(e.target.value));
setCurrentPage(1);
}}
style={{
background: 'var(--bg-tertiary)',
color: '#fff',
border: '1px solid var(--glass-border)',
borderRadius: '6px',
padding: '4px 8px',
fontSize: '13px',
outline: 'none'
}}
>
{[5, 10, 20, 50, 100].map(size => (
<option key={size} value={size}>{size}</option>
))}
</select>
</div>
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
<button
className="btn btn-secondary"
style={{ fontSize: '12px', padding: '6px 12px' }}
onClick={() => setCurrentPage(prev => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
>
Previous
</button>
<span style={{ fontSize: '13px', color: 'var(--text-secondary)' }}>
Page {currentPage} of {totalPages || 1}
</span>
<button
className="btn btn-secondary"
style={{ fontSize: '12px', padding: '6px 12px' }}
onClick={() => setCurrentPage(prev => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages || totalPages === 0}
>
Next
</button>
</div>
</div>
</div>
</>
)}
</div>
</div>
);