822 lines
30 KiB
TypeScript
822 lines
30 KiB
TypeScript
import React, { createContext, useContext, useState, useEffect } from 'react';
|
|
import { CONFIG, apiFetch } from '../config';
|
|
|
|
type Page = 'home' | 'about' | 'contact' | 'login' | 'signup' | 'profile-completion' | 'confirmation' | 'dashboard' | 'forgot-password' | 'login-otp' | 'welcome-tour';
|
|
type DashboardTab = 'overview' | 'products' | 'orders' | 'returns' | 'wallet' | 'settings' | 'barcode-generator' | 'analytics';
|
|
type DateFilter = 'year' | 'week' | 'day' | 'custom';
|
|
|
|
interface Product {
|
|
id: string;
|
|
title: string;
|
|
category: string;
|
|
price: number;
|
|
stock: number;
|
|
sku: string;
|
|
image: string;
|
|
}
|
|
|
|
interface Order {
|
|
id: string;
|
|
date: string;
|
|
item: string;
|
|
quantity: number;
|
|
customer: string;
|
|
total: number;
|
|
status: 'Ready to Ship test' | 'Ready to Ship' | 'Shipped' | 'Delivered' | 'Cancelled' | 'Pending Acceptance' | 'Rejected';
|
|
carrier: string;
|
|
tracking: string;
|
|
eta: string;
|
|
}
|
|
|
|
interface ReturnRequest {
|
|
id: string;
|
|
orderId: string;
|
|
customer: string;
|
|
item: string;
|
|
reason: string;
|
|
status: 'Pending Approval' | 'Approved' | 'Rejected' | 'In Transit';
|
|
image: string;
|
|
returningTracking: string;
|
|
}
|
|
|
|
export const SellerContext = createContext<any>(null);
|
|
|
|
export const useSeller = () => useContext(SellerContext);
|
|
|
|
export const SellerProvider: React.FC<{children: React.ReactNode}> = ({ children }) => {
|
|
const [currentPage, setCurrentPage] = useState<Page>('home')
|
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false)
|
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
|
const [activeTab, setActiveTab] = useState<'login' | 'register'>('login')
|
|
|
|
// Onboarding Status
|
|
const [isProfileComplete, setIsProfileComplete] = useState(false)
|
|
const [profileStep, setProfileStep] = useState(1)
|
|
|
|
// Registration / Onboarding Form States
|
|
const [email, setEmail] = useState('')
|
|
const [phone, setPhone] = useState('')
|
|
const [password, setPassword] = useState('')
|
|
|
|
// Step 1: Verification
|
|
const [phoneVerified, setPhoneVerified] = useState(false)
|
|
const [emailVerified, setEmailVerified] = useState(false)
|
|
const [phoneOtpSent, setPhoneOtpSent] = useState(false)
|
|
const [emailOtpSent, setEmailOtpSent] = useState(false)
|
|
const [enteredPhoneOtp, setEnteredPhoneOtp] = useState('')
|
|
const [enteredEmailOtp, setEnteredEmailOtp] = useState('')
|
|
|
|
// Step 2: Business details (moved GSTIN here)
|
|
const [gstin, setGstin] = useState('29AAAAA1111A1Z1')
|
|
const [isGstinVerified, setIsGstinVerified] = useState(false)
|
|
const [aadharFile, setAadharFile] = useState<string | null>(null)
|
|
const [panFile, setPanFile] = useState<string | null>(null)
|
|
const [aadharS3Key, setAadharS3Key] = useState<string | null>(null)
|
|
const [panS3Key, setPanS3Key] = useState<string | null>(null)
|
|
|
|
// New onboarding customization states
|
|
const [businessType, setBusinessType] = useState<'registered_company' | 'self_help_group' | 'individual_maker'>('registered_company')
|
|
const [storeSlug, setStoreSlug] = useState('')
|
|
const [supportEmail, setSupportEmail] = useState('')
|
|
const [supportPhone, setSupportPhone] = useState('')
|
|
const [selectedCategories, setSelectedCategories] = useState<string[]>([])
|
|
|
|
// Step 3: Store and Location details
|
|
const [storeName, setStoreName] = useState('My Artisan Handloom')
|
|
const [storeLogo, setStoreLogo] = useState<string | null>(null)
|
|
const [logoS3Key, setLogoS3Key] = useState<string | null>(null)
|
|
const [businessBio, setBusinessBio] = useState('Traditional weaving and local sustainable designs.')
|
|
const [address, setAddress] = useState({
|
|
street: '123 Handloom Lane',
|
|
city: 'Textile Town',
|
|
state: 'Karnataka',
|
|
pincode: '560001'
|
|
})
|
|
const [mapCoordinates, setMapCoordinates] = useState({ lat: 12.9716, lng: 77.5946 })
|
|
|
|
// --- Password visibility, confirm password and policy states ---
|
|
const [showLoginPass, setShowLoginPass] = useState(false)
|
|
const [showSignupPass, setShowSignupPass] = useState(false)
|
|
const [showSignupConfirmPass, setShowSignupConfirmPass] = useState(false)
|
|
const [confirmPassword, setConfirmPassword] = useState('')
|
|
const [policyAccepted, setPolicyAccepted] = useState(false)
|
|
|
|
// --- Password Reset Page States ---
|
|
const [resetEmail, setResetEmail] = useState('')
|
|
const [resetOtpSent, setResetOtpSent] = useState(false)
|
|
const [resetOtp, setResetOtp] = useState('')
|
|
const [resetPassword, setResetPassword] = useState('')
|
|
|
|
// --- OTP Login States ---
|
|
const [otpLoginPhone, setOtpLoginPhone] = useState('')
|
|
const [otpLoginSent, setOtpLoginSent] = useState(false)
|
|
const [otpLoginCode, setOtpLoginCode] = useState('')
|
|
|
|
// --- Barcode Generator States ---
|
|
const [barcodeProductSku, setBarcodeProductSku] = useState('')
|
|
const [barcodeGenerated, setBarcodeGenerated] = useState(false)
|
|
|
|
// --- Active Dashboard States ---
|
|
const [dashTab, setDashTab] = useState<DashboardTab>('overview')
|
|
const [dateFilter, setDateFilter] = useState<DateFilter>('year')
|
|
const [customDates, setCustomDates] = useState({ start: '2026-06-01', end: '2026-06-15' })
|
|
|
|
// Lists
|
|
const [products, setProducts] = useState<Product[]>([])
|
|
const [orders, setOrders] = useState<Order[]>([])
|
|
const [returns, setReturns] = useState<ReturnRequest[]>([])
|
|
|
|
// Forms & Editing
|
|
const [productForm, setProductForm] = useState({
|
|
id: '',
|
|
title: '',
|
|
category: 'Apparel',
|
|
price: 0,
|
|
stock: 0,
|
|
sku: '',
|
|
image: 'https://images.unsplash.com/photo-1544022613-e87ca75a784a?auto=format&fit=crop&q=80&w=100'
|
|
})
|
|
const [isEditingProduct, setIsEditingProduct] = useState(false)
|
|
|
|
// New Heritage UI wizard states
|
|
const [productWizardStep, setProductWizardStep] = useState<number>(0) // 0: Catalog list, 1-5: Product upload wizard steps
|
|
const [productFormDetails, setProductFormDetails] = useState({
|
|
id: '',
|
|
name: '',
|
|
category: 'Textiles & Apparel',
|
|
description: '',
|
|
isGiTagged: false,
|
|
primaryImage: 'https://images.unsplash.com/photo-1544022613-e87ca75a784a?auto=format&fit=crop&q=80&w=300',
|
|
additionalViews: [] as string[],
|
|
videoUrl: '',
|
|
view360Url: '',
|
|
basePrice: '',
|
|
compareAtPrice: '',
|
|
trackInventory: true,
|
|
sku: '',
|
|
initialStock: '',
|
|
shippingProfile: 'Standard Fragile',
|
|
processingDays: 3,
|
|
packageWeight: 1.0
|
|
})
|
|
const [selectedOrderDetail, setSelectedOrderDetail] = useState<any | null>(null)
|
|
const [orderNotes, setOrderNotes] = useState<string>('')
|
|
|
|
// Payout outstanding states
|
|
const [wallet, setWallet] = useState({
|
|
outstanding: 850.00,
|
|
withdrawn: 1250.00,
|
|
history: [
|
|
{ id: 'TX-9031', date: '2026-08-01', amount: 500.00, status: 'Transferred' },
|
|
{ id: 'TX-9022', date: '2026-07-15', amount: 750.00, status: 'Transferred' }
|
|
]
|
|
})
|
|
const [withdrawAmount, setWithdrawAmount] = useState('')
|
|
|
|
// Load data from backend on mount or when profile is complete / logged in
|
|
useEffect(() => {
|
|
const token = localStorage.getItem('access_token');
|
|
if (token) {
|
|
apiFetch(`${CONFIG.apiBaseUrl}/api/profile/`)
|
|
.then(res => {
|
|
if (res.ok) return res.json();
|
|
throw new Error('Session expired');
|
|
})
|
|
.then(user => {
|
|
if (user && user.profile) {
|
|
const profile = user.profile;
|
|
setPhone(profile.phone || '');
|
|
setPhoneVerified(profile.phone_verified || false);
|
|
setEmail(user.email || '');
|
|
setEmailVerified(profile.email_verified || false);
|
|
setGstin(profile.gstin || '29AAAAA1111A1Z1');
|
|
setIsGstinVerified(profile.is_gstin_verified || false);
|
|
setAadharFile(profile.aadhar_file || null);
|
|
setPanFile(profile.pan_file || null);
|
|
setAadharS3Key(profile.aadhar_s3_key || null);
|
|
setPanS3Key(profile.pan_s3_key || null);
|
|
|
|
setBusinessType(profile.business_type || 'registered_company');
|
|
setStoreSlug(profile.store_slug || '');
|
|
setSupportEmail(profile.support_email || '');
|
|
setSupportPhone(profile.support_phone || '');
|
|
setSelectedCategories(profile.categories || []);
|
|
|
|
setStoreName(profile.store_name || 'My Artisan Handloom');
|
|
setStoreLogo(profile.store_logo || null);
|
|
setLogoS3Key(profile.logo_s3_key || null);
|
|
setBusinessBio(profile.business_bio || 'Traditional weaving and local sustainable designs.');
|
|
setAddress({
|
|
street: profile.street || '123 Handloom Lane',
|
|
city: profile.city || 'Textile Town',
|
|
state: profile.state || 'Karnataka',
|
|
pincode: profile.pincode || '560001'
|
|
});
|
|
if (profile.latitude && profile.longitude) {
|
|
setMapCoordinates({ lat: Number(profile.latitude), lng: Number(profile.longitude) });
|
|
}
|
|
|
|
const step = profile.onboarding_step;
|
|
if (step >= 4) {
|
|
setIsProfileComplete(true);
|
|
setCurrentPage('dashboard');
|
|
} else {
|
|
setProfileStep(step);
|
|
setIsProfileComplete(false);
|
|
setCurrentPage('profile-completion');
|
|
}
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error('Session restore failed:', err);
|
|
localStorage.removeItem('access_token');
|
|
});
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (isProfileComplete || currentPage === 'dashboard') {
|
|
// Fetch Products
|
|
apiFetch(`${CONFIG.apiBaseUrl}/api/products/`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (Array.isArray(data)) setProducts(data);
|
|
else if (data && Array.isArray(data.results)) setProducts(data.results);
|
|
})
|
|
.catch(err => console.error('Error fetching products:', err));
|
|
|
|
// Fetch Orders
|
|
apiFetch(`${CONFIG.apiBaseUrl}/api/orders/`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (Array.isArray(data)) setOrders(data);
|
|
else if (data && Array.isArray(data.results)) setOrders(data.results);
|
|
})
|
|
.catch(err => console.error('Error fetching orders:', err));
|
|
|
|
// Fetch Returns
|
|
apiFetch(`${CONFIG.apiBaseUrl}/api/returns/`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (Array.isArray(data)) setReturns(data);
|
|
else if (data && Array.isArray(data.results)) setReturns(data.results);
|
|
})
|
|
.catch(err => console.error('Error fetching returns:', err));
|
|
|
|
// Fetch Wallet
|
|
apiFetch(`${CONFIG.apiBaseUrl}/api/wallet/`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data) setWallet(data);
|
|
})
|
|
.catch(err => console.error('Error fetching wallet:', err));
|
|
}
|
|
}, [isProfileComplete, currentPage]);
|
|
|
|
const uploadDocument = async (file: File, fileType: string) => {
|
|
try {
|
|
const response = await apiFetch(`${CONFIG.apiBaseUrl}/api/profile/presigned-url/`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ file_type: fileType, content_type: file.type })
|
|
});
|
|
if (!response.ok) throw new Error('Failed to get presigned URL');
|
|
const data = await response.json();
|
|
|
|
// Simulate file upload PUT to mock presigned_url
|
|
await fetch(data.presigned_url, {
|
|
method: 'PUT',
|
|
body: file,
|
|
headers: { 'Content-Type': file.type }
|
|
}).catch(err => console.log('Mock S3 upload:', err));
|
|
|
|
return data.s3_key;
|
|
} catch (err) {
|
|
console.error(err);
|
|
return `suppliers/default/${fileType}.jpg`;
|
|
}
|
|
};
|
|
|
|
const handleAcceptOrder = (id: string) => {
|
|
apiFetch(`${CONFIG.apiBaseUrl}/api/orders/${id}/accept/`, {
|
|
method: 'POST'
|
|
})
|
|
.then(res => res.json())
|
|
.then(updatedOrder => {
|
|
setOrders(orders.map(o => o.id === updatedOrder.id ? updatedOrder : o))
|
|
alert(`Order accepted successfully!`)
|
|
})
|
|
.catch(err => console.error(err))
|
|
}
|
|
|
|
const handleRejectOrder = (id: string) => {
|
|
apiFetch(`${CONFIG.apiBaseUrl}/api/orders/${id}/reject/`, {
|
|
method: 'POST'
|
|
})
|
|
.then(res => res.json())
|
|
.then(updatedOrder => {
|
|
setOrders(orders.map(o => o.id === updatedOrder.id ? updatedOrder : o))
|
|
alert(`Order rejected.`)
|
|
})
|
|
.catch(err => console.error(err))
|
|
}
|
|
|
|
// Bulk Upload
|
|
const [bulkLog, setBulkLog] = useState<string[]>([])
|
|
const [isParsingBulk, setIsParsingBulk] = useState(false)
|
|
const [bulkCsvFile, setBulkCsvFile] = useState<File | null>(null)
|
|
const [bulkZipFile, setBulkZipFile] = useState<File | null>(null)
|
|
|
|
// GSTIN verification simulator
|
|
const handleVerifyGstin = () => {
|
|
if (!gstin.trim() || gstin.length !== 15) {
|
|
alert('Invalid GSTIN length. Must be 15 chars.');
|
|
return;
|
|
}
|
|
apiFetch(`${CONFIG.apiBaseUrl}/api/profile/submit-gstin/`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ gstin })
|
|
})
|
|
.then(res => {
|
|
if (!res.ok) throw new Error('Failed to verify GSTIN.');
|
|
return res.json();
|
|
})
|
|
.then(data => {
|
|
if (data.verified) {
|
|
setIsGstinVerified(true);
|
|
}
|
|
})
|
|
.catch(err => {
|
|
alert(err.message);
|
|
});
|
|
}
|
|
|
|
// Handle Logo Upload simulation
|
|
const handleLogoChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0]
|
|
if (file) {
|
|
const reader = new FileReader()
|
|
reader.onloadend = () => {
|
|
setStoreLogo(reader.result as string)
|
|
}
|
|
reader.readAsDataURL(file)
|
|
|
|
const s3Key = await uploadDocument(file, 'logo');
|
|
setLogoS3Key(s3Key);
|
|
}
|
|
}
|
|
|
|
// Route/Navigation Guard: If profile is not complete, redirect to profile-completion
|
|
const navigateTo = (page: Page, forceComplete: boolean = false) => {
|
|
const publicPages: Page[] = ['home', 'about', 'contact', 'login', 'signup', 'forgot-password', 'login-otp']
|
|
const complete = isProfileComplete || forceComplete
|
|
|
|
if (!complete && !publicPages.includes(page) && page !== 'profile-completion') {
|
|
alert('Access Denied: Please complete your supplier profile first!')
|
|
setCurrentPage('profile-completion')
|
|
} else {
|
|
setCurrentPage(page)
|
|
}
|
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
|
}
|
|
|
|
const handleLogout = () => {
|
|
apiFetch(`${CONFIG.apiBaseUrl}/api/auth/logout/`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
})
|
|
.catch(err => console.error('Error logging out:', err))
|
|
.finally(() => {
|
|
localStorage.removeItem('access_token')
|
|
// Reset all onboarding & profile states to their defaults
|
|
setIsProfileComplete(false)
|
|
setProfileStep(1)
|
|
setEmail('')
|
|
setPhone('')
|
|
setPassword('')
|
|
setConfirmPassword('')
|
|
setPolicyAccepted(false)
|
|
|
|
setPhoneVerified(false)
|
|
setEmailVerified(false)
|
|
setPhoneOtpSent(false)
|
|
setEmailOtpSent(false)
|
|
setEnteredPhoneOtp('')
|
|
setEnteredEmailOtp('')
|
|
|
|
setGstin('29AAAAA1111A1Z1')
|
|
setIsGstinVerified(false)
|
|
setAadharFile(null)
|
|
setPanFile(null)
|
|
setAadharS3Key(null)
|
|
setPanS3Key(null)
|
|
|
|
setStoreName('My Artisan Handloom')
|
|
setStoreLogo(null)
|
|
setLogoS3Key(null)
|
|
setBusinessBio('Traditional weaving and local sustainable designs.')
|
|
setAddress({
|
|
street: '123 Handloom Lane',
|
|
city: 'Textile Town',
|
|
state: 'Karnataka',
|
|
pincode: '560001'
|
|
})
|
|
setMapCoordinates({ lat: 12.9716, lng: 77.5946 })
|
|
|
|
navigateTo('home', true)
|
|
})
|
|
}
|
|
|
|
|
|
const handleRegisterSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
|
|
// Email Validation
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
if (!emailRegex.test(email)) {
|
|
alert('Please enter a valid email address.');
|
|
return;
|
|
}
|
|
|
|
// Phone Validation
|
|
const phoneRegex = /^[6-9]\d{9}$/;
|
|
if (!phoneRegex.test(phone)) {
|
|
alert('Please enter a valid 10-digit mobile number starting with 6, 7, 8, or 9.');
|
|
return;
|
|
}
|
|
|
|
// Password Validation
|
|
if (password.length < 8) {
|
|
alert('Password must be at least 8 characters long.');
|
|
return;
|
|
}
|
|
if (password !== confirmPassword) {
|
|
alert('Passwords do not match.');
|
|
return;
|
|
}
|
|
|
|
// Policy Validation
|
|
if (!policyAccepted) {
|
|
alert('You must accept the Terms of Service and Privacy Policy.');
|
|
return;
|
|
}
|
|
|
|
apiFetch(`${CONFIG.apiBaseUrl}/api/auth/register/`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
username: email,
|
|
email: email,
|
|
phone: phone,
|
|
password: password,
|
|
confirm_password: confirmPassword,
|
|
policy_accepted: policyAccepted
|
|
})
|
|
})
|
|
.then(res => {
|
|
if (!res.ok) throw new Error('Registration failed. Username/email might already be taken.');
|
|
return res.json();
|
|
})
|
|
.then(() => {
|
|
// Auto login after signup
|
|
apiFetch(`${CONFIG.apiBaseUrl}/api/auth/login/`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username: email, password: password })
|
|
})
|
|
.then(res => {
|
|
if (!res.ok) throw new Error('Auto-login failed.');
|
|
return res.json();
|
|
})
|
|
.then(data => {
|
|
localStorage.setItem('access_token', data.access_token);
|
|
navigateTo('profile-completion')
|
|
})
|
|
})
|
|
.catch(err => {
|
|
alert(err.message);
|
|
});
|
|
}
|
|
|
|
const handleLoginSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
|
|
if (!email.trim()) {
|
|
alert('Please enter your email or phone number.');
|
|
return;
|
|
}
|
|
|
|
apiFetch(`${CONFIG.apiBaseUrl}/api/auth/login/`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username: email, password: password })
|
|
})
|
|
.then(res => {
|
|
if (!res.ok) throw new Error('Invalid credentials.');
|
|
return res.json();
|
|
})
|
|
.then(data => {
|
|
localStorage.setItem('access_token', data.access_token);
|
|
if (data.user && data.user.profile) {
|
|
const profile = data.user.profile;
|
|
setPhone(profile.phone || '');
|
|
setPhoneVerified(profile.phone_verified || false);
|
|
setEmail(data.user.email || '');
|
|
setEmailVerified(profile.email_verified || false);
|
|
setGstin(profile.gstin || '29AAAAA1111A1Z1');
|
|
setIsGstinVerified(profile.is_gstin_verified || false);
|
|
setAadharFile(profile.aadhar_file || null);
|
|
setPanFile(profile.pan_file || null);
|
|
setAadharS3Key(profile.aadhar_s3_key || null);
|
|
setPanS3Key(profile.pan_s3_key || null);
|
|
setStoreName(profile.store_name || 'My Artisan Handloom');
|
|
setStoreLogo(profile.store_logo || null);
|
|
setLogoS3Key(profile.logo_s3_key || null);
|
|
setBusinessBio(profile.business_bio || 'Traditional weaving and local sustainable designs.');
|
|
setAddress({
|
|
street: profile.street || '123 Handloom Lane',
|
|
city: profile.city || 'Textile Town',
|
|
state: profile.state || 'Karnataka',
|
|
pincode: profile.pincode || '560001'
|
|
});
|
|
if (profile.latitude && profile.longitude) {
|
|
setMapCoordinates({ lat: Number(profile.latitude), lng: Number(profile.longitude) });
|
|
}
|
|
|
|
const step = profile.onboarding_step;
|
|
if (step >= 4) {
|
|
setIsProfileComplete(true);
|
|
navigateTo('dashboard', true);
|
|
} else {
|
|
setProfileStep(step);
|
|
setIsProfileComplete(false);
|
|
navigateTo('profile-completion');
|
|
}
|
|
} else {
|
|
setIsProfileComplete(false);
|
|
navigateTo('profile-completion');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
alert(err.message);
|
|
});
|
|
}
|
|
|
|
const saveProfileBackend = (isComplete: boolean) => {
|
|
return apiFetch(`${CONFIG.apiBaseUrl}/api/profile/`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
phone: phone,
|
|
phone_verified: phoneVerified,
|
|
email_verified: emailVerified,
|
|
gstin: gstin,
|
|
is_gstin_verified: isGstinVerified,
|
|
business_type: businessType,
|
|
store_slug: storeSlug,
|
|
support_email: supportEmail,
|
|
support_phone: supportPhone,
|
|
categories: selectedCategories,
|
|
aadhar_file: aadharFile,
|
|
pan_file: panFile,
|
|
aadhar_s3_key: aadharS3Key || 'suppliers/default/aadhar.pdf',
|
|
pan_s3_key: panS3Key || 'suppliers/default/pan.pdf',
|
|
store_name: storeName,
|
|
store_logo: storeLogo || 'https://images.unsplash.com/photo-1513519245088-0e12902e5a38?auto=format&fit=crop&q=80&w=800',
|
|
logo_s3_key: logoS3Key || 'suppliers/default/logo.jpg',
|
|
business_bio: businessBio,
|
|
street: address.street,
|
|
city: address.city,
|
|
state: address.state,
|
|
pincode: address.pincode,
|
|
latitude: mapCoordinates.lat,
|
|
longitude: mapCoordinates.lng,
|
|
is_profile_complete: isComplete
|
|
})
|
|
})
|
|
.then(res => {
|
|
if (!res.ok) throw new Error('Failed to update profile.');
|
|
return res.json();
|
|
});
|
|
};
|
|
|
|
const handleProfileSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (profileStep === 2) {
|
|
setIsGstinVerified(true)
|
|
}
|
|
const isLastStep = profileStep === 6;
|
|
saveProfileBackend(isLastStep)
|
|
.then(() => {
|
|
if (!isLastStep) {
|
|
setProfileStep(prev => prev + 1)
|
|
} else {
|
|
setProfileStep(7);
|
|
}
|
|
})
|
|
.catch(err => {
|
|
alert(err.message);
|
|
});
|
|
}
|
|
|
|
// --- Dashboard Logic Actions ---
|
|
|
|
// Product creation/modification
|
|
const handleSaveProduct = (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
const url = isEditingProduct
|
|
? `${CONFIG.apiBaseUrl}/api/products/${productForm.id}/`
|
|
: `${CONFIG.apiBaseUrl}/api/products/`
|
|
const method = isEditingProduct ? 'PUT' : 'POST'
|
|
|
|
apiFetch(url, {
|
|
method: method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
title: productForm.title,
|
|
category: productForm.category,
|
|
price: String(productForm.price),
|
|
stock: Number(productForm.stock),
|
|
sku: productForm.sku || `PROD-${Date.now().toString().slice(-6)}`,
|
|
image: productForm.image
|
|
})
|
|
})
|
|
.then(res => res.json())
|
|
.then(() => {
|
|
// Refresh products from backend
|
|
apiFetch(`${CONFIG.apiBaseUrl}/api/products/`)
|
|
.then(r => r.json())
|
|
.then(prods => {
|
|
if (Array.isArray(prods)) setProducts(prods);
|
|
else if (prods && Array.isArray(prods.results)) setProducts(prods.results);
|
|
})
|
|
setIsEditingProduct(false)
|
|
alert(isEditingProduct ? 'Product modified successfully!' : 'Product uploaded successfully!')
|
|
})
|
|
.catch(err => console.error(err))
|
|
|
|
// reset form
|
|
setProductForm({ id: '', title: '', category: 'Apparel', price: 0, stock: 0, sku: '', image: 'https://images.unsplash.com/photo-1544022613-e87ca75a784a?auto=format&fit=crop&q=80&w=100' })
|
|
}
|
|
|
|
const handleEditClick = (p: Product) => {
|
|
setProductForm(p)
|
|
setIsEditingProduct(true)
|
|
}
|
|
|
|
const handleDeleteProduct = (id: string) => {
|
|
if (confirm('Are you sure you want to delete this listing?')) {
|
|
apiFetch(`${CONFIG.apiBaseUrl}/api/products/${id}/`, {
|
|
method: 'DELETE'
|
|
})
|
|
.then(() => {
|
|
setProducts(products.filter(p => p.id !== id))
|
|
})
|
|
.catch(err => console.error(err))
|
|
}
|
|
}
|
|
|
|
const handleBulkUploadSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!bulkCsvFile) {
|
|
alert("Please select a CSV file to upload.");
|
|
return;
|
|
}
|
|
setIsParsingBulk(true);
|
|
setBulkLog(['Uploading files to server...', 'Parsing CSV data and extracting ZIP images...']);
|
|
|
|
const formData = new FormData();
|
|
formData.append('csv_file', bulkCsvFile);
|
|
if (bulkZipFile) {
|
|
formData.append('zip_file', bulkZipFile);
|
|
}
|
|
|
|
try {
|
|
const token = localStorage.getItem('access_token');
|
|
const response = await fetch(`${CONFIG.apiBaseUrl}/api/products/bulk-upload/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
|
},
|
|
body: formData
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (!response.ok) {
|
|
throw new Error(data.error || 'Failed to complete bulk upload.');
|
|
}
|
|
|
|
setBulkLog([
|
|
'CSV file parsed successfully.',
|
|
`Extracted and matched images for SKUs from ZIP file.`,
|
|
`Successfully added ${data.products?.length || 0} product listings!`,
|
|
]);
|
|
|
|
// Refresh product list
|
|
const prodRes = await apiFetch(`${CONFIG.apiBaseUrl}/api/products/`);
|
|
const prods = await prodRes.json();
|
|
if (Array.isArray(prods)) {
|
|
setProducts(prods);
|
|
} else if (prods && Array.isArray(prods.results)) {
|
|
setProducts(prods.results);
|
|
}
|
|
setBulkCsvFile(null);
|
|
setBulkZipFile(null);
|
|
} catch (err: any) {
|
|
setBulkLog(prev => [...prev, `Error: ${err.message}`]);
|
|
} finally {
|
|
setIsParsingBulk(false);
|
|
}
|
|
};
|
|
|
|
// Wallet outstanding requests
|
|
const handleWithdrawRequest = (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
const amount = Number(withdrawAmount)
|
|
if (isNaN(amount) || amount <= 0) {
|
|
alert('Please enter a valid amount')
|
|
return
|
|
}
|
|
|
|
apiFetch(`${CONFIG.apiBaseUrl}/api/wallet/withdraw/`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ amount: String(amount) })
|
|
})
|
|
.then(res => {
|
|
if (!res.ok) throw new Error('Insufficient funds or invalid request');
|
|
return res.json();
|
|
})
|
|
.then(updatedWallet => {
|
|
setWallet(updatedWallet)
|
|
setWithdrawAmount('')
|
|
alert(`Payout of $${amount} successfully transferred!`)
|
|
})
|
|
.catch(err => {
|
|
alert(err.message)
|
|
})
|
|
}
|
|
|
|
// Returns actions
|
|
const handleReturnAction = (id: string, action: 'Approved' | 'Rejected') => {
|
|
apiFetch(`${CONFIG.apiBaseUrl}/api/returns/${id}/action/`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: action })
|
|
})
|
|
.then(res => res.json())
|
|
.then(updatedReturn => {
|
|
setReturns(returns.map(ret => ret.id === id ? updatedReturn : ret))
|
|
alert(`Return request ${action.toLowerCase()}!`)
|
|
})
|
|
.catch(err => console.error(err))
|
|
}
|
|
|
|
// Compute realtime metrics based on state loaded from backend
|
|
const computeRealtimeMetrics = () => {
|
|
const totalSales = orders
|
|
.filter(o => o.status !== 'Cancelled' && o.status !== 'Rejected')
|
|
.reduce((sum, o) => sum + Number(o.total), 0)
|
|
|
|
const totalEarned = Number(wallet.outstanding) + Number(wallet.withdrawn)
|
|
const totalStock = products.reduce((sum, p) => sum + Number(p.stock), 0)
|
|
const totalReturns = returns.length
|
|
|
|
const isLoggedIn = currentPage === 'dashboard' || currentPage === 'profile-completion' || currentPage === 'welcome-tour' || isProfileComplete
|
|
|
|
if (isLoggedIn) {
|
|
return {
|
|
totalSales,
|
|
totalEarned,
|
|
stockDetails: totalStock,
|
|
returnedItems: totalReturns,
|
|
chartValues: orders.length > 0
|
|
? orders.map(o => Number(o.total))
|
|
: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
|
}
|
|
}
|
|
|
|
return {
|
|
totalSales: 45280.00,
|
|
totalEarned: 38488.00,
|
|
stockDetails: 342,
|
|
returnedItems: 12,
|
|
chartValues: CONFIG.dashboardData.filters[dateFilter]?.chartValues || [30, 45, 35, 60, 50, 75, 65, 80, 70, 95, 90, 110]
|
|
}
|
|
}
|
|
|
|
const selectedMetrics = computeRealtimeMetrics()
|
|
|
|
|
|
|
|
const contextValue = {
|
|
setBusinessBio, email, resetPassword, setLogoS3Key, setStoreName, setShowLoginPass, storeName, otpLoginSent, setOtpLoginSent, aadharS3Key, logoS3Key, customDates, setProductFormDetails, handleBulkUploadSubmit, setMapCoordinates, setAadharFile, gstin, mapCoordinates, setIsGstinVerified, enteredPhoneOtp, address, setEmailVerified, dashTab, selectedOrderDetail, setShowSignupConfirmPass, handleRegisterSubmit, setBulkCsvFile, confirmPassword, navigateTo, setResetPassword, orderNotes, setOrders, handleRejectOrder, barcodeProductSku, setEnteredEmailOtp, setIsEditingProduct, setBulkLog, isMobileMenuOpen, resetOtp, productWizardStep, setCustomDates, setConfirmPassword, isGstinVerified, bulkCsvFile, setAddress, storeLogo, handleProfileSubmit, setPhoneOtpSent, setOrderNotes, handleDeleteProduct, setWallet, policyAccepted, emailOtpSent, setResetOtpSent, setGstin, phoneOtpSent, showLoginPass, showSignupConfirmPass, setPhoneVerified, aadharFile, setDateFilter, activeTab, computeRealtimeMetrics, setIsParsingBulk, handleLogout, panS3Key, setCurrentPage, selectedCategories, setEmailOtpSent, showSignupPass, resetEmail, handleLogoChange, dateFilter, setShowSignupPass, businessBio, setWithdrawAmount, phoneVerified, currentPage, setResetOtp, enteredEmailOtp, setEnteredPhoneOtp, setPanFile, setIsSidebarOpen, setSelectedOrderDetail, handleAcceptOrder, saveProfileBackend, supportPhone, handleEditClick, setStoreLogo, setEmail, password, setOtpLoginPhone, bulkLog, businessType, setPanS3Key, setProducts, setSupportEmail, setOtpLoginCode, withdrawAmount, handleSaveProduct, setStoreSlug, setBarcodeProductSku, setProductForm, setBulkZipFile, productFormDetails, setIsProfileComplete, otpLoginPhone, orders, setActiveTab, bulkZipFile, isSidebarOpen, setBarcodeGenerated, setBusinessType, handleReturnAction, setSupportPhone, barcodeGenerated, products, productForm, setProductWizardStep, resetOtpSent, emailVerified, panFile, isParsingBulk, setDashTab, setReturns, setPassword, handleWithdrawRequest, uploadDocument, setPolicyAccepted, isEditingProduct, setIsMobileMenuOpen, profileStep, isProfileComplete, setSelectedCategories, storeSlug, phone, setPhone, setAadharS3Key, handleVerifyGstin, setProfileStep, returns, handleLoginSubmit, setResetEmail, wallet, otpLoginCode, supportEmail
|
|
};
|
|
|
|
return (
|
|
<SellerContext.Provider value={contextValue}>
|
|
{children}
|
|
</SellerContext.Provider>
|
|
);
|
|
};
|