feat: implement supplier inventory management and product creation workflow
This commit is contained in:
parent
b28c438a4e
commit
d2d7dc2a06
5 changed files with 65 additions and 4 deletions
|
|
@ -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
|
||||
})));
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue