feat: integrate API-backed product list and dashboard statistics with authentication headers
This commit is contained in:
parent
0488eb9feb
commit
13bc5f9078
2 changed files with 164 additions and 76 deletions
|
|
@ -1,16 +1,49 @@
|
|||
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
|
||||
});
|
||||
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 = {
|
||||
|
|
@ -18,7 +51,7 @@ const Dashboard = () => {
|
|||
datasets: [
|
||||
{
|
||||
label: 'Platform Sales (₹ Lakhs)',
|
||||
data: [2.5, 3.8, 3.2, 5.4, 6.1, 7.8, 9.2, 12.45],
|
||||
data: [2.5, 3.8, 3.2, 5.4, 6.1, 7.8, 9.2, stats.total_revenue / 100000],
|
||||
borderColor: '#8b5cf6',
|
||||
backgroundColor: 'rgba(139, 92, 246, 0.2)',
|
||||
tension: 0.4,
|
||||
|
|
@ -49,6 +82,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>
|
||||
|
|
@ -85,7 +122,7 @@ const Dashboard = () => {
|
|||
borderRadius: 'var(--border-radius)',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
<div style={{ fontWeight: '600', marginBottom: '4px' }}>Validate 8 New Sellers</div>
|
||||
<div style={{ fontWeight: '600', marginBottom: '4px' }}>Validate New Sellers</div>
|
||||
<p style={{ color: 'var(--text-secondary)', fontSize: '12px' }}>Verify uploaded KYC Aadhar and PAN records.</p>
|
||||
</div>
|
||||
<div style={{
|
||||
|
|
@ -106,7 +143,7 @@ const Dashboard = () => {
|
|||
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' }}>Check failed doorstep quality check (QC) status.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,30 +1,75 @@
|
|||
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 [products, setProducts] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const handleStatusChange = (prodId, newStatus) => {
|
||||
setProducts(products.map(p => p.id === prodId ? { ...p, status: newStatus } : p));
|
||||
alert(`Product status changed to ${newStatus}`);
|
||||
const fetchProducts = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await fetch(`${API_BASE_URL}/products/`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to fetch products');
|
||||
const data = await res.json();
|
||||
setProducts(data);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (prodId) => {
|
||||
useEffect(() => {
|
||||
fetchProducts();
|
||||
}, [token]);
|
||||
|
||||
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));
|
||||
alert('Product permanently deleted');
|
||||
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()))
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -42,60 +87,66 @@ const ProductList = () => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="table-container">
|
||||
<table className="custom-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Product Title</th>
|
||||
<th>Seller</th>
|
||||
<th>Price</th>
|
||||
<th>Stock</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredProducts.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>{prod.stock} items</td>
|
||||
<td><StatusBadge status={prod.status} /></td>
|
||||
<td>
|
||||
<div style={{ display: 'flex', gap: '6px' }}>
|
||||
{prod.status === 'pending_approval' && (
|
||||
<>
|
||||
<button className="btn btn-primary" style={{ fontSize: '11px', padding: '4px 8px' }} onClick={() => handleStatusChange(prod.id, 'approved')}>
|
||||
Approve
|
||||
</button>
|
||||
<button className="btn btn-danger" style={{ fontSize: '11px', padding: '4px 8px' }} onClick={() => handleStatusChange(prod.id, 'rejected')}>
|
||||
Reject
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{prod.status === 'approved' && (
|
||||
<button className="btn btn-secondary" style={{ fontSize: '11px', padding: '4px 8px' }} onClick={() => handleStatusChange(prod.id, 'on_hold')}>
|
||||
Hold Item
|
||||
</button>
|
||||
)}
|
||||
{prod.status === 'on_hold' && (
|
||||
<button className="btn btn-primary" style={{ fontSize: '11px', padding: '4px 8px' }} onClick={() => handleStatusChange(prod.id, 'approved')}>
|
||||
Activate
|
||||
</button>
|
||||
)}
|
||||
<button className="btn btn-danger" style={{ fontSize: '11px', padding: '4px 8px', background: 'transparent', border: '1px solid var(--color-danger)', color: 'var(--color-danger)' }} onClick={() => handleRemove(prod.id)}>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
{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>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Product Title</th>
|
||||
<th>Seller</th>
|
||||
<th>Price</th>
|
||||
<th>Stock</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredProducts.map(prod => (
|
||||
<tr key={prod.id}>
|
||||
<td>#{prod.id}</td>
|
||||
<td style={{ fontWeight: '600' }}>{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' && (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
{prod.status === 'approved' && (
|
||||
<button className="btn btn-secondary" style={{ fontSize: '11px', padding: '4px 8px' }} onClick={() => handleStatusChange(prod.id, 'hold')}>
|
||||
Hold Item
|
||||
</button>
|
||||
)}
|
||||
{prod.status === 'on_hold' && (
|
||||
<button className="btn btn-primary" style={{ fontSize: '11px', padding: '4px 8px' }} onClick={() => handleStatusChange(prod.id, 'approve')}>
|
||||
Activate
|
||||
</button>
|
||||
)}
|
||||
<button className="btn btn-danger" style={{ fontSize: '11px', padding: '4px 8px', background: 'transparent', border: '1px solid var(--color-danger)', color: 'var(--color-danger)' }} onClick={() => handleRemove(prod.id)}>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue