Compare commits
8 commits
3bc0c3fe67
...
f3d428784c
| Author | SHA1 | Date | |
|---|---|---|---|
| f3d428784c | |||
| 9d38d8a053 | |||
| e79c7369bc | |||
| 04632f1ca3 | |||
| a0299efa5f | |||
| 2ba073b9b0 | |||
| 9bb761f176 | |||
| 6906d30ed0 |
9 changed files with 700 additions and 131 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,5 +1,6 @@
|
|||
# Logs
|
||||
logs
|
||||
html
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
|
|
|||
62
cypress/e2e/api.cy.ts
Normal file
62
cypress/e2e/api.cy.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
describe('Seller Central Django Backend API E2E Tests', () => {
|
||||
const backendUrl = 'http://localhost:8000';
|
||||
|
||||
it('verifies product list', () => {
|
||||
cy.request(`${backendUrl}/api/products/`).then((response) => {
|
||||
expect(response.status).to.eq(200);
|
||||
expect(response.body).to.be.an('array');
|
||||
});
|
||||
});
|
||||
|
||||
it('creates and deletes a product', () => {
|
||||
const uniqueSku = `SKU-CY-${Date.now()}`;
|
||||
cy.request('POST', `${backendUrl}/api/products/`, {
|
||||
title: 'Cypress Test Silk Scarf',
|
||||
category: 'Apparel',
|
||||
price: '55.00',
|
||||
stock: 20,
|
||||
sku: uniqueSku
|
||||
}).then((response) => {
|
||||
expect(response.status).to.eq(201);
|
||||
expect(response.body.sku).to.eq(uniqueSku);
|
||||
const prodId = response.body.id;
|
||||
|
||||
// Delete the created product
|
||||
cy.request('DELETE', `${backendUrl}/api/products/${prodId}/`).then((delRes) => {
|
||||
expect(delRes.status).to.eq(204); // django rest framework delete returns 204 No Content
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('verifies order list and acceptance', () => {
|
||||
cy.request(`${backendUrl}/api/orders/`).then((response) => {
|
||||
expect(response.status).to.eq(200);
|
||||
expect(response.body).to.be.an('array');
|
||||
expect(response.body.length).to.be.greaterThan(0);
|
||||
|
||||
const pendingOrder = response.body.find((o: any) => o.status === 'Pending Acceptance');
|
||||
if (pendingOrder) {
|
||||
cy.request('POST', `${backendUrl}/api/orders/${pendingOrder.id}/accept/`).then((acceptRes) => {
|
||||
expect(acceptRes.status).to.eq(200);
|
||||
expect(acceptRes.body.status).to.eq('Ready to Ship');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('verifies wallet summary and payout withdrawal', () => {
|
||||
cy.request(`${backendUrl}/api/wallet/`).then((response) => {
|
||||
expect(response.status).to.eq(200);
|
||||
expect(response.body).to.have.property('outstanding');
|
||||
expect(response.body).to.have.property('withdrawn');
|
||||
|
||||
const currentOutstanding = parseFloat(response.body.outstanding);
|
||||
if (currentOutstanding > 10) {
|
||||
cy.request('POST', `${backendUrl}/api/wallet/withdraw/`, { amount: '10.00' }).then((withdrawRes) => {
|
||||
expect(withdrawRes.status).to.eq(200);
|
||||
expect(parseFloat(withdrawRes.body.outstanding)).to.eq(currentOutstanding - 10);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -6,9 +6,12 @@ describe('Supplier Portal Onboarding & Feature E2E Flow', () => {
|
|||
|
||||
// 2. Registration Page
|
||||
cy.contains('Get Started Today').click();
|
||||
cy.get('input#reg-email').type('test_seller@example.com');
|
||||
const randUser = `test_seller_${Date.now()}@example.com`;
|
||||
cy.get('input#reg-email').type(randUser);
|
||||
cy.get('input#reg-phone').type('9876543210');
|
||||
cy.get('input#reg-pass').type('super_secure_pass_123');
|
||||
cy.get('input#reg-confirm-pass').type('super_secure_pass_123');
|
||||
cy.get('input#reg-policy').check();
|
||||
cy.get('button').contains('Register Business').click();
|
||||
|
||||
// 3. Step 1: Contact Verification
|
||||
|
|
@ -27,8 +30,8 @@ describe('Supplier Portal Onboarding & Feature E2E Flow', () => {
|
|||
// 4. Step 2: Tax & Identity Verification
|
||||
cy.contains('Step 2 of 3: Tax & Identity Verification').should('be.visible');
|
||||
cy.get('input#verify-gst').clear().type('29AAAAA1111A1Z1');
|
||||
cy.contains('Verify GSTIN').click();
|
||||
cy.contains('GSTIN successfully verified with government registry.').should('be.visible');
|
||||
cy.contains('Submit GSTIN').click();
|
||||
cy.contains('verification will be completed within next 24 hrs').should('be.visible');
|
||||
|
||||
// Upload mock Aadhaar & PAN files
|
||||
cy.get('input#aadhar-upload').selectFile({
|
||||
|
|
@ -63,8 +66,8 @@ describe('Supplier Portal Onboarding & Feature E2E Flow', () => {
|
|||
cy.contains('Submit Supplier Profile').click();
|
||||
|
||||
// 6. Setup Welcome Tour Stepper
|
||||
cy.contains('Welcome to Global Artisans Hub! 🎉').should('be.visible');
|
||||
cy.contains('Start Tour').click();
|
||||
cy.contains('Supplier Account Under Review ⏳').should('be.visible');
|
||||
cy.contains('Start Guided Tour 🎬').click();
|
||||
cy.contains('📦 Products & Inventory Management').should('be.visible');
|
||||
cy.contains('Next: Order Management').click();
|
||||
cy.contains('🚚 Order Acceptance Control').should('be.visible');
|
||||
|
|
@ -72,7 +75,7 @@ describe('Supplier Portal Onboarding & Feature E2E Flow', () => {
|
|||
cy.contains('🏷️ Barcode Identification & Tracking').should('be.visible');
|
||||
cy.contains('Next: Earnings & Wallet').click();
|
||||
cy.contains('💼 Wallet & Payout Withdrawals').should('be.visible');
|
||||
cy.contains('Launch Dashboard 🚀').click();
|
||||
cy.contains('Explore Dashboard 🚀').click();
|
||||
|
||||
// 7. Check Active Dashboard Tab
|
||||
cy.contains('Performance Analytics').should('be.visible');
|
||||
|
|
@ -98,4 +101,54 @@ describe('Supplier Portal Onboarding & Feature E2E Flow', () => {
|
|||
cy.contains('Print Label').should('be.visible');
|
||||
cy.contains('Download SVG').should('be.visible');
|
||||
});
|
||||
|
||||
it('enforces security constraints and validation rules (negative test cases)', () => {
|
||||
// 1. Visit Home and verify dashboard components are not in DOM (route guarding)
|
||||
cy.visit('/');
|
||||
cy.get('.dashboard-container').should('not.exist');
|
||||
cy.get('.sidebar-menu').should('not.exist');
|
||||
|
||||
// 2. Go to Registration
|
||||
cy.contains('Get Started Today').click();
|
||||
const randSecurityUser = `security_test_${Date.now()}@example.com`;
|
||||
cy.get('input#reg-email').type(randSecurityUser);
|
||||
cy.get('input#reg-phone').type('9000000000');
|
||||
cy.get('input#reg-pass').type('password123');
|
||||
cy.get('input#reg-confirm-pass').type('password123');
|
||||
cy.get('input#reg-policy').check();
|
||||
cy.get('button').contains('Register Business').click();
|
||||
|
||||
// 3. Step 1: Negative OTP validations
|
||||
cy.contains('Step 1 of 3: Contact Verification').should('be.visible');
|
||||
|
||||
// Proceed button must be disabled initially
|
||||
cy.get('button').contains('Next Step: Identity Verification').should('be.disabled');
|
||||
|
||||
// Submit invalid WhatsApp OTP
|
||||
cy.contains('Send WhatsApp OTP').click();
|
||||
cy.get('input[placeholder="Enter 123456"]').first().type('000000');
|
||||
|
||||
// Capture alert for incorrect OTP
|
||||
const alertStub = cy.stub();
|
||||
cy.on('window:alert', alertStub);
|
||||
|
||||
cy.contains('Verify Code').first().click().then(() => {
|
||||
expect(alertStub).to.have.been.calledWith('Incorrect code. Enter 123456');
|
||||
});
|
||||
cy.contains('✓ Mobile & WhatsApp Verified').should('not.exist');
|
||||
|
||||
// Correct the code to enable proceed
|
||||
cy.get('input[placeholder="Enter 123456"]').first().clear().type('123456');
|
||||
cy.contains('Verify Code').first().click();
|
||||
cy.contains('✓ Mobile & WhatsApp Verified').should('be.visible');
|
||||
|
||||
// Submit invalid Email OTP
|
||||
cy.contains('Send Email OTP').click();
|
||||
cy.get('input[placeholder="Enter 123456"]').last().type('999999');
|
||||
cy.contains('Verify Code').last().click().then(() => {
|
||||
expect(alertStub).to.have.been.calledWith('Incorrect code. Enter 123456');
|
||||
});
|
||||
cy.contains('✓ Email Verified').should('not.exist');
|
||||
cy.get('button').contains('Next Step: Identity Verification').should('be.disabled');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
79
package-lock.json
generated
79
package-lock.json
generated
|
|
@ -19,6 +19,7 @@
|
|||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.4",
|
||||
"@vitest/ui": "^4.1.10",
|
||||
"cypress": "^15.20.0",
|
||||
"jsdom": "^30.0.1",
|
||||
"oxlint": "^1.75.0",
|
||||
|
|
@ -696,6 +697,13 @@
|
|||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@polka/url": {
|
||||
"version": "1.0.0-next.29",
|
||||
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
|
||||
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz",
|
||||
|
|
@ -1233,6 +1241,28 @@
|
|||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/ui": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.10.tgz",
|
||||
"integrity": "sha512-EOUqfXHTXtpSHsyLHH40ts3Ue+hRhSGwzwzMlK0dTEOLSDYyOXLyr5JDGmHQWhN2DYI30gw6dVx3cdgM9FZl+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "4.1.10",
|
||||
"fflate": "^0.8.2",
|
||||
"flatted": "^3.4.2",
|
||||
"pathe": "^2.0.3",
|
||||
"sirv": "^3.0.2",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"tinyrainbow": "^3.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": "4.1.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "4.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",
|
||||
|
|
@ -2172,6 +2202,20 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/fflate": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
|
||||
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
|
||||
"integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/forever-agent": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||
|
|
@ -3177,6 +3221,16 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/mrmime": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
|
||||
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
|
@ -3819,6 +3873,21 @@
|
|||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/sirv": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
|
||||
"integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@polka/url": "^1.0.0-next.24",
|
||||
"mrmime": "^2.0.0",
|
||||
"totalist": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/slice-ansi": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz",
|
||||
|
|
@ -4123,6 +4192,16 @@
|
|||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/totalist": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
|
||||
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.4",
|
||||
"@vitest/ui": "^4.1.10",
|
||||
"cypress": "^15.20.0",
|
||||
"jsdom": "^30.0.1",
|
||||
"oxlint": "^1.75.0",
|
||||
|
|
|
|||
|
|
@ -2,9 +2,27 @@ import { render, screen, fireEvent } from '@testing-library/react'
|
|||
import { describe, it, expect, vi, beforeAll } from 'vitest'
|
||||
import App from './App'
|
||||
|
||||
// Mock window.scrollTo since jsdom does not implement it
|
||||
// Mock window.scrollTo and fetch since jsdom does not implement them
|
||||
beforeAll(() => {
|
||||
window.scrollTo = vi.fn()
|
||||
global.fetch = vi.fn((url) => {
|
||||
let responseData: any = {};
|
||||
if (url.includes('/api/auth/register') || url.includes('/api/auth/login')) {
|
||||
responseData = { id: 1, username: 'test_seller', email: 'test@example.com' };
|
||||
} else if (url.includes('/api/products')) {
|
||||
responseData = [];
|
||||
} else if (url.includes('/api/orders')) {
|
||||
responseData = [];
|
||||
} else if (url.includes('/api/returns')) {
|
||||
responseData = [];
|
||||
} else if (url.includes('/api/wallet')) {
|
||||
responseData = { outstanding: 850.00, withdrawn: 1250.00, transactions: [] };
|
||||
}
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(responseData)
|
||||
} as Response);
|
||||
})
|
||||
})
|
||||
|
||||
describe('Supplier Portal Onboarding & Active Dashboard Tests', () => {
|
||||
|
|
@ -49,10 +67,14 @@ describe('Supplier Portal Onboarding & Active Dashboard Tests', () => {
|
|||
const emailInput = screen.getByLabelText(/Business Email \*/i)
|
||||
const phoneInput = screen.getByLabelText(/Mobile Number \*/i)
|
||||
const passInput = screen.getByLabelText(/Create Password \*/i)
|
||||
const confirmPassInput = screen.getByLabelText(/Confirm Password \*/i)
|
||||
const policyCheckbox = screen.getByLabelText(/I accept the/i)
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: 'seller@example.com' } })
|
||||
fireEvent.change(phoneInput, { target: { value: '9876543210' } })
|
||||
fireEvent.change(passInput, { target: { value: 'password123' } })
|
||||
fireEvent.change(confirmPassInput, { target: { value: 'password123' } })
|
||||
fireEvent.click(policyCheckbox)
|
||||
|
||||
const alertMock = vi.spyOn(window, 'alert').mockImplementation(() => {})
|
||||
|
||||
|
|
@ -60,7 +82,7 @@ describe('Supplier Portal Onboarding & Active Dashboard Tests', () => {
|
|||
fireEvent.click(registerBtn)
|
||||
|
||||
// Step 1: Contact Verification
|
||||
expect(screen.getByText(/Step 1 of 3: Contact Verification/i)).toBeInTheDocument()
|
||||
expect(await screen.findByText(/Step 1 of 3: Contact Verification/i)).toBeInTheDocument()
|
||||
|
||||
const sendWhatsappBtn = screen.getByText(/Send WhatsApp OTP/i)
|
||||
fireEvent.click(sendWhatsappBtn)
|
||||
|
|
@ -87,7 +109,7 @@ describe('Supplier Portal Onboarding & Active Dashboard Tests', () => {
|
|||
|
||||
const taxInput = screen.getByLabelText(/GSTIN Number \*/i)
|
||||
fireEvent.change(taxInput, { target: { value: '29AAAAA1111A1Z1' } })
|
||||
const verifyGstinBtn = screen.getByRole('button', { name: /Verify GSTIN/i })
|
||||
const verifyGstinBtn = screen.getByRole('button', { name: /Submit GSTIN/i })
|
||||
fireEvent.click(verifyGstinBtn)
|
||||
|
||||
// Upload files
|
||||
|
|
@ -121,11 +143,11 @@ describe('Supplier Portal Onboarding & Active Dashboard Tests', () => {
|
|||
const submitBtn = screen.getByRole('button', { name: /Submit Supplier Profile/i })
|
||||
fireEvent.click(submitBtn)
|
||||
|
||||
// Welcome Page & Setup Tour
|
||||
expect(screen.getByText(/Welcome to Global Artisans Hub!/i)).toBeInTheDocument()
|
||||
// Welcome Page & Setup Tour (Account Under Review)
|
||||
expect(screen.getByText(/Supplier Account Under Review/i)).toBeInTheDocument()
|
||||
|
||||
// Complete tour steps
|
||||
const startTourBtn = screen.getByRole('button', { name: /Start Tour/i })
|
||||
const startTourBtn = screen.getByRole('button', { name: /Start Guided Tour/i })
|
||||
fireEvent.click(startTourBtn)
|
||||
|
||||
const next1 = screen.getByRole('button', { name: /Next: Order Management/i })
|
||||
|
|
@ -137,7 +159,7 @@ describe('Supplier Portal Onboarding & Active Dashboard Tests', () => {
|
|||
const next3 = screen.getByRole('button', { name: /Next: Earnings & Wallet/i })
|
||||
fireEvent.click(next3)
|
||||
|
||||
const finishBtn = screen.getByRole('button', { name: /Launch Dashboard 🚀/i })
|
||||
const finishBtn = screen.getByRole('button', { name: /Explore Dashboard 🚀/i })
|
||||
fireEvent.click(finishBtn)
|
||||
|
||||
// Redirects to Dashboard Page
|
||||
|
|
@ -147,7 +169,7 @@ describe('Supplier Portal Onboarding & Active Dashboard Tests', () => {
|
|||
alertMock.mockRestore()
|
||||
})
|
||||
|
||||
it('logs in successfully and navigates dashboard tools', () => {
|
||||
it('logs in successfully and navigates dashboard tools', async () => {
|
||||
render(<App />)
|
||||
|
||||
const loginBtn = screen.getByRole('button', { name: /^Login$/i })
|
||||
|
|
@ -163,7 +185,7 @@ describe('Supplier Portal Onboarding & Active Dashboard Tests', () => {
|
|||
fireEvent.click(submitBtn)
|
||||
|
||||
// Check dashboard rendering
|
||||
expect(screen.getByText('Performance Analytics')).toBeInTheDocument()
|
||||
expect(await screen.findByText('Performance Analytics')).toBeInTheDocument()
|
||||
|
||||
// Check Sidebar Tab click: Manage Products
|
||||
const productsTabBtn = screen.getByText(/Manage Products/i)
|
||||
|
|
|
|||
522
src/App.tsx
522
src/App.tsx
|
|
@ -1,5 +1,5 @@
|
|||
import { useState } from 'react'
|
||||
import { CONFIG } from './config'
|
||||
import { 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'
|
||||
|
|
@ -78,6 +78,13 @@ export default function App() {
|
|||
})
|
||||
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)
|
||||
|
|
@ -132,6 +139,67 @@ export default function App() {
|
|||
})
|
||||
const [withdrawAmount, setWithdrawAmount] = useState('')
|
||||
|
||||
// Load data from backend on mount or when profile is complete / logged in
|
||||
useEffect(() => {
|
||||
if (isProfileComplete || currentPage === 'dashboard') {
|
||||
// Fetch Products
|
||||
apiFetch(`${CONFIG.apiBaseUrl}/api/products/`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (Array.isArray(data)) setProducts(data);
|
||||
})
|
||||
.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);
|
||||
})
|
||||
.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);
|
||||
})
|
||||
.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 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 simulations
|
||||
const [bulkLog, setBulkLog] = useState<string[]>([])
|
||||
const [isParsingBulk, setIsParsingBulk] = useState(false)
|
||||
|
|
@ -170,17 +238,100 @@ export default function App() {
|
|||
|
||||
const handleRegisterSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
navigateTo('profile-completion')
|
||||
|
||||
// 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
|
||||
})
|
||||
})
|
||||
.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(() => {
|
||||
navigateTo('profile-completion')
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
alert(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
const handleLoginSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setIsProfileComplete(true)
|
||||
navigateTo('dashboard', true)
|
||||
|
||||
// Email Validation
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(email)) {
|
||||
alert('Please enter a valid email address.');
|
||||
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(() => {
|
||||
setIsProfileComplete(true)
|
||||
navigateTo('dashboard', true)
|
||||
})
|
||||
.catch(err => {
|
||||
alert(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
const handleProfileSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (profileStep === 2) {
|
||||
setIsGstinVerified(true)
|
||||
}
|
||||
if (profileStep < 3) {
|
||||
setProfileStep(prev => prev + 1)
|
||||
} else {
|
||||
|
|
@ -194,21 +345,34 @@ export default function App() {
|
|||
// Product creation/modification
|
||||
const handleSaveProduct = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (isEditingProduct) {
|
||||
setProducts(products.map(p => p.id === productForm.id ? { ...productForm, price: Number(productForm.price), stock: Number(productForm.stock) } : p))
|
||||
setIsEditingProduct(false)
|
||||
alert('Product modified successfully!')
|
||||
} else {
|
||||
const newProd: Product = {
|
||||
...productForm,
|
||||
id: String(products.length + 1),
|
||||
price: Number(productForm.price),
|
||||
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)}`
|
||||
}
|
||||
setProducts([...products, newProd])
|
||||
alert('Product uploaded successfully!')
|
||||
}
|
||||
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 => setProducts(prods))
|
||||
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' })
|
||||
}
|
||||
|
|
@ -220,7 +384,13 @@ export default function App() {
|
|||
|
||||
const handleDeleteProduct = (id: string) => {
|
||||
if (confirm('Are you sure you want to delete this listing?')) {
|
||||
setProducts(products.filter(p => p.id !== id))
|
||||
apiFetch(`${CONFIG.apiBaseUrl}/api/products/${id}/`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(() => {
|
||||
setProducts(products.filter(p => p.id !== id))
|
||||
})
|
||||
.catch(err => console.error(err))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -230,19 +400,37 @@ export default function App() {
|
|||
setIsParsingBulk(true)
|
||||
setBulkLog(['Parsing CSV file template...', 'Validating rows against inventory Schema...'])
|
||||
|
||||
const mockBulkProducts = [
|
||||
{ title: 'Hand-Carved Walnut Bowl', category: 'Kitchenware', price: '65.00', stock: 15, sku: 'BOWL-WAL-12' },
|
||||
{ title: 'Organic Cotton Tablecloth', category: 'Linens', price: '48.00', stock: 30, sku: 'LINE-COT-15' }
|
||||
]
|
||||
|
||||
setTimeout(() => {
|
||||
const parsedProducts: Product[] = [
|
||||
{ id: String(products.length + 1), title: 'Hand-Carved Walnut Bowl', category: 'Kitchenware', price: 65.00, stock: 15, sku: 'BOWL-WAL-12', image: 'https://images.unsplash.com/photo-1610701596007-11502861dcfa?auto=format&fit=crop&q=80&w=100' },
|
||||
{ id: String(products.length + 2), title: 'Organic Cotton Tablecloth', category: 'Linens', price: 48.00, stock: 30, sku: 'LINE-COT-15', image: 'https://images.unsplash.com/photo-1603006905003-be475563bc59?auto=format&fit=crop&q=80&w=100' }
|
||||
]
|
||||
setProducts(prev => [...prev, ...parsedProducts])
|
||||
setBulkLog(prev => [
|
||||
...prev,
|
||||
'Row 1: Verified SKU BOWL-WAL-12 - Added',
|
||||
'Row 2: Verified SKU LINE-COT-15 - Added',
|
||||
'Successfully added 2 new listings bulk!'
|
||||
])
|
||||
setIsParsingBulk(false)
|
||||
apiFetch(`${CONFIG.apiBaseUrl}/api/products/bulk-upload/`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ products: mockBulkProducts })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(() => {
|
||||
setBulkLog(prev => [
|
||||
...prev,
|
||||
'Row 1: Verified SKU BOWL-WAL-12 - Added',
|
||||
'Row 2: Verified SKU LINE-COT-15 - Added',
|
||||
'Successfully added 2 new listings bulk!'
|
||||
])
|
||||
// Refresh products
|
||||
apiFetch(`${CONFIG.apiBaseUrl}/api/products/`)
|
||||
.then(r => r.json())
|
||||
.then(prods => {
|
||||
setProducts(prods)
|
||||
setIsParsingBulk(false)
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
setBulkLog(prev => [...prev, 'Error during upload to server.'])
|
||||
setIsParsingBulk(false)
|
||||
})
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
|
|
@ -254,43 +442,75 @@ export default function App() {
|
|||
alert('Please enter a valid amount')
|
||||
return
|
||||
}
|
||||
if (amount > wallet.outstanding) {
|
||||
alert('Amount exceeds outstanding ready payout balance')
|
||||
return
|
||||
}
|
||||
|
||||
const txId = `TX-${Math.floor(1000 + Math.random() * 9000)}`
|
||||
const newTx = {
|
||||
id: txId,
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
amount: amount,
|
||||
status: 'Transferred'
|
||||
}
|
||||
|
||||
setWallet({
|
||||
outstanding: wallet.outstanding - amount,
|
||||
withdrawn: wallet.withdrawn + amount,
|
||||
history: [newTx, ...wallet.history]
|
||||
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)
|
||||
})
|
||||
setWithdrawAmount('')
|
||||
alert(`Payout of $${amount} successfully transferred!`)
|
||||
}
|
||||
|
||||
// Returns actions
|
||||
const handleReturnAction = (id: string, action: 'Approved' | 'Rejected') => {
|
||||
setReturns(returns.map(ret => {
|
||||
if (ret.id === id) {
|
||||
return {
|
||||
...ret,
|
||||
status: action === 'Approved' ? 'In Transit' : 'Rejected'
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}))
|
||||
alert(`Return request ${action.toLowerCase()}!`)
|
||||
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))
|
||||
}
|
||||
|
||||
const selectedMetrics = CONFIG.dashboardData.filters[dateFilter]
|
||||
// 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()
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -518,15 +738,35 @@ export default function App() {
|
|||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="login-password">Password</label>
|
||||
<input
|
||||
id="login-password"
|
||||
type="password"
|
||||
className="form-control"
|
||||
placeholder="Enter your password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<input
|
||||
id="login-password"
|
||||
type={showLoginPass ? 'text' : 'password'}
|
||||
className="form-control"
|
||||
placeholder="Enter your password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
style={{ paddingRight: '45px' }}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowLoginPass(!showLoginPass)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '12px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
fontSize: '1.1rem',
|
||||
color: 'var(--text-muted)'
|
||||
}}
|
||||
>
|
||||
{showLoginPass ? '👁️' : '🙈'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right', marginBottom: '1.5rem' }}>
|
||||
<button type="button" className="nav-link" style={{ color: 'var(--text-muted)', fontSize: '0.85rem', textDecoration: 'none', background: 'none', border: 'none', cursor: 'pointer' }} onClick={(e) => { e.preventDefault(); navigateTo('forgot-password'); }}>Forgot Password?</button>
|
||||
|
|
@ -567,17 +807,84 @@ export default function App() {
|
|||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="reg-pass">Create Password *</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<input
|
||||
id="reg-pass"
|
||||
type={showSignupPass ? 'text' : 'password'}
|
||||
className="form-control"
|
||||
placeholder="Minimum 8 characters"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
style={{ paddingRight: '45px' }}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSignupPass(!showSignupPass)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '12px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
fontSize: '1.1rem',
|
||||
color: 'var(--text-muted)'
|
||||
}}
|
||||
>
|
||||
{showSignupPass ? '👁️' : '🙈'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="reg-confirm-pass">Confirm Password *</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<input
|
||||
id="reg-confirm-pass"
|
||||
type={showSignupConfirmPass ? 'text' : 'password'}
|
||||
className="form-control"
|
||||
placeholder="Re-enter password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
style={{ paddingRight: '45px' }}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSignupConfirmPass(!showSignupConfirmPass)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '12px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
fontSize: '1.1rem',
|
||||
color: 'var(--text-muted)'
|
||||
}}
|
||||
>
|
||||
{showSignupConfirmPass ? '👁️' : '🙈'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group" style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', margin: '1.25rem 0' }}>
|
||||
<input
|
||||
id="reg-pass"
|
||||
type="password"
|
||||
className="form-control"
|
||||
placeholder="Minimum 8 characters"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
id="reg-policy"
|
||||
type="checkbox"
|
||||
checked={policyAccepted}
|
||||
onChange={(e) => setPolicyAccepted(e.target.checked)}
|
||||
required
|
||||
/>
|
||||
<label htmlFor="reg-policy" style={{ fontSize: '0.85rem', color: 'var(--text-muted)', marginBottom: 0, cursor: 'pointer' }}>
|
||||
I accept the <a href="#terms" onClick={(e) => e.preventDefault()}>Terms of Service</a> and <a href="#privacy" onClick={(e) => e.preventDefault()}>Privacy Policy</a> *
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary" style={{ width: '100%', padding: '0.75rem', marginTop: '1rem' }}>
|
||||
|
||||
<button type="submit" className="btn btn-primary" style={{ width: '100%', padding: '0.75rem', marginTop: '0.5rem' }}>
|
||||
Register Business
|
||||
</button>
|
||||
</form>
|
||||
|
|
@ -869,12 +1176,12 @@ export default function App() {
|
|||
disabled={isGstinVerified}
|
||||
style={{ minWidth: '120px' }}
|
||||
>
|
||||
{isGstinVerified ? 'Verified ✓' : 'Verify GSTIN'}
|
||||
{isGstinVerified ? 'Submitted ✓' : 'Submit GSTIN'}
|
||||
</button>
|
||||
</div>
|
||||
{isGstinVerified && (
|
||||
<p style={{ color: 'var(--success)', fontSize: '0.85rem', marginTop: '0.5rem', fontWeight: 600 }}>
|
||||
GSTIN successfully verified with government registry.
|
||||
verification will be completed within next 24 hrs
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -945,7 +1252,7 @@ export default function App() {
|
|||
type="submit"
|
||||
className="btn btn-primary"
|
||||
style={{ flex: 1, backgroundColor: 'var(--accent)', color: 'var(--primary)', fontWeight: 'bold' }}
|
||||
disabled={!isGstinVerified || !aadharFile || !panFile}
|
||||
disabled={!gstin || !aadharFile || !panFile}
|
||||
>
|
||||
Next Step: Store & Location
|
||||
</button>
|
||||
|
|
@ -1332,7 +1639,7 @@ export default function App() {
|
|||
</td>
|
||||
<td>{p.sku}</td>
|
||||
<td>{p.category}</td>
|
||||
<td>${p.price.toFixed(2)}</td>
|
||||
<td>${Number(p.price).toFixed(2)}</td>
|
||||
<td style={{ fontWeight: 600, color: p.stock < 15 ? 'var(--error)' : 'inherit' }}>
|
||||
{p.stock} pcs
|
||||
</td>
|
||||
|
|
@ -1535,7 +1842,7 @@ export default function App() {
|
|||
<td>{o.item}</td>
|
||||
<td>{o.quantity}</td>
|
||||
<td>{o.customer}</td>
|
||||
<td style={{ fontWeight: 600 }}>${o.total.toFixed(2)}</td>
|
||||
<td style={{ fontWeight: 600 }}>${Number(o.total).toFixed(2)}</td>
|
||||
<td>
|
||||
<span className={`badge ${
|
||||
o.status === 'Delivered' ? 'badge-success' :
|
||||
|
|
@ -1552,32 +1859,14 @@ export default function App() {
|
|||
<button
|
||||
className="btn btn-primary"
|
||||
style={{ backgroundColor: 'var(--success)', color: 'white', border: 'none', padding: '0.35rem 0.75rem', fontSize: '0.8rem', cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setOrders(orders.map(item => item.id === o.id ? {
|
||||
...item,
|
||||
status: 'Ready to Ship',
|
||||
carrier: 'DHL Express',
|
||||
tracking: `DHL-${Math.floor(100000 + Math.random() * 900000)}`,
|
||||
eta: '3 Days'
|
||||
} : item));
|
||||
alert(`Order ${o.id} accepted successfully!`);
|
||||
}}
|
||||
onClick={() => handleAcceptOrder(o.id)}
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
style={{ backgroundColor: 'var(--error)', color: 'white', border: 'none', padding: '0.35rem 0.75rem', fontSize: '0.8rem', cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setOrders(orders.map(item => item.id === o.id ? {
|
||||
...item,
|
||||
status: 'Rejected',
|
||||
carrier: 'N/A',
|
||||
tracking: 'N/A',
|
||||
eta: 'N/A'
|
||||
} : item));
|
||||
alert(`Order ${o.id} rejected.`);
|
||||
}}
|
||||
onClick={() => handleRejectOrder(o.id)}
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
|
|
@ -1832,11 +2121,11 @@ export default function App() {
|
|||
<div className="metrics-row">
|
||||
<div className="metric-data-card">
|
||||
<div className="metric-title">Outstanding Ready Balance</div>
|
||||
<div className="metric-value" style={{ color: 'var(--success)' }}>${wallet.outstanding.toFixed(2)}</div>
|
||||
<div className="metric-value" style={{ color: 'var(--success)' }}>${Number(wallet.outstanding).toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="metric-data-card">
|
||||
<div className="metric-title">Total Payouts Withdrawn</div>
|
||||
<div className="metric-value">${wallet.withdrawn.toFixed(2)}</div>
|
||||
<div className="metric-value">${Number(wallet.withdrawn).toFixed(2)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -1877,11 +2166,11 @@ export default function App() {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{wallet.history.map(tx => (
|
||||
<tr key={tx.id}>
|
||||
<td>{tx.id}</td>
|
||||
{(wallet.history || (wallet as any).transactions || []).map((tx: any) => (
|
||||
<tr key={tx.id || tx.tx_id}>
|
||||
<td>{tx.id || tx.tx_id}</td>
|
||||
<td>{tx.date}</td>
|
||||
<td style={{ fontWeight: 600 }}>${tx.amount.toFixed(2)}</td>
|
||||
<td style={{ fontWeight: 600 }}>${Number(tx.amount).toFixed(2)}</td>
|
||||
<td><span className="badge badge-success">{tx.status}</span></td>
|
||||
</tr>
|
||||
))}
|
||||
|
|
@ -2029,10 +2318,10 @@ function WelcomeTourWizard({ onComplete, storeName }: { onComplete: () => void;
|
|||
const [tourStep, setTourStep] = useState(1)
|
||||
const steps = [
|
||||
{
|
||||
title: "Welcome to Global Artisans Hub! 🎉",
|
||||
description: `Congratulations ${storeName}! Your supplier profile is verified and complete. Let's take a 1-minute tour to get you familiar with your active workspace so you can begin selling immediately.`,
|
||||
icon: "🎉",
|
||||
action: "Start Tour"
|
||||
title: "Supplier Account Under Review ⏳",
|
||||
description: `Thank you for completing your profile! Your GSTIN, PAN, and Aadhaar card details have been submitted. Our compliance team is verifying your documents. This review is typically completed within the next 24 hours. While we verify your credentials, let's take a quick animated tour to get you familiar with your dashboard!`,
|
||||
icon: "⏳",
|
||||
action: "Start Guided Tour 🎬"
|
||||
},
|
||||
{
|
||||
title: "📦 Products & Inventory Management",
|
||||
|
|
@ -2056,17 +2345,17 @@ function WelcomeTourWizard({ onComplete, storeName }: { onComplete: () => void;
|
|||
title: "💼 Wallet & Payout Withdrawals",
|
||||
description: "Monitor outstanding payouts and withdraw your earnings directly to your bank account anytime. Keep track of transaction receipts directly inside the Wallet tab.",
|
||||
icon: "💼",
|
||||
action: "Launch Dashboard 🚀"
|
||||
action: "Explore Dashboard 🚀"
|
||||
}
|
||||
]
|
||||
|
||||
const current = steps[tourStep - 1]
|
||||
|
||||
return (
|
||||
<div className="form-card" style={{ maxWidth: '640px', margin: '2rem auto', textAlign: 'center', padding: '2.5rem' }}>
|
||||
<div style={{ fontSize: '4rem', marginBottom: '1rem' }}>{current.icon}</div>
|
||||
<div key={tourStep} className="form-card tour-card-animated" style={{ maxWidth: '640px', margin: '2rem auto', textAlign: 'center', padding: '2.5rem' }}>
|
||||
<div className="tour-icon-animated" style={{ fontSize: '4.5rem', marginBottom: '1rem' }}>{current.icon}</div>
|
||||
<h2 className="form-card-title">{current.title}</h2>
|
||||
<p style={{ color: 'var(--text-muted)', fontSize: '1.05rem', margin: '1.5rem 0 2rem' }}>
|
||||
<p style={{ color: 'var(--text-muted)', fontSize: '1.05rem', margin: '1.5rem 0 2rem', lineHeight: '1.6' }}>
|
||||
{current.description}
|
||||
</p>
|
||||
|
||||
|
|
@ -2074,6 +2363,7 @@ function WelcomeTourWizard({ onComplete, storeName }: { onComplete: () => void;
|
|||
{steps.map((_, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={tourStep === idx + 1 ? 'tour-dot-active' : ''}
|
||||
style={{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
|
|
|
|||
|
|
@ -1,4 +1,12 @@
|
|||
const getApiBaseUrl = () => {
|
||||
if (typeof window !== 'undefined' && window.location.hostname === 'betasuppliers.tipro.in') {
|
||||
return 'http://16.113.57.127:8000';
|
||||
}
|
||||
return 'http://localhost:8000';
|
||||
};
|
||||
|
||||
export const CONFIG = {
|
||||
apiBaseUrl: getApiBaseUrl(),
|
||||
companyName: 'Global Artisans Hub',
|
||||
logoLetter: 'A',
|
||||
supportEmail: 'support@globalartisanshub.com',
|
||||
|
|
@ -114,3 +122,10 @@ export const CONFIG = {
|
|||
]
|
||||
}
|
||||
}
|
||||
|
||||
export function apiFetch(url: string, options: RequestInit = {}): Promise<Response> {
|
||||
return fetch(url, {
|
||||
...options,
|
||||
credentials: 'include'
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1093,3 +1093,49 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
/* Animated Tour Keyframes */
|
||||
@keyframes tourCardEntrance {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.98);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tourIconBounce {
|
||||
0%, 100% {
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-8px) scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tourDotPulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.25);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tour-card-animated {
|
||||
animation: tourCardEntrance 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
|
||||
.tour-icon-animated {
|
||||
animation: tourIconBounce 2s infinite ease-in-out;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tour-dot-active {
|
||||
animation: tourDotPulse 1.5s infinite ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue