feat: implement supplier inventory management and product creation workflow
All checks were successful
Beta CI/CD Pipeline / build-and-test (push) Successful in 1m37s
Beta CI/CD Pipeline / deploy-beta (push) Successful in 45s

This commit is contained in:
vickytechkey 2026-10-02 13:59:21 +05:30
parent b28c438a4e
commit d2d7dc2a06
5 changed files with 65 additions and 4 deletions

View file

@ -160,6 +160,7 @@ export const SellerProvider: React.FC<{ children: React.ReactNode }> = ({ childr
mrp: parseFloat(p.compare_at_price) || ((parseFloat(p.price) || 0) * 1.2),
stock: p.stock || 0,
status: p.stock === 0 ? 'out_of_stock' : p.stock < 5 ? 'low_stock' : 'active',
approvalStatus: (p.status && (p.status.toLowerCase() === 'approved' || p.status.toLowerCase() === 'active')) ? 'approved' : (p.status === 'rejected' ? 'rejected' : 'approved'),
image: p.image || '/images/placeholder.jpg',
salesCount: 0
})));

View file

@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useSeller } from '../../context/SellerContext';
import { api } from '../../services/api';
import {
ArrowRight,
ArrowLeft,
@ -76,15 +77,30 @@ export const AddProductPage: React.FC = () => {
const [autoSaved, setAutoSaved] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
// 1. Instant local preview
const reader = new FileReader();
reader.onloadend = () => {
setImage(reader.result as string);
simulateAutoSave();
};
reader.readAsDataURL(file);
// 2. Upload to S3 storage bucket
try {
const presignedData = await api.getPresignedUrl('product_cover', file.type || 'image/jpeg');
if (presignedData && presignedData.presigned_url) {
await api.uploadToS3(presignedData.presigned_url, file);
console.log('Successfully uploaded product cover to S3:', presignedData.s3_key);
if (presignedData.s3_key) {
setImage(`https://betasupplierdocumentstorage.s3.ap-south-2.amazonaws.com/${presignedData.s3_key}`);
}
}
} catch (err) {
console.warn('S3 direct upload notice (using local preview fallback):', err);
}
}
};
const { addProduct } = useSeller();

View file

@ -16,7 +16,10 @@ import {
ChevronLeft,
ChevronRight,
Edit3,
ShieldCheck
ShieldCheck,
CheckCircle2,
Clock,
XCircle
} from 'lucide-react';
const HEALTH_CONFIG: Record<string, { label: string; bg: string; color: string; dot: string }> = {
@ -222,7 +225,7 @@ export const InventoryPage: React.FC = () => {
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ backgroundColor: '#FAFAFA', borderBottom: '1px solid #E5E7EB' }}>
{['Craft Product & SKU', 'Category & GI Tag', 'Pricing', 'Stock Qty', 'Health', 'Actions'].map(h => (
{['Craft Product & SKU', 'Category & GI Tag', 'Pricing', 'Stock Qty', 'Approval Status', 'Health', 'Actions'].map(h => (
<th key={h} style={{ padding: '10px 16px', fontSize: '0.68rem', fontWeight: 700, color: '#6B7280', letterSpacing: '0.04em', textTransform: 'uppercase', textAlign: 'left', whiteSpace: 'nowrap' }}>{h}</th>
))}
</tr>
@ -285,6 +288,25 @@ export const InventoryPage: React.FC = () => {
<div style={{ fontSize: '0.68rem', color: '#9CA3AF', marginTop: '4px' }}>{item.salesCount} sold all-time</div>
</td>
<td style={{ padding: '14px 16px', verticalAlign: 'middle', minWidth: '140px' }}>
{((item as any).approvalStatus === 'pending' || (item as any).status === 'pending') ? (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '5px', backgroundColor: '#FEF3C7', color: '#92400E', fontSize: '0.72rem', fontWeight: 700, borderRadius: '9999px', padding: '4px 10px' }}>
<Clock size={12} color="#D97706" />
Pending Approval
</span>
) : (item as any).approvalStatus === 'rejected' ? (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '5px', backgroundColor: '#FEE2E2', color: '#991B1B', fontSize: '0.72rem', fontWeight: 700, borderRadius: '9999px', padding: '4px 10px' }}>
<XCircle size={12} color="#DC2626" />
Rejected
</span>
) : (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '5px', backgroundColor: '#E6F4EA', color: '#166534', fontSize: '0.72rem', fontWeight: 700, borderRadius: '9999px', padding: '4px 10px' }}>
<CheckCircle2 size={12} color="#16A34A" />
Approved
</span>
)}
</td>
<td style={{ padding: '14px 16px', verticalAlign: 'middle', minWidth: '120px' }}>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '5px', backgroundColor: hConf.bg, color: hConf.color, fontSize: '0.72rem', fontWeight: 700, borderRadius: '9999px', padding: '4px 10px' }}>
<span style={{ width: '6px', height: '6px', borderRadius: '50%', backgroundColor: hConf.dot, flexShrink: 0 }} />
@ -314,7 +336,7 @@ export const InventoryPage: React.FC = () => {
);
}) : (
<tr>
<td colSpan={6} style={{ padding: '3rem', textAlign: 'center' }}>
<td colSpan={7} style={{ padding: '3rem', textAlign: 'center' }}>
<AlertCircle size={32} color="#D1D5DB" style={{ marginBottom: '8px' }} />
<p style={{ fontSize: '0.88rem', color: '#6B7280' }}>No matching craft SKUs found.</p>
</td>

View file

@ -20,6 +20,27 @@ const getAuthHeaders = (isMultipart = false) => {
};
export const api = {
// S3 Presigned Uploads
getPresignedUrl: async (fileType: string, contentType: string = 'image/jpeg') => {
const res = await fetch(`${BASE_URL}/profile/presigned-url/`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({ file_type: fileType, content_type: contentType })
});
if (!res.ok) throw new Error('Failed to get presigned upload URL');
return res.json();
},
uploadToS3: async (uploadUrl: string, file: File) => {
const res = await fetch(uploadUrl, {
method: 'PUT',
headers: { 'Content-Type': file.type || 'image/jpeg' },
body: file
});
if (!res.ok) throw new Error('Failed to upload file to S3');
return res;
},
// Auth
login: async (username: string, password?: string, otpCode?: string, otpType?: string) => {
const payload: any = {

View file

@ -70,6 +70,7 @@ export interface InventoryItem {
mrp: number;
stock: number;
status: 'active' | 'low_stock' | 'out_of_stock';
approvalStatus?: 'approved' | 'pending' | 'rejected';
image: string;
salesCount: number;
}