/* ============================================================= KASDAP ONE — STORE (auth · mode · cart · pricing · routing) Acts as a mock backend persisted to localStorage. ============================================================= */ const { createContext, useContext, useState, useEffect, useMemo, useCallback, useRef } = React; const LS = 'kasdap.v1.'; const load = (k, d) => { try { const v = localStorage.getItem(LS + k); return v ? JSON.parse(v) : d; } catch { return d; } }; const save = (k, v) => { try { localStorage.setItem(LS + k, JSON.stringify(v)); } catch {} }; /* ---- deterministic hash for stable "random" attributes ---- */ function hash(str) { let h = 2166136261; for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); } return (h >>> 0); } /* ---- default tier / pricing config (admin-editable) ---- */ const DEFAULT_PRICING = { d2cDiscount: 0.08, // retail online discount off MRP margins: { distributor: 0.42, chemist: 0.30, hospital: 0.36 }, // off-MRP wholesale margin slabs: [ { min: 10, off: 0.03 }, { min: 25, off: 0.05 }, { min: 50, off: 0.08 } ], // extra B2B qty discounts (boxes) gst: 0.12, }; /* ---- seed users (demo credentials shown on login) ---- */ const SEED_USERS = [ { id: 'u-dist', name: 'Rajesh Distributors', email: 'distributor@kasdap.in', pass: 'demo', role: 'distributor', accountType: 'both', modes: ['b2b','d2c'], org: 'Rajesh Pharma Distributors', city: 'Mumbai', gst: '27AAACR1111B1Z2' }, { id: 'u-chem', name: 'Apollo Chemist', email: 'chemist@kasdap.in', pass: 'demo', role: 'chemist', accountType: 'b2b', modes: ['b2b'], org: 'Apollo Medical Store', city: 'Nagpur', gst: '27AAFCA2222C1Z9' }, { id: 'u-hosp', name: 'Sahyadri Hospital', email: 'hospital@kasdap.in', pass: 'demo', role: 'hospital', accountType: 'b2b', modes: ['b2b'], org: 'Sahyadri Multispecialty', city: 'Pune', gst: '27AAGCS3333D1Z4' }, { id: 'u-cust', name: 'Priya Sharma', email: 'priya@gmail.com', pass: 'demo', role: 'retail', accountType: 'd2c', modes: ['d2c'], org: '', city: 'Delhi', gst: '' }, ]; /* ---- company admin (separate OTP-gated console, NOT a customer account) ---- */ const ADMIN_EMAIL = 'thegrowthxmedia@gmail.com'; const ROLE_META = { distributor: { label: 'Distributor', modes: ['b2b','d2c'], tier: 'distributor', short: 'DS' }, chemist: { label: 'Chemist / Retailer', modes: ['b2b','d2c'], tier: 'chemist', short: 'CH' }, hospital: { label: 'Hospital / Institution', modes: ['b2b','d2c'], tier: 'hospital', short: 'HP' }, retail: { label: 'Retail Customer', modes: ['d2c'], tier: 'retail', short: 'RT' }, }; const ACCOUNT_MODES = { d2c: ['d2c'], b2b: ['b2b'], both: ['b2b','d2c'] }; /* ---- parse packing string -> packs per box ---- */ function packsPerBox(packing) { if (!packing) return 10; const m = String(packing).match(/(\d+)\s*[xX]\s*(\d+)/); if (m) return parseInt(m[1]) || 10; return 10; } function unitsPerPack(packing) { const m = String(packing).match(/(\d+)\s*[xX]\s*(\d+)/); if (m) return parseInt(m[2]) || 10; return 10; } const CATEGORY_ICON = { 'Cardio-Diabetic': 'heart', 'CNS & Neuro': 'brain', 'Renal & Urology': 'droplet', 'Pain, Ortho & Rheumatology': 'bone', 'Respiratory & Allergy': 'lungs', 'Gastro & Nutritional': 'stomach', 'Specialty & Advanced': 'spark', 'Anti-Infective': 'shield', "Women's & Hormonal": 'venus', }; const StoreCtx = createContext(null); const useStore = () => useContext(StoreCtx); /* enrich a raw catalogue row into a full product object */ function enrichProduct(p) { const h = hash(p.handle || p.name || Math.random().toString()); const ppb = packsPerBox(p.packing); const upp = unitsPerPack(p.packing); const rating = (3.9 + ((h % 11) / 10)).toFixed(1); const reviews = 6 + (h % 240); const stock = p.qty != null ? p.qty : (20 + (h % 400)); return { ...p, id: p.handle, category: p.mainCat || 'Specialty & Advanced', icon: CATEGORY_ICON[p.mainCat] || 'spark', ppb, upp, rating, reviews, stock, rx: !/(VITAMIN|NUTRITION|PRE PRO|SUPPLIMENT)/i.test(p.therapeutic || ''), isNew: (h % 9) === 0, hot: (h % 7) === 0, h, }; } function StoreProvider({ children }) { /* ---------- base catalogue (from real data file) ---------- */ const baseProducts = useMemo(() => (window.KASDAP_PRODUCTS || []).map(enrichProduct), []); /* ---------- admin catalogue patches (persisted) ---------- */ const [patches, setPatches] = useState(() => load('patches', {})); // handle -> partial fields {stock,image,mrp,name,...} const [removed, setRemoved] = useState(() => load('removed', [])); // [handle] const [extras, setExtras] = useState(() => load('extras', [])); // [rawProduct] added by admin const products = useMemo(() => { const rm = new Set(removed); const base = baseProducts.filter(p => !rm.has(p.id)).map(p => { const patch = patches[p.id]; return patch ? enrichProduct({ ...p, ...patch }) : p; }); const add = extras.filter(e => !rm.has(e.handle)).map(e => enrichProduct(patches[e.handle] ? { ...e, ...patches[e.handle] } : e)); return [...add, ...base]; }, [baseProducts, patches, removed, extras]); const productById = useMemo(() => { const m = {}; products.forEach(p => m[p.id] = p); return m; }, [products]); /* facets */ const facets = useMemo(() => { const cat = {}, ther = {}, type = {}; products.forEach(p => { cat[p.category] = (cat[p.category]||0)+1; ther[p.therapeutic]=(ther[p.therapeutic]||0)+1; type[p.type]=(type[p.type]||0)+1; }); const sortE = o => Object.entries(o).sort((a,b)=>b[1]-a[1]); return { categories: sortE(cat), therapeutic: sortE(ther), types: sortE(type) }; }, [products]); /* ---------- persisted state ---------- */ const [theme, setTheme] = useState(() => load('theme', 'dark')); const [mode, setModeRaw] = useState(() => load('mode', 'd2c')); const [user, setUser] = useState(() => load('user', null)); const [cart, setCart] = useState(() => load('cart', [])); // {handle, qty, mode} const [pricing, setPricing] = useState(() => load('pricing', DEFAULT_PRICING)); const [overrides, setOverrides] = useState(() => load('overrides', {})); // handle -> {mrp, d2c, distributor, chemist, hospital} const [orders, setOrders] = useState(() => load('orders', [])); const [users, setUsers] = useState(() => load('users', SEED_USERS)); const [adminAuthed, setAdminAuthed] = useState(() => load('adminAuthed', false)); const [route, setRoute] = useState({ name: user ? 'catalog' : 'login', params: {} }); const [toasts, setToasts] = useState([]); useEffect(() => save('patches', patches), [patches]); useEffect(() => save('removed', removed), [removed]); useEffect(() => save('extras', extras), [extras]); useEffect(() => save('adminAuthed', adminAuthed), [adminAuthed]); useEffect(() => { document.documentElement.dataset.theme = theme; save('theme', theme); }, [theme]); useEffect(() => { document.documentElement.dataset.mode = mode; save('mode', mode); }, [mode]); useEffect(() => save('user', user), [user]); useEffect(() => save('cart', cart), [cart]); useEffect(() => save('pricing', pricing), [pricing]); useEffect(() => save('overrides', overrides), [overrides]); useEffect(() => save('orders', orders), [orders]); useEffect(() => save('users', users), [users]); /* ---------- live backend hydration (no-op in demo mode) ---------- */ useEffect(() => { if (window.KASDAP_SETTINGS) { if (window.KASDAP_SETTINGS.pricing) setPricing(window.KASDAP_SETTINGS.pricing); if (window.KASDAP_SETTINGS.theme) applyThemeColors(window.KASDAP_SETTINGS.theme); } }, []); useEffect(() => { if (window.KasdapAPI && window.KasdapAPI.live && user) { window.KasdapAPI.listOrders().then(({ orders }) => { if (Array.isArray(orders)) setOrders(orders); }).catch(() => {}); } }, [user]); /* ---------- toast ---------- */ const toast = useCallback((msg, kind = 'info') => { const id = Math.random().toString(36).slice(2); setToasts(t => [...t, { id, msg, kind }]); setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 3200); }, []); /* ---------- routing ---------- */ const go = useCallback((name, params = {}) => { setRoute({ name, params }); window.scrollTo(0, 0); }, []); /* ---------- auth ---------- */ const roleMeta = (r) => ROLE_META[r] || ROLE_META.retail; const userModes = (u) => (u && u.modes && u.modes.length) ? u.modes : (u ? roleMeta(u.role).modes : ['d2c']); const login = useCallback(async (email, pass) => { if (window.KasdapAPI && window.KasdapAPI.live) { try { const { token, user: u } = await window.KasdapAPI.login(email.trim(), pass); window.KasdapAPI.setToken(token); setUser(u); setModeRaw(userModes(u)[0]); setRoute({ name: 'catalog', params: {} }); toast(`Welcome back, ${u.name.split(' ')[0]} 👋`, 'ok'); return { ok: true }; } catch (e) { return { ok: false, err: e.message }; } } const u = users.find(x => x.email.toLowerCase() === email.trim().toLowerCase()); if (!u || u.pass !== pass) return { ok: false, err: 'Invalid email or password' }; setUser(u); setModeRaw(userModes(u)[0]); setRoute({ name: 'catalog', params: {} }); toast(`Welcome back, ${u.name.split(' ')[0]} 👋`, 'ok'); return { ok: true }; }, [users, toast]); const signup = useCallback(async (data) => { if (window.KasdapAPI && window.KasdapAPI.live) { try { const { token, user: u } = await window.KasdapAPI.signup({ name: data.name, email: data.email, password: data.pass, accountType: data.accountType || 'd2c', role: data.role, org: data.org || '', city: data.city || '', gst: data.gst || '' }); window.KasdapAPI.setToken(token); setUser(u); setModeRaw(userModes(u)[0]); setRoute({ name: 'catalog', params: {} }); toast('Account created — welcome to Kasdap ONE', 'ok'); return { ok: true }; } catch (e) { return { ok: false, err: e.message }; } } if (users.some(u => u.email.toLowerCase() === data.email.toLowerCase())) return { ok: false, err: 'Email already registered' }; const at = data.accountType || 'd2c'; const modes = ACCOUNT_MODES[at] || ['d2c']; const role = at === 'd2c' ? 'retail' : (data.role || 'chemist'); const u = { id: 'u-' + Math.random().toString(36).slice(2, 8), pass: data.pass, accountType: at, modes, role, name: data.name, email: data.email, org: data.org || '', city: data.city || '', gst: data.gst || '' }; setUsers(us => [...us, u]); setUser(u); setModeRaw(modes[0]); setRoute({ name: 'catalog', params: {} }); toast('Account created — welcome to Kasdap ONE', 'ok'); return { ok: true }; }, [users, toast]); const logout = useCallback(() => { if (window.KasdapAPI) window.KasdapAPI.setToken(null); setUser(null); setRoute({ name: 'login', params: {} }); }, []); const setMode = useCallback((m) => { const allowed = userModes(user); if (!allowed.includes(m)) { toast('Your account can only order in ' + allowed.map(x=>x.toUpperCase()).join(' / '), 'warn'); return; } setModeRaw(m); }, [user, toast]); /* ---------- pricing engine ---------- */ const priceOf = useCallback((p, m = mode, qtyBoxes = 1) => { const ov = overrides[p.id] || {}; const mrp = ov.mrp != null ? ov.mrp : p.mrp; if (m === 'd2c') { const unit = ov.d2c != null ? ov.d2c : Math.round(mrp * (1 - pricing.d2cDiscount) * 100) / 100; return { unit, mrp, unitLabel: 'pack', save: Math.max(0, mrp - unit), savePct: Math.round((1 - unit / mrp) * 100) }; } // b2b: per box. base pack wholesale from tier const tier = user ? roleMeta(user.role).tier : 'distributor'; const margin = pricing.margins[tier] != null ? pricing.margins[tier] : 0.40; let packPrice = ov[tier] != null ? ov[tier] : mrp * (1 - margin); // qty slab on top let slab = 0; pricing.slabs.forEach(s => { if (qtyBoxes >= s.min) slab = Math.max(slab, s.off); }); packPrice = packPrice * (1 - slab); const boxPrice = Math.round(packPrice * p.ppb * 100) / 100; const packR = Math.round(packPrice * 100) / 100; const boxMrp = Math.round(mrp * p.ppb * 100) / 100; return { unit: boxPrice, packPrice: packR, mrp, boxMrp, unitLabel: 'box', tier, slab, save: Math.max(0, boxMrp - boxPrice), savePct: Math.round((1 - boxPrice / boxMrp) * 100) }; }, [mode, overrides, pricing, user]); /* ---------- cart ---------- */ const cartLineKey = (handle, m) => handle + '::' + m; const addToCart = useCallback((handle, qty = 1, m = mode) => { setCart(c => { const i = c.findIndex(x => x.handle === handle && x.mode === m); if (i >= 0) { const n = [...c]; n[i] = { ...n[i], qty: n[i].qty + qty }; return n; } return [...c, { handle, qty, mode: m }]; }); }, [mode]); const setQty = useCallback((handle, m, qty) => { setCart(c => qty <= 0 ? c.filter(x => !(x.handle===handle && x.mode===m)) : c.map(x => (x.handle===handle && x.mode===m) ? { ...x, qty } : x)); }, []); const removeFromCart = useCallback((handle, m) => setCart(c => c.filter(x => !(x.handle===handle && x.mode===m))), []); const clearCart = useCallback(() => setCart([]), []); const inCart = useCallback((handle, m = mode) => cart.some(x => x.handle === handle && x.mode === m), [cart, mode]); const cartDetail = useMemo(() => { const lines = cart.map(l => { const p = productById[l.handle]; if (!p) return null; const pr = priceOf(p, l.mode, l.qty); return { ...l, p, pr, lineTotal: pr.unit * l.qty }; }).filter(Boolean); const d2c = lines.filter(l => l.mode === 'd2c'); const b2b = lines.filter(l => l.mode === 'b2b'); const sum = arr => arr.reduce((s, l) => s + l.lineTotal, 0); const d2cSub = sum(d2c), b2bSub = sum(b2b); const subtotal = d2cSub + b2bSub; const gst = Math.round(subtotal * pricing.gst * 100) / 100; return { lines, d2c, b2b, d2cSub, b2bSub, subtotal, gst, count: lines.length, units: lines.reduce((s,l)=>s+l.qty,0), total: subtotal + gst }; }, [cart, productById, priceOf, pricing.gst]); /* ---------- orders ---------- */ const placeOrder = useCallback((kind, payload) => { const seq = (orders.length + 1).toString().padStart(4, '0'); const id = (kind === 'rfq' ? 'RFQ-' : 'KD-') + new Date().getFullYear() + '-' + seq; const order = { id, kind, mode: payload.mode, createdAt: Date.now(), uid: user ? user.id : null, lines: payload.lines.map(l => ({ handle: l.handle, name: l.p.name, qty: l.qty, mode: l.mode, unit: l.pr.unit, unitLabel: l.pr.unitLabel })), total: payload.total, status: kind === 'rfq' ? 'quoted' : 'confirmed', customer: user ? { name: user.name, org: user.org, city: user.city } : null, address: payload.address || null, stage: kind === 'rfq' ? 1 : 1, }; setOrders(o => [order, ...o]); // sync to backend when live (optimistic local update already done) if (window.KasdapAPI && window.KasdapAPI.live) { window.KasdapAPI.placeOrder({ kind, mode: order.mode, lines: order.lines, total: order.total, customer: order.customer, address: order.address }) .then(({ order: saved }) => { if (saved && saved.id) setOrders(os => os.map(o => o === order ? { ...order, id: saved.id, status: saved.status } : o)); }) .catch(e => console.warn('Order sync failed:', e.message)); } return order; }, [orders, user]); /* ---------- admin: order lifecycle ---------- */ const ORDER_STAGES = { order: ['Confirmed','Packed','Shipped','Delivered'], rfq: ['Requested','Quoted','Approved','Dispatched'] }; const updateOrderStatus = useCallback((id, stage) => { setOrders(os => os.map(o => { if (o.id !== id) return o; const stages = ORDER_STAGES[o.kind] || ORDER_STAGES.order; const st = Math.max(1, Math.min(stages.length, stage)); const statusMap = { order: ['confirmed','packed','shipped','delivered'], rfq: ['requested','quoted','approved','dispatched'] }; const status = (statusMap[o.kind] || statusMap.order)[st-1]; if (window.KasdapAPI && window.KasdapAPI.live) window.KasdapAPI.adminUpdateOrder(id, { stage: st, status }).catch(()=>{}); return { ...o, stage: st, status }; })); }, []); const cancelOrder = useCallback((id) => { if (window.KasdapAPI && window.KasdapAPI.live) window.KasdapAPI.adminUpdateOrder(id, { status: 'cancelled' }).catch(()=>{}); setOrders(os => os.map(o => o.id===id ? { ...o, status:'cancelled' } : o)); }, []); /* ---------- admin: catalogue bulk ops ---------- */ const patchProduct = useCallback((handle, fields) => { if (window.KasdapAPI && window.KasdapAPI.live) window.KasdapAPI.adminEditProduct(handle, fields).catch(()=>{}); setPatches(p => ({ ...p, [handle]: { ...(p[handle]||{}), ...fields } })); }, []); const removeProduct = useCallback((handle) => { if (window.KasdapAPI && window.KasdapAPI.live) window.KasdapAPI.adminDeleteProduct(handle).catch(()=>{}); setRemoved(r => r.includes(handle) ? r : [...r, handle]); }, []); const addProduct = useCallback((raw) => { const handle = raw.handle || ('sku-' + Math.random().toString(36).slice(2,8)); if (window.KasdapAPI && window.KasdapAPI.live) window.KasdapAPI.adminAddProduct({ ...raw, handle }).catch(()=>{}); setExtras(e => [{ ...raw, handle, brand: raw.brand || 'Kasdap' }, ...e]); setRemoved(r => r.filter(h => h !== handle)); return handle; }, []); /* bulk import: rows = [{handle,name,...}]. mergeMode 'replace' wipes catalogue first (dedupe by handle/SKU). */ const bulkImport = useCallback((rows, mergeMode) => { const seen = new Set(); const clean = []; rows.forEach(r => { const key = (r.handle || r.sku || r.name || '').toString().toLowerCase().trim(); if (!key || seen.has(key)) return; seen.add(key); clean.push({ ...r, handle: r.handle || ('imp-' + key.replace(/[^a-z0-9]/g,'').slice(0,24)) }); }); if (window.KasdapAPI && window.KasdapAPI.live) window.KasdapAPI.adminBulk(clean, mergeMode).catch(()=>{}); if (mergeMode === 'replace') { // replace-all: hide every base product, keep only the imported set live setRemoved(baseProducts.map(p => p.id)); setExtras(clean); setPatches({}); } else { setExtras(e => { const map = {}; [...e, ...clean].forEach(x => { map[x.handle] = x; }); return Object.values(map); }); setRemoved(r => r.filter(h => !clean.some(c => c.handle === h))); } return { imported: clean.length, skipped: rows.length - clean.length }; }, [baseProducts]); const restoreCatalogue = useCallback(() => { setRemoved([]); setExtras([]); setPatches({}); }, []); /* ---------- admin: OTP auth (company console) ---------- */ const otpRef = useRef(null); const requestOtp = useCallback(async (email) => { if (window.KasdapAPI && window.KasdapAPI.live) { try { await window.KasdapAPI.adminOtpRequest(email.trim()); return { ok: true, emailed: true }; } catch (e) { return { ok: false, err: e.message }; } } if (email.trim().toLowerCase() !== ADMIN_EMAIL) return { ok: false, err: 'This email is not authorised for admin access' }; const code = ('' + Math.floor(100000 + Math.random() * 900000)); otpRef.current = { code, exp: Date.now() + 5 * 60000, email }; return { ok: true, code }; // demo mode surfaces the code on-screen; live mode e-mails it }, []); const verifyOtp = useCallback(async (code, email) => { if (window.KasdapAPI && window.KasdapAPI.live) { try { const { token } = await window.KasdapAPI.adminOtpVerify((email || ADMIN_EMAIL).trim(), code.trim()); window.KasdapAPI.setAdminToken(token); setAdminAuthed(true); return { ok: true }; } catch (e) { return { ok: false, err: e.message }; } } const o = otpRef.current; if (!o) return { ok: false, err: 'Request a code first' }; if (Date.now() > o.exp) return { ok: false, err: 'Code expired — request a new one' }; if (code.trim() !== o.code) return { ok: false, err: 'Incorrect code' }; setAdminAuthed(true); otpRef.current = null; return { ok: true }; }, []); const adminLogout = useCallback(() => { if (window.KasdapAPI) window.KasdapAPI.setAdminToken(null); setAdminAuthed(false); }, []); const loadAdminOrders = useCallback(() => { if (window.KasdapAPI && window.KasdapAPI.live) { window.KasdapAPI.adminOrders().then(({ orders }) => { if (Array.isArray(orders)) setOrders(orders); }).catch(() => {}); } }, []); /* seed a couple of historical orders once */ useEffect(() => { if (load('seeded', false) || products.length === 0) return; const pick = (i) => products[(i * 53) % products.length]; const demo = [ { id: 'KD-2026-0007', uid: 'u-cust', kind: 'order', mode: 'd2c', status: 'delivered', stage: 4, total: 1840, createdAt: Date.now()-86400000*9, lines: [{handle:pick(2).id,name:pick(2).name,qty:2,mode:'d2c',unit:190,unitLabel:'pack'}], customer:{name:'Priya Sharma',org:'',city:'Delhi'} }, { id: 'KD-2026-0006', uid: 'u-dist', kind: 'order', mode: 'b2b', status: 'shipped', stage: 3, total: 48200, createdAt: Date.now()-86400000*3, lines:[{handle:pick(7).id,name:pick(7).name,qty:14,mode:'b2b',unit:1820,unitLabel:'box'}], customer:{name:'Rajesh Distributors',org:'Rajesh Pharma',city:'Mumbai'} }, { id: 'RFQ-2026-0005', uid: 'u-hosp', kind: 'rfq', mode: 'b2b', status: 'quoted', stage: 2, total: 126500, createdAt: Date.now()-86400000*1, lines:[{handle:pick(11).id,name:pick(11).name,qty:40,mode:'b2b',unit:1600,unitLabel:'box'}], customer:{name:'Sahyadri Hospital',org:'Sahyadri',city:'Pune'} }, ]; setOrders(o => o.length ? o : demo); save('seeded', true); }, [products]); const resetData = useCallback(() => { Object.keys(localStorage).filter(k => k.startsWith(LS)).forEach(k => localStorage.removeItem(k)); location.reload(); }, []); const value = { products, productById, facets, baseProducts, theme, setTheme, toggleTheme: () => setTheme(t => t === 'dark' ? 'light' : 'dark'), mode, setMode, modeLabel: mode === 'b2b' ? 'B2B · Trade' : 'D2C · Retail', user, users, setUsers, login, signup, logout, roleMeta, userModes, ROLE_META, SEED_USERS, ACCOUNT_MODES, ADMIN_EMAIL, cart, addToCart, setQty, removeFromCart, clearCart, inCart, cartDetail, pricing, setPricing, overrides, setOverrides, priceOf, DEFAULT_PRICING, orders, placeOrder, resetData, updateOrderStatus, cancelOrder, ORDER_STAGES, patchProduct, removeProduct, addProduct, bulkImport, restoreCatalogue, patches, removed, extras, adminAuthed, requestOtp, verifyOtp, adminLogout, loadAdminOrders, route, go, toast, toasts, }; return React.createElement(StoreCtx.Provider, { value }, children); } Object.assign(window, { StoreProvider, useStore, ROLE_META, CATEGORY_ICON });