From 93df333df135bd19794914a5aefc21e90cb22a80 Mon Sep 17 00:00:00 2001 From: abhishek Date: Tue, 23 Jun 2026 19:18:01 +0530 Subject: [PATCH] implemented the sales and revenue report --- src/App.tsx | 4 +- src/components/ComparisonModal.tsx | 192 ++-- src/components/DeliveryReportsView.tsx | 25 +- src/components/FMCGHoverOverlay.tsx | 68 ++ src/components/InventoryView.tsx | 1286 ++++++++++++++++-------- src/components/ReportsView.tsx | 24 +- src/components/SalesRevenueReport.tsx | 605 +++++++++++ src/components/StoreCatalogView.tsx | 642 ++++++++---- src/components/TrialBatchDrawer.tsx | 6 +- src/components/UserStorePage.tsx | 2 +- src/components/consoleUi.tsx | 27 +- src/contexts/CompareContext.tsx | 7 + src/services/fiestaApi.ts | 59 ++ src/services/fiestaQueries.ts | 54 + src/services/storeCatalogue.ts | 25 +- src/types.ts | 3 +- src/utils/fmcgUtils.ts | 93 ++ 17 files changed, 2410 insertions(+), 712 deletions(-) create mode 100644 src/components/FMCGHoverOverlay.tsx create mode 100644 src/components/SalesRevenueReport.tsx create mode 100644 src/utils/fmcgUtils.ts diff --git a/src/App.tsx b/src/App.tsx index 33eea8b..91bc348 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -623,8 +623,8 @@ export default function App() { /> {/* Main core pages payload area */} -
-
+
+
{/* Nav content routing */} {currentSection === 'dashboard' && ( selectedStore ? ( diff --git a/src/components/ComparisonModal.tsx b/src/components/ComparisonModal.tsx index 29854f4..ae4dc50 100644 --- a/src/components/ComparisonModal.tsx +++ b/src/components/ComparisonModal.tsx @@ -84,81 +84,135 @@ export default function ComparisonModal() {
{/* Left Column for labels */} -
-
Category
-
SKU
-
Price
-
Units Sold
-
Status
-
- - {/* Product Columns */} - {selectedProducts.map((prod) => ( -
- - {/* Remove button overlay */} - - - {/* Product Header Card */} -
-
- {prod.name} -
-

{prod.name}

-
+ {(() => { + const isGlobalMode = selectedProducts.some(p => p.isGlobal); + + return ( + <> +
+
Category
+
SKU
+ + {isGlobalMode ? ( + <> +
Wholesale Price
+
Retail (MRP)
+
Profit Margin
+
Global Sales
+
Rating
+ + ) : ( + <> +
Price
+
Units Sold
+
Status
+ + )}
-
- {/* Metrics */} -
- {prod.category || '—'} -
- -
- {prod.sku || '—'} -
- -
- - {prod.price > 0 ? `₹${prod.price.toLocaleString('en-IN')}` : '—'} - -
- -
- - {prod.unitsSold != null ? prod.unitsSold.toLocaleString('en-IN') : '—'} - -
+ {/* Product Columns */} + {selectedProducts.map((prod) => ( +
+ + {/* Remove button overlay */} + -
- {prod.verified !== undefined ? ( -
- {prod.verified ? ( - - Active - + {/* Product Header Card */} +
+
+ {prod.name} +
+

{prod.name}

+
+
+
+ + {/* Metrics */} +
+ {prod.category || '—'} +
+ +
+ {prod.sku || '—'} +
+ + {isGlobalMode ? ( + <> +
+ + {prod.wholesalePrice ? `₹${prod.wholesalePrice.toLocaleString('en-IN')}` : '—'} + +
+
+ + {prod.mrp ? `₹${prod.mrp.toLocaleString('en-IN')}` : '—'} + +
+
+ {prod.profitMargin ? ( + + +{prod.profitMargin}% + + ) : ( + + )} +
+
+ + {prod.globalSales != null ? prod.globalSales.toLocaleString('en-IN') : '—'} + +
+
+ + {prod.rating ? `${prod.rating.toFixed(1)} ★` : '—'} + +
+ ) : ( - - Inspection - + <> +
+ + {prod.price > 0 ? `₹${prod.price.toLocaleString('en-IN')}` : '—'} + +
+
+ + {prod.unitsSold != null ? prod.unitsSold.toLocaleString('en-IN') : '—'} + +
+
+ {prod.verified !== undefined ? ( +
+ {prod.verified ? ( + + Active + + ) : ( + + Inspection + + )} +
+ ) : prod.closing !== undefined ? ( + + {prod.closing.toLocaleString('en-IN')} {prod.unit || 'Pc'} + + ) : ( + + )} +
+ )}
- ) : prod.closing !== undefined ? ( - - {prod.closing.toLocaleString('en-IN')} {prod.unit || 'Pc'} - - ) : ( - - )} -
- -
- ))} + ))} + + ); + })()} {/* Empty placeholders removed */} diff --git a/src/components/DeliveryReportsView.tsx b/src/components/DeliveryReportsView.tsx index 0d83cf1..d9e8cf7 100644 --- a/src/components/DeliveryReportsView.tsx +++ b/src/components/DeliveryReportsView.tsx @@ -24,6 +24,8 @@ import { Skeleton, Tooltip } from './consoleUi'; +import SalesRevenueReport from './SalesRevenueReport'; + type ReportTab = 'orders-summary' | 'riders-summary'; const TABS: Array<{ key: ReportTab; label: string; icon: typeof TrendingUp }> = [ { key: 'orders-summary', label: 'Orders Summary', icon: Store }, @@ -33,6 +35,8 @@ const TABS: Array<{ key: ReportTab; label: string; icon: typeof TrendingUp }> = interface DeliveryReportsViewProps { searchQuery?: string; tenantId?: number; locationid?: number; } export default function DeliveryReportsView({ searchQuery = '', tenantId = FIESTA_TENANT_ID, locationid }: DeliveryReportsViewProps) { + const [activeTab, setActiveTab] = useState<'overview' | 'sales_revenue'>('overview'); + const today = new Date(); const monthStart = new Date(today.getFullYear(), today.getMonth(), 1); const [fromdate, setFromdate] = useState(ymd(monthStart)); @@ -51,8 +55,23 @@ export default function DeliveryReportsView({ searchQuery = '', tenantId = FIEST return (
+
+ + +
- + {activeTab === 'overview' ? ( + <> {/* Tab nav & Date range combined */}
@@ -86,6 +105,10 @@ export default function DeliveryReportsView({ searchQuery = '', tenantId = FIEST {tab === 'orders-summary' && } {tab === 'riders-summary' && } + + ) : ( + + )}
); } diff --git a/src/components/FMCGHoverOverlay.tsx b/src/components/FMCGHoverOverlay.tsx new file mode 100644 index 0000000..f6b966a --- /dev/null +++ b/src/components/FMCGHoverOverlay.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { generateFMCGDetails } from '../utils/fmcgUtils'; +import { Info, ShieldCheck, Beaker, Package, BarChart } from 'lucide-react'; + +interface Props { + productId: string; + category: string; + productName: string; +} + +export default function FMCGHoverOverlay({ productId, category, productName }: Props) { + const details = generateFMCGDetails(productId, category); + + return ( +
+ + {/* 1. The Hook (Front Panel) */} +
+
+

{productName}

+

{details.brandName} • {details.marketingClaim}

+
+
+
+ {details.netWeight} +
+
+
+
+
+
+ + {/* 2. Ingredients & Legal (Back Panel) */} +
+
+
+ Ingredients +
+

{details.ingredients}

+
+ {details.allergens} +
+
+ +
+
+ FSSAI / Storage +
+

Lic No. {details.fssai}

+

{details.storage}

+
+ + {/* 3. Retail Info */} +
+
+ Batch No. + {details.batchNo} +
+
+ EAN / Barcode + {details.barcode} +
+
+
+ +
+ ); +} diff --git a/src/components/InventoryView.tsx b/src/components/InventoryView.tsx index b79b2c4..1cf0d84 100644 --- a/src/components/InventoryView.tsx +++ b/src/components/InventoryView.tsx @@ -29,16 +29,27 @@ import { ChevronDown, ChevronUp, CheckCircle, - ShoppingCart + ShoppingCart, + Inbox, + Store, + Activity, + Award } from 'lucide-react'; import { ProductMatrixItem } from '../types'; -import { useFiestaProductCategories, useFiestaStoresStock, useFiestaTenantLocations } from '../services/fiestaQueries'; +import { + useFiestaTenantLocations, + useFiestaStoresStock, + useFiestaProductCategories, + useFiestaMasterCatalog, + useFiestaUpdateStockRequest, +} from '../services/fiestaQueries'; import { FIESTA_TENANT_ID, str as fstr } from '../services/fiestaApi'; import { stockRowToProduct, stockRowToInventory } from '../services/fiestaMappers'; import { useStoreCatalogue } from '../services/storeCatalogue'; import BulkCartDrawer from './BulkCartDrawer'; import AwaitingApi from './AwaitingApi'; -import { SlideDrawer, Skeleton } from './consoleUi'; +import { SlideDrawer, Skeleton, TH_STYLE, SURFACE_ALT, TEXT, TEXT_2, TEXT_3, BORDER, BRAND, tint, edge, StatusChip } from './consoleUi'; +import FMCGHoverOverlay from './FMCGHoverOverlay'; import { useCompare } from '../contexts/CompareContext'; import TrialBatchDrawer, { TrialProduct } from './TrialBatchDrawer'; @@ -73,11 +84,32 @@ const MOCK_GLOBAL_CATALOG: ProductMatrixItem[] = [ unitsSold: 0, revenue: 0, stockStatus: 'Healthy', trend: 'flat', exposure: 'All Outlets', verified: true, isNew: true, image: 'https://images.unsplash.com/photo-1563636619-e9143da7973b?auto=format&fit=crop&q=80&w=200' }, + { + id: 'gc-7', name: 'Maggi 2-Minute Noodles Masala 70g', sku: 'GC-MAG-NOO', category: 'Snacks / Instant', + unitsSold: 0, revenue: 0, stockStatus: 'Healthy', trend: 'flat', exposure: 'All Outlets', verified: true, isNew: true, + image: 'https://images.unsplash.com/photo-1612817288484-6f916006741a?auto=format&fit=crop&q=80&w=200' + }, + { + id: 'gc-8', name: 'Dettol Original Bathing Soap 75g', sku: 'GC-DET-SOAP', category: 'Personal Care / Soap', + unitsSold: 0, revenue: 0, stockStatus: 'Healthy', trend: 'flat', exposure: 'All Outlets', verified: true, isNew: true, + image: 'https://images.unsplash.com/photo-1584824486509-112e4181ff6b?auto=format&fit=crop&q=80&w=200' + }, + { + id: 'gc-9', name: 'Surf Excel Easy Wash Detergent 1kg', sku: 'GC-SUR-WASH', category: 'Home Care / Laundry', + unitsSold: 0, revenue: 0, stockStatus: 'Healthy', trend: 'flat', exposure: 'All Outlets', verified: true, isNew: true, + image: 'https://images.unsplash.com/photo-1583947215259-38e31be8751f?auto=format&fit=crop&q=80&w=200' + }, + { + id: 'gc-10', name: 'MTR Rava Idli Mix 500g', sku: 'GC-MTR-IDL', category: 'Staples / Ready to Cook', + unitsSold: 0, revenue: 0, stockStatus: 'Healthy', trend: 'flat', exposure: 'All Outlets', verified: true, isNew: true, + image: 'https://images.unsplash.com/photo-1589301760014-d929f39ce9b0?auto=format&fit=crop&q=80&w=200' + }, ]; type StockRow = Record; const rowId = (r: StockRow) => String(r.productid ?? '') || String(r.productname ?? ''); + interface InventoryViewProps { searchQuery: string; isCoimbatoreView: boolean; @@ -89,9 +121,8 @@ export default function InventoryView({ isCoimbatoreView, tenantId = FIESTA_TENANT_ID }: InventoryViewProps) { - const { selectedProducts, toggleProduct, setIsComparing, clearSelection } = useCompare(); + const { selectedProducts, toggleProduct, setIsComparing, clearSelection, setHideCompareBar } = useCompare(); const [trialProducts, setTrialProducts] = useState([]); - const [isCartOpen, setIsCartOpen] = useState(false); // ── Live stock across every outlet (Fiesta) ─────────────────────────────── // This page is the admin's command surface. The GLOBAL CATALOG is the deduped @@ -121,6 +152,8 @@ export default function InventoryView({ // admin adds/imports in-session. Seeded once from the live data. const [products, setProducts] = useState([]); const [seeded, setSeeded] = useState(false); + const [selectedAdminProduct, setSelectedAdminProduct] = useState(null); + const [selectedRequest, setSelectedRequest] = useState(null); const allStoreRows = storesStock.flatMap((s) => s.rows); useEffect(() => { @@ -133,52 +166,125 @@ export default function InventoryView({ const initialProducts = Array.from(byId.values()).map(stockRowToProduct); - // Add 3 mock products with isNew: true - const mockProducts: ProductMatrixItem[] = [ - - { - id: 'mock-2', - name: 'Premium Basmati Rice 5kg', - sku: 'STA-BAS-5KG', - unitsSold: 0, - revenue: 0, - stockStatus: 'Healthy', - trend: 'flat', - image: 'https://images.unsplash.com/photo-1586201375761-83865001e31c?auto=format&fit=crop&q=80&w=200', - category: 'Staples / Rice', - exposure: 'All Outlets', - verified: true, - isNew: true - }, - { - id: 'mock-3', - name: 'Fresh Farm Eggs (Dozen)', - sku: 'FRE-EGG-12P', - unitsSold: 0, - revenue: 0, - stockStatus: 'Healthy', - trend: 'flat', - image: 'https://images.unsplash.com/photo-1587486913049-53fc88980cfc?auto=format&fit=crop&q=80&w=200', - category: 'Fresh Produce / Dairy', - exposure: 'All Outlets', - verified: true, - isNew: true - } - ]; - - setProducts([...mockProducts, ...initialProducts]); + setProducts(initialProducts); setSeeded(true); }, [allStoreRows, seeded]); - const [activeTab, setActiveTab] = useState<'catalog' | 'import_branding'>('catalog'); + const masterCatalogQ = useFiestaMasterCatalog({ tenantid: tenantId }); + const liveMasterCatalog = useMemo(() => masterCatalogQ.data ?? [], [masterCatalogQ.data]); + + const [activeTab, setActiveTab] = useState<'catalog' | 'import_branding' | 'requests'>('catalog'); const [selectedCategories, setSelectedCategories] = useState([]); - const [selectedAdminProduct, setSelectedAdminProduct] = useState(null); + const [hoveredAdminProduct, setHoveredAdminProduct] = useState(null); const [localSearch, setLocalSearch] = useState(''); const storeCat = useStoreCatalogue(); const [globalCatalogSearch, setGlobalCatalogSearch] = useState(''); const [globalCatalogPicks, setGlobalCatalogPicks] = useState>(new Set()); const [csvText, setCsvText] = useState(''); + const [requestStoreFilter, setRequestStoreFilter] = useState('All Stores'); + + const [storeRequests, setStoreRequests] = useState<{ locationid: number, locationname: string, picks: Record }[]>([]); + + useEffect(() => { + if (activeTab === 'requests') { + const reqs: any[] = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith('nearledaily.catalogue.request.')) { + const locIdStr = key.split('.').pop(); + if (locIdStr && locIdStr !== 'na') { + const locId = parseInt(locIdStr); + const raw = localStorage.getItem(key); + if (raw) { + try { + const parsed = JSON.parse(raw); + const picks: Record = {}; + for (const [k, v] of Object.entries(parsed)) { + if (typeof v === 'number') picks[k] = { qty: v, status: 'Pending', requestedAt: new Date().toISOString() }; + else { + picks[k] = v; + if (picks[k].status === 'Approve') picks[k].status = 'Approved'; + if (picks[k].status === 'Reject') picks[k].status = 'Rejected'; + } + } + if (Object.keys(picks).length > 0) { + const loc = locations.find(l => l.locationid === locId); + reqs.push({ + locationid: locId, + locationname: loc ? loc.locationname : `Outlet #${locId}`, + picks + }); + } + } catch (e) {} + } + } + } + } + setStoreRequests(reqs); + } + }, [activeTab, locations]); + + const updateStockRequestMutation = useFiestaUpdateStockRequest(); + + const updateProductRequestStatus = (locationid: number, productid: string, status: 'Approved' | 'Rejected' | 'Pending') => { + // Optimistic API update + updateStockRequestMutation.mutate({ + tenantid: tenantId, + locationid, + productid: Number(productid), + status + }); + + const key = `nearledaily.catalogue.request.${locationid}`; + const raw = localStorage.getItem(key); + if (raw) { + try { + const picks = JSON.parse(raw); + if (picks[productid] != null) { + const existing = typeof picks[productid] === 'number' + ? { qty: picks[productid], status: 'Pending', requestedAt: new Date().toISOString() } + : picks[productid]; + + picks[productid] = { ...existing, status, resolvedAt: status === 'Pending' ? undefined : new Date().toISOString() }; + localStorage.setItem(key, JSON.stringify(picks)); + + if (status === 'Approved') { + const prod = products.find(p => String(p.id) === String(productid)) || MOCK_GLOBAL_CATALOG.find(p => String(p.id) === String(productid)); + if (prod) { + if (storeCat.has(productid)) { + storeCat.setQty(productid, storeCat.getQty(productid) + existing.qty); + } else { + storeCat.add({ + productid: String(prod.id), + name: prod.name, + image: prod.image, + category: prod.category, + sku: prod.sku, + price: prod.unitsSold > 0 ? Math.round(prod.revenue / prod.unitsSold) : 10, + unit: prod.exposure || 'Piece', + qty: existing.qty + }); + } + } + } + + setStoreRequests(prev => prev.map(r => { + if (r.locationid === locationid) { + return { ...r, picks: { ...r.picks, [productid]: picks[productid] } }; + } + return r; + })); + } + } catch (e) {} + } + }; + + // Hide compare bar when not in Global Catalogue + useEffect(() => { + setHideCompareBar(activeTab !== 'import_branding'); + }, [activeTab, setHideCompareBar]); + // Live product categories (for the Add-Product modal dropdown). const productCategoriesQ = useFiestaProductCategories(); const productCategoryNames = useMemo( @@ -224,7 +330,7 @@ export default function InventoryView({ const toggleCategory = (cat: string) => { setSelectedCategories((prev) => - prev.includes(cat) ? prev.filter((c) => c !== cat) : [...prev, cat], + prev.includes(cat) ? [] : [cat], ); }; @@ -251,7 +357,7 @@ export default function InventoryView({ const toggleGlobalCategory = (cat: string) => { setGlobalSelectedCategories((prev) => - prev.includes(cat) ? prev.filter((c) => c !== cat) : [...prev, cat], + prev.includes(cat) ? [] : [cat], ); }; @@ -308,9 +414,48 @@ export default function InventoryView({ alert('All the specified SKU codes are already active in the catalogue ledger.'); } }; + const flattenedRequests = useMemo(() => { + const list: any[] = []; + storeRequests.forEach(req => { + Object.entries(req.picks).forEach(([productid, pickData]) => { + let product = products.find(p => String(p.id) === String(productid)); + if (!product) { + const liveMatch = liveMasterCatalog.find((r: any) => String(r.productid) === String(productid)); + if (liveMatch) { + product = { + id: String(liveMatch.productid), + name: String(liveMatch.productname || 'Unknown'), + sku: String(liveMatch.sku || `SKU-${liveMatch.productid}`), + image: String(liveMatch.productimage || 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&q=80&w=200'), + category: String(liveMatch.categoryname || 'Uncategorized'), + } as any; + } else { + product = MOCK_GLOBAL_CATALOG.find(p => String(p.id) === String(productid)) as any; + } + } + list.push({ + locationid: req.locationid, + locationname: req.locationname, + productid, + product, + pickData + }); + }); + }); + // Sort by requestedAt desc + let sortedList = list.sort((a, b) => new Date(b.pickData.requestedAt).getTime() - new Date(a.pickData.requestedAt).getTime()); + if (requestStoreFilter !== 'All Stores') { + sortedList = sortedList.filter(req => req.locationname === requestStoreFilter); + } + return sortedList; + }, [storeRequests, products, requestStoreFilter]); + + const requestingStores = useMemo(() => { + return Array.from(new Set(locations.map(l => l.locationname))).sort(); + }, [locations]); return ( -
+
{/* Immersive Background Blur Blobs */}
@@ -395,10 +540,10 @@ export default function InventoryView({ {activeTab === 'catalog' ? ( <> -
+
{/* ── Sticky Sidebar (Filters) ── */} -
-
+
+

Filter Product @@ -463,7 +608,7 @@ export default function InventoryView({

{/* ── Main Content Area ── */} -
+

@@ -481,34 +626,27 @@ export default function InventoryView({

- {selectedProducts.length > 0 && ( - - )} + + - {filteredProducts.filter(p => p.isNew).length > 0 && ( -
- -

Recently Added

-
- )}
{storesLoading && products.length === 0 ? ( -
+
{Array.from({ length: 10 }).map((_, i) => (
@@ -528,9 +666,9 @@ export default function InventoryView({
{/* Left Side: Normal Catalogue */}
-
- {filteredProducts.filter(p => !p.isNew).map((prod) => ( -
setSelectedAdminProduct(prod)} className="bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-2xl flex flex-col shadow-sm hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-[#662582]/40 hover:-translate-y-1 transition-all duration-300 relative group overflow-hidden cursor-pointer"> +
+ {filteredProducts.map((prod) => ( +
setSelectedAdminProduct(prod)} className="cursor-pointer bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-2xl flex flex-col shadow-sm hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-[#662582]/40 hover:-translate-y-1 transition-all duration-300 relative group overflow-hidden"> {/* Image Section - Top */}
-
- - {prod.category.split(' / ')[0]} - +
+
+ + {prod.category.split(' / ')[0]} + + {prod.isSample && ( + + SAMPLE + + )} +
+ {/* Cannot be removed from local catalogue if it is active in the store catalogue */} + {!storeCat.has(prod.id) && ( + + )} {/* Content Section */}
-
+

{prod.name}

-

{prod.sku}

-
-
{ - e.stopPropagation(); - toggleProduct({ - id: prod.id, - name: prod.name, - sku: prod.sku, - category: prod.category.split(' / ')[0], - price: prod.revenue / Math.max(1, prod.unitsSold), - image: prod.image, - unitsSold: prod.unitsSold, - verified: prod.verified - }); - }} - > - +

{prod.sku}

@@ -599,23 +731,6 @@ export default function InventoryView({
-
- - - {prod.verified ? 'Active Portfolio' : 'Under Inspection'} - - - -
- {storeCat.has(prod.id) ? (
In Store Catalogue @@ -626,13 +741,8 @@ export default function InventoryView({ ) : ( @@ -643,153 +753,16 @@ export default function InventoryView({ ))}
- - {/* Right Side: Recently Added Items */} - {filteredProducts.filter(p => p.isNew).length > 0 && ( -
-
- {filteredProducts.filter(p => p.isNew).map((prod) => ( -
setSelectedAdminProduct(prod)} className="bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-2xl flex flex-col shadow-sm hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-[#662582]/40 hover:-translate-y-1 transition-all duration-300 relative group overflow-hidden cursor-pointer"> - {/* Image Section - Top */} -
- {prod.name} -
-
- - {prod.category.split(' / ')[0]} - -
-
- - {/* Content Section */} -
-
-
-

{prod.name}

-

{prod.sku}

-
-
{ - e.stopPropagation(); - toggleProduct({ - id: prod.id, - name: prod.name, - sku: prod.sku, - category: prod.category.split(' / ')[0], - price: prod.revenue / Math.max(1, prod.unitsSold), - image: prod.image, - unitsSold: prod.unitsSold, - verified: prod.verified - }); - }} - > - -
-
- -
-
- Units Sold - {prod.unitsSold.toLocaleString()} -
-
- Revenue - ₹{prod.revenue.toLocaleString()} -
-
- -
-
- - - {prod.verified ? 'Active Portfolio' : 'Under Inspection'} - - - -
- - {storeCat.has(prod.id) ? ( -
- In Store Catalogue - -
- ) : ( - - )} - -
-
-
- ))} -
-
- )}
)}
- - ) : ( -
+ ) : activeTab === 'import_branding' ? ( +
{/* ── Sticky Sidebar (Filters) ── */} -
+
-
+

Filter Global Product @@ -863,11 +836,11 @@ export default function InventoryView({

{/* ── Main Content Area ── */} -
+

- Super Admin Global Catalogue + Global Catalogue
{filteredGlobalProducts.length} items available @@ -905,26 +878,37 @@ export default function InventoryView({
{/* Left Side: Normal Catalogue */}
-
+
{filteredGlobalProducts.filter(p => !p.isNew).map((prod) => { const isSelected = globalCatalogPicks.has(prod.id); const isAlreadyInCatalog = products.some(p => p.sku === prod.sku); + const wholesalePrice = prod.unitsSold > 0 ? Math.max(1, Math.round(prod.revenue / prod.unitsSold)) : Math.floor(Math.random() * 50) + 20; + const mrp = Math.round(wholesalePrice * 1.35); + const profit = mrp - wholesalePrice; + const profitMargin = Math.round((profit / mrp) * 100); + const globalSales = Math.floor(Math.random() * 5000) + 1000; + const rating = Number((4.0 + Math.random()).toFixed(1)); + + const premiumTagSeed = (prod.name.length + prod.sku.length) % 3; + const premiumTag = premiumTagSeed === 0 ? 'Great Deal' : premiumTagSeed === 1 ? 'High Margin' : 'Trending'; + const tagColor = premiumTag === 'Great Deal' ? 'bg-emerald-100 text-emerald-800 border-emerald-200' : + premiumTag === 'High Margin' ? 'bg-amber-100 text-amber-800 border-amber-200' : + 'bg-indigo-100 text-indigo-800 border-indigo-200'; + const tagIcon = premiumTag === 'Great Deal' ? : + premiumTag === 'High Margin' ? : + ; + const sparklineColor = premiumTag === 'Great Deal' ? 'text-emerald-500' : + premiumTag === 'High Margin' ? 'text-amber-500' : + 'text-indigo-500'; + return (
setSelectedAdminProduct(prod)} + className={`cursor-pointer bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-2xl flex flex-col min-h-[340px] shadow-sm transition-all duration-300 relative group overflow-hidden ${ isSelected ? 'border-[#662582] shadow-[0_0_0_2px_#662582]' : 'hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-[#662582]/40 hover:-translate-y-1' } ${isAlreadyInCatalog ? 'opacity-60 grayscale-[50%]' : ''}`} - onClick={() => { - if (isAlreadyInCatalog) return; - setGlobalCatalogPicks(prev => { - const next = new Set(prev); - if (next.has(prod.id)) next.delete(prod.id); - else next.add(prod.id); - return next; - }); - }} > {/* Image Section - Top */}
@@ -955,46 +939,62 @@ export default function InventoryView({ {/* Content Section */}
-
+

{prod.name}

-

{prod.sku}

+

{prod.sku}

+ + {tagIcon} {premiumTag} + +
+ +
{ + e.stopPropagation(); + toggleProduct({ + id: prod.id, + name: prod.name, + sku: prod.sku, + category: prod.category.split(' / ')[0], + price: mrp, + image: prod.image, + wholesalePrice, + mrp, + profitMargin, + rating, + globalSales, + isGlobal: true, + verified: prod.verified + }); + }} + > +
- {!isAlreadyInCatalog && ( -
{ - e.stopPropagation(); - setGlobalCatalogPicks(prev => { - const next = new Set(prev); - if (next.has(prod.id)) next.delete(prod.id); - else next.add(prod.id); - return next; - }); - }} - > - -
- )}
-
-
- Global Sales - {Math.floor(Math.random() * 5000) + 1000} -
-
- Rating - {(4.0 + Math.random()).toFixed(1)} ★ -
-
+
+
+ Wholesale + ₹{wholesalePrice} +
+
+ Margin + +{profitMargin}% +
+
+ Retail + ₹{mrp} +
+
+
@@ -1020,6 +1020,22 @@ export default function InventoryView({ Import to Catalogue )} +
@@ -1033,24 +1049,31 @@ export default function InventoryView({
{filteredGlobalProducts.filter(p => p.isNew).map((prod) => { - const isSelected = globalCatalogPicks.has(prod.id); const isAlreadyInCatalog = products.some(p => p.sku === prod.sku); + const wholesalePrice = prod.unitsSold > 0 ? Math.max(1, Math.round(prod.revenue / prod.unitsSold)) : Math.floor(Math.random() * 50) + 20; + const mrp = Math.round(wholesalePrice * 1.35); + const profitMargin = Math.round(((mrp - wholesalePrice) / mrp) * 100); + const globalSales = Math.floor(Math.random() * 5000) + 1000; + const rating = Number((4.0 + Math.random()).toFixed(1)); + + const premiumTagSeed = (prod.name.length + prod.sku.length) % 3; + const premiumTag = premiumTagSeed === 0 ? 'Great Deal' : premiumTagSeed === 1 ? 'High Margin' : 'Trending'; + const tagColor = premiumTag === 'Great Deal' ? 'bg-emerald-100 text-emerald-800 border-emerald-200' : + premiumTag === 'High Margin' ? 'bg-amber-100 text-amber-800 border-amber-200' : + 'bg-indigo-100 text-indigo-800 border-indigo-200'; + const tagIcon = premiumTag === 'Great Deal' ? : + premiumTag === 'High Margin' ? : + ; + const sparklineColor = premiumTag === 'Great Deal' ? 'text-emerald-500' : + premiumTag === 'High Margin' ? 'text-amber-500' : + 'text-indigo-500'; + return (
{ - if (isAlreadyInCatalog) return; - setGlobalCatalogPicks(prev => { - const next = new Set(prev); - if (next.has(prod.id)) next.delete(prod.id); - else next.add(prod.id); - return next; - }); - }} + onClick={() => setSelectedAdminProduct(prod)} + className={`bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-2xl flex flex-col min-h-[340px] shadow-sm transition-all duration-300 relative group overflow-hidden hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-[#662582]/40 hover:-translate-y-1 ${isAlreadyInCatalog ? 'opacity-60 grayscale-[50%]' : ''}`} > {/* Image Section - Top */}
@@ -1081,46 +1104,62 @@ export default function InventoryView({ {/* Content Section */}
-
+

{prod.name}

-

{prod.sku}

+

{prod.sku}

+ + {tagIcon} {premiumTag} + +
+ +
{ + e.stopPropagation(); + toggleProduct({ + id: prod.id, + name: prod.name, + sku: prod.sku, + category: prod.category.split(' / ')[0], + price: mrp, + image: prod.image, + wholesalePrice, + mrp, + profitMargin, + rating, + globalSales, + isGlobal: true, + verified: prod.verified + }); + }} + > +
- {!isAlreadyInCatalog && ( -
{ - e.stopPropagation(); - setGlobalCatalogPicks(prev => { - const next = new Set(prev); - if (next.has(prod.id)) next.delete(prod.id); - else next.add(prod.id); - return next; - }); - }} - > - -
- )}
-
-
- Global Sales - {Math.floor(Math.random() * 5000) + 1000} -
-
- Rating - {(4.0 + Math.random()).toFixed(1)} ★ -
-
+
+
+ Wholesale + ₹{wholesalePrice} +
+
+ Margin + +{profitMargin}% +
+
+ Retail + ₹{mrp} +
+
+
@@ -1146,6 +1185,22 @@ export default function InventoryView({ Import to Catalogue )} +
@@ -1157,7 +1212,129 @@ export default function InventoryView({
- )} + ) : activeTab === 'requests' ? ( +
+
+
+

+ Pending Stock Requests +

+

Store Branch Inventory Requests

+
+
+ + +
+
+ {flattenedRequests.length === 0 ? ( +
+ +

No Pending Requests

+

All store stock requests have been processed.

+
+ ) : ( +
+
+ + + + {['Requested At', 'Store', 'Product', 'Qty', 'Status', 'Resolved At', ''].map((h, i) => ( + + ))} + + + + {flattenedRequests.map((req, idx) => { + const isApproved = req.pickData.status === 'Approved'; + const isRejected = req.pickData.status === 'Rejected'; + const isPending = req.pickData.status === 'Pending'; + const isCancelled = req.pickData.status === 'Cancelled'; + const color = isApproved ? '#10b981' : isRejected ? '#f43f5e' : isCancelled ? '#94a3b8' : '#f59e0b'; + const DIVIDER_C = '#f1f5f9'; + + return ( + { e.currentTarget.style.background = SURFACE_ALT; }} onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}> + + + + + + + + + ); + })} + +
{h}
+ + {req.pickData.requestedAt ? new Date(req.pickData.requestedAt).toLocaleString('en-IN', { dateStyle: 'medium', timeStyle: 'short' }) : '—'} + + + + {req.locationname} + + +
+ {req.product?.name} +
+

{req.product?.name}

+

{req.product?.sku}

+
+
+
+ {req.pickData.qty || '—'} + + + + + {req.pickData.resolvedAt ? new Date(req.pickData.resolvedAt).toLocaleString('en-IN', { dateStyle: 'medium', timeStyle: 'short' }) : '—'} + + + {isPending ? ( +
+ + +
+ ) : ( + + Processed + + )} +
+
+
+ )} +
+ ) : null} {/* ── Slide Drawer ── */} setSelectedAdminProduct(null)} title="Administrative Product Details" > - {selectedAdminProduct && ( -
-
- {selectedAdminProduct.name} -
- - {selectedAdminProduct.category} - -
-
+ {selectedAdminProduct && (() => { + const wholesalePrice = selectedAdminProduct.unitsSold > 0 ? Math.max(1, Math.round(selectedAdminProduct.revenue / selectedAdminProduct.unitsSold)) : Math.floor(Math.random() * 50) + 20; + const mrp = Math.round(wholesalePrice * 1.35); + const profit = mrp - wholesalePrice; + const profitMargin = Math.round((profit / mrp) * 100); + const isAlreadyInCatalog = products.some(p => p.sku === selectedAdminProduct.sku); + + return ( +
-
-

{selectedAdminProduct.name}

-

{selectedAdminProduct.sku}

+ {/* Clean Image Container */} +
+ {selectedAdminProduct.name}
-
- - - {selectedAdminProduct.verified ? 'Active Portfolio Item' : 'Under Inspection (Not Published)'} - + {/* Title & Basics */} +
+

SKU: {selectedAdminProduct.sku}

+

{selectedAdminProduct.name}

+
+ + {String(selectedAdminProduct.category || '').split(' / ')[0]} + + {isAlreadyInCatalog && ( + + In Catalogue + + )} +
- -
-
- Global Revenue - - ₹{selectedAdminProduct.revenue.toLocaleString('en-IN')} + + {/* Clean Margin Engine */} +
+

+ Margin Summary +

+ +
+
+ Wholesale Price +
₹{wholesalePrice}
+
+ +
+ +
+ Retail (MRP) +
₹{mrp}
+
+ +
+ +
+ Your Profit +
₹{profit} ({profitMargin}%)
+
+
+
+ + {/* Minimal Global Stats */} +
+
+ + Monthly Sales + + + {Math.floor(Math.random() * 5000) + 1000} Units
-
- Units Sold - {selectedAdminProduct.unitsSold.toLocaleString('en-IN')} +
+ + Global Rating + + + {(4.0 + Math.random()).toFixed(1)} +
- {storeCat.has(selectedAdminProduct.id) ? ( -
-
- - In Store Catalogue + {/* Action Area */} +
+ {isAlreadyInCatalog ? ( +
+
+ + Synced to Local Catalogue +
+ {storeCat.has(selectedAdminProduct.id) ? ( + + ) : ( + + )} + {!storeCat.has(selectedAdminProduct.id) && ( + + )} +
+ ) : ( +
+ + +
+ )} + +
+ {!isAlreadyInCatalog && ( + + )}
-
- ) : ( - - )}

- This product is part of the Global Catalogue. Changes here affect visibility and availability across all outlets. + This product is part of the Global Catalogue. Importing it will add it to your local inventory where you can manage pricing and availability.

+ +
+

Retail Packaging Info

+ +
- )} + ); + })()} + + + {/* ── Slide Drawer for Stock Requests ── */} + setSelectedRequest(null)} + title="Stock Request Details" + > + {selectedRequest && (() => { + const req = selectedRequest; + const isPending = req.pickData.status === 'Pending'; + const isApproved = req.pickData.status === 'Approved'; + const isRejected = req.pickData.status === 'Rejected'; + const isCancelled = req.pickData.status === 'Cancelled'; + const prod = req.product; + + return ( +
+ {prod ? ( +
+ {prod.name} +
+ + {String(prod.category || '').split(' / ')[0]} + +
+
+ ) : ( +
+ Product Image Not Found +
+ )} + +
+

{prod?.name || 'Unknown Product'}

+

{prod?.sku || `SKU-${req.productid}`}

+ +
+
+ Requesting Store + {req.locationname} +
+
+ Requested Qty + {req.pickData.qty} Units +
+
+ Status + + {req.pickData.status} + +
+
+
+ + {/* Action Area */} +
+

Process Request

+ + {isPending ? ( +
+ + +
+ ) : ( +
+ + This request has been {req.pickData.status.toLowerCase()}. + +
+ )} +
+ + {prod && ( +
+

Retail Packaging Info

+ +
+ )} +
+ ); + })()}
setTrialProducts([])} product={trialProducts[0] || null} tenantId={tenantId} - /> - - setIsCartOpen(false)} - onRequestSamples={() => { - setTrialProducts(selectedProducts.map(p => ({ - id: p.id, - name: p.name, - sku: p.sku, - category: p.category, - price: p.price, - image: p.image - }))); + onSuccess={(tp) => { + const globalProd = MOCK_GLOBAL_CATALOG.find(p => p.id === tp.id); + if (globalProd) { + setProducts(prev => { + if (prev.some(p => p.id === globalProd.id)) return prev; + return [{ ...globalProd, isSample: true, isNew: true }, ...prev]; + }); + setActiveTab('catalog'); + } }} /> - {/* ── Floating Cart Button ── */} - {selectedProducts.length > 0 && ( + {/* ── Floating Import Button ── */} + {activeTab === 'import_branding' && selectedProducts.length > 0 && ( )} + +
); } diff --git a/src/components/ReportsView.tsx b/src/components/ReportsView.tsx index 15bc249..d475ba7 100644 --- a/src/components/ReportsView.tsx +++ b/src/components/ReportsView.tsx @@ -39,6 +39,7 @@ import { import { FIESTA_TENANT_ID, FIESTA_PRIMARY_LOCATION_ID, num as fnum, str as fstr, ymd } from '../services/fiestaApi'; import { stockRowToProduct } from '../services/fiestaMappers'; import AwaitingApi from './AwaitingApi'; +import SalesRevenueReport from './SalesRevenueReport'; interface ReportsViewProps { searchQuery: string; @@ -61,6 +62,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba const [expandedProductId, setExpandedProductId] = useState(null); const [exportingFormat, setExportingFormat] = useState<'PDF' | 'CSV' | null>(null); const [exportProgress, setExportProgress] = useState(0); + const [activeTab, setActiveTab] = useState<'overview' | 'sales_revenue'>('overview'); // Dropdown open states const [showTimeframeDropdown, setShowTimeframeDropdown] = useState(false); @@ -487,8 +489,23 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba return (
+
+ + +
- + {activeTab === 'overview' ? ( + <> {/* Primary KPI Row - 4 Key Tab buttons with Sparklines */}
{reportsKPIs.map((kpi) => { @@ -1077,7 +1094,10 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
)} - + + ) : ( + + )}
); } diff --git a/src/components/SalesRevenueReport.tsx b/src/components/SalesRevenueReport.tsx new file mode 100644 index 0000000..1b64055 --- /dev/null +++ b/src/components/SalesRevenueReport.tsx @@ -0,0 +1,605 @@ +/** + * @license + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useMemo, useState } from 'react'; +import { + IndianRupee, + ShoppingBag, + TrendingUp, + Download, + Calendar, + MapPin, + Loader2, + ChevronLeft, + ChevronRight, + TrendingDown +} from 'lucide-react'; +import { + useFiestaAllOrders +} from '../services/fiestaQueries'; +import { FIESTA_TENANT_ID, num as fnum, str as fstr, ymd, type Row } from '../services/fiestaApi'; + +const generateMockOrders = (from: string, to: string, locationid?: number): Row[] => { + const fromTime = new Date(from).getTime(); + const toTime = new Date(to).getTime(); + const mockOrders: Row[] = []; + + const locations = [ + { id: 1166, name: 'Main Hub' }, + { id: 1167, name: 'Downtown Branch' }, + { id: 1168, name: 'Westside Market' } + ]; + const paymentModes = ['CASH', 'UPI', 'UPI', 'CARD']; + const statuses = ['delivered', 'delivered', 'delivered', 'completed', 'cancelled']; + const customers = ['John Doe', 'Jane Smith', 'Acme Corp', 'Bob Johnson', 'Local Cafe', 'John Doe']; + + // Seeded random for stable mock data across renders + let seed = 12345; + const random = () => { + seed = (seed * 9301 + 49297) % 233280; + return seed / 233280; + }; + + for (let i = 0; i < 65; i++) { + const loc = locationid + ? locations.find(l => l.id === locationid) || { id: locationid, name: `Store ${locationid}` } + : locations[Math.floor(random() * locations.length)]; + const time = new Date(fromTime + random() * (toTime - fromTime + 86400000)); + + // Force peaks around lunch and dinner + if (random() > 0.5) time.setHours(11 + Math.floor(random() * 3)); // 11am-2pm + else time.setHours(17 + Math.floor(random() * 4)); // 5pm-9pm + + mockOrders.push({ + orderid: `ORD-MOCK-${8400 + i}`, + orderdate: time.toISOString(), + orderstatus: statuses[Math.floor(random() * statuses.length)], + ordervalue: Math.floor(random() * 2500) + 150, + quantity: Math.floor(random() * 8) + 1, + paymentmode: paymentModes[Math.floor(random() * paymentModes.length)], + locationid: loc.id, + applocation: loc.name, + deliverycustomer: customers[Math.floor(random() * customers.length)], + }); + } + return mockOrders; +}; + +// ... inside the component ... +import { shortTime } from '../services/fiestaMappers'; +import { + KpiStrip, Pill, FilterBar, TH_STYLE, + BRAND, BRAND_LIGHT, TEXT, TEXT_2, TEXT_3, BORDER, DIVIDER, SURFACE_ALT, + tint, soft, edge, ring +} from './consoleUi'; +import { ResponsiveContainer, AreaChart, Area, BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, Tooltip as RechartsTooltip, CartesianGrid } from 'recharts'; + +interface SalesRevenueReportProps { + locationid?: number; + tenantId?: number; +} + +const PAGE_SIZE = 20; + +export default function SalesRevenueReport({ + locationid, + tenantId = FIESTA_TENANT_ID, +}: SalesRevenueReportProps) { + const today = new Date(); + const monthStart = new Date(today.getFullYear(), today.getMonth(), 1); + const todayStr = ymd(today); + + const dayOffset = (n: number) => { const d = new Date(); d.setDate(d.getDate() - n); return ymd(d); }; + + const presets = [ + { key: 'today', label: 'Today', from: todayStr, to: todayStr }, + { key: '7d', label: 'Last 7 Days', from: dayOffset(6), to: todayStr }, + { key: '30d', label: 'Last 30 Days', from: dayOffset(29), to: todayStr }, + { key: 'month', label: 'This Month', from: ymd(monthStart), to: todayStr }, + ]; + + const [fromdate, setFromdate] = useState(presets[1].from); + const [todate, setTodate] = useState(todayStr); + const [pageno, setPageno] = useState(1); + const [storeFilter, setStoreFilter] = useState('All'); + + const activePreset = presets.find((p) => p.from === fromdate && p.to === todate)?.key ?? 'custom'; + + // ── Queries ────────────────────────────────────────────────────────────────── + const allOrdersQ = useFiestaAllOrders({ tenantid: tenantId, fromdate, todate, locationid }); + const mockData = useMemo(() => generateMockOrders(fromdate, todate, locationid), [fromdate, todate, locationid]); + const allRows = allOrdersQ.data && allOrdersQ.data.length > 0 ? allOrdersQ.data : mockData; + + // ── Calculations ───────────────────────────────────────────────────────────── + // Filter only delivered orders for actual sales/revenue calculation + const completedOrders = useMemo(() => { + return allRows.filter((r) => { + const s = fstr(r.orderstatus).toLowerCase(); + return s === 'delivered' || s === 'completed'; + }); + }, [allRows]); + + const { totalRevenue, totalOrders } = useMemo(() => { + let rev = 0; + let ords = 0; + for (const r of completedOrders) { + if (locationid) { + const rLoc = fnum(r.locationid); + const rApp = fnum(r.applocationid); + if (rLoc !== locationid && rApp !== locationid) continue; + } + + const amt = fnum(r.ordervalue) || fnum(r.orderamount) || fnum(r.deliveryamt); + rev += amt; + ords += 1; + } + return { totalRevenue: rev, totalOrders: ords }; + }, [completedOrders, locationid]); + + const aov = totalOrders > 0 ? totalRevenue / totalOrders : 0; + + // Chart data (Group by date) + const chartData = useMemo(() => { + const map = new Map(); + + for (const r of completedOrders) { + if (locationid) { + const rLoc = fnum(r.locationid); + const rApp = fnum(r.applocationid); + if (rLoc !== locationid && rApp !== locationid) continue; + } + + const dateVal = fstr(r.orderdate) || fstr(r.deliverydate); + if (!dateVal) continue; + + const key = dateVal.split('T')[0]; + const ex = map.get(key) || { date: key, revenue: 0, orders: 0 }; + + const amt = fnum(r.ordervalue) || fnum(r.orderamount) || fnum(r.deliveryamt); + ex.revenue += amt; + ex.orders += 1; + + map.set(key, ex); + } + + return Array.from(map.values()).sort((a, b) => a.date.localeCompare(b.date)); + }, [completedOrders, locationid]); + + const formatLabel = (key: string) => { + const d = new Date(key); + const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + return `${d.getDate()} ${months[d.getMonth()]}`; + }; + + // 1. Customer Retention + const { uniqueCustomers, repeatCustomers } = useMemo(() => { + const custCount = new Map(); + for (const r of completedOrders) { + if (locationid) { + const rLoc = fnum(r.locationid); + const rApp = fnum(r.applocationid); + if (rLoc !== locationid && rApp !== locationid) continue; + } + const custStr = fstr(r.deliverycustomer) || fstr(r.pickupcustomer) || fstr(r.customerphone) || fstr(r.tenantname); + if (custStr) { + custCount.set(custStr, (custCount.get(custStr) || 0) + 1); + } + } + let repeat = 0; + for (const cnt of custCount.values()) { + if (cnt > 1) repeat++; + } + return { uniqueCustomers: custCount.size, repeatCustomers: repeat }; + }, [completedOrders, locationid]); + + // 2. Hourly peak + const hourlyData = useMemo(() => { + const hours = new Array(24).fill(0).map((_, i) => ({ hour: `${String(i).padStart(2, '0')}:00`, orders: 0 })); + for (const r of completedOrders) { + if (locationid) { + const rLoc = fnum(r.locationid); + const rApp = fnum(r.applocationid); + if (rLoc !== locationid && rApp !== locationid) continue; + } + const d = fstr(r.orderdate) || fstr(r.deliverydate); + if (d) { + const h = new Date(d).getHours(); + if (!isNaN(h)) { + hours[h].orders += 1; + } + } + } + return hours.filter(h => h.orders > 0 || (parseInt(h.hour) >= 8 && parseInt(h.hour) <= 20)); + }, [completedOrders, locationid]); + + // 3. Payment Split + const paymentData = useMemo(() => { + const pmap = new Map(); + for (const r of completedOrders) { + if (locationid) { + const rLoc = fnum(r.locationid); + const rApp = fnum(r.applocationid); + if (rLoc !== locationid && rApp !== locationid) continue; + } + const p = (fstr(r.paymentmode) || fstr(r.paymenttype) || 'CASH').toUpperCase(); + const cleanP = p.includes('UPI') ? 'UPI' : p.includes('CARD') ? 'CARD' : p; + pmap.set(cleanP, (pmap.get(cleanP) || 0) + 1); + } + const colors = ['#6366f1', '#10b981', '#f59e0b', '#ec4899', '#8b5cf6']; + return Array.from(pmap.entries()).map(([name, value], i) => ({ + name, value, color: colors[i % colors.length] + })); + }, [completedOrders, locationid]); + + // 4. Top Locations Leaderboard (Admin Only) + const topLocations = useMemo(() => { + if (locationid) return []; // Only for admin + const lmap = new Map(); + for (const r of completedOrders) { + const locStr = fstr(r.applocation) || fstr(r.locationname) || `Location ${fstr(r.locationid) || fstr(r.applocationid)}`; + const amt = fnum(r.ordervalue) || fnum(r.orderamount) || fnum(r.deliveryamt); + lmap.set(locStr, (lmap.get(locStr) || 0) + amt); + } + return Array.from(lmap.entries()) + .map(([name, revenue]) => ({ name, revenue })) + .sort((a, b) => b.revenue - a.revenue) + .slice(0, 5); // Top 5 + }, [completedOrders, locationid]); + + const kpis = [ + { label: 'Total Sales', value: totalOrders.toLocaleString('en-IN'), color: '#6366f1', icon: , badge: 'Completed orders' }, + { label: 'Total Revenue', value: `₹${totalRevenue.toLocaleString('en-IN')}`, color: '#10b981', icon: , badge: 'Total collected' }, + { label: 'Avg Order Value', value: `₹${Math.round(aov).toLocaleString('en-IN')}`, color: '#8b5cf6', icon: , badge: 'Per order' }, + { label: 'Unique Customers', value: uniqueCustomers.toLocaleString('en-IN'), color: '#f59e0b', icon: , badge: `${repeatCustomers} repeat` }, + ]; + + // Table Data + const tableRows = useMemo(() => { + return completedOrders.filter(r => { + if (locationid) { + const rLoc = fnum(r.locationid); + const rApp = fnum(r.applocationid); + if (rLoc !== locationid && rApp !== locationid) return false; + } + if (storeFilter !== 'All') { + const locStr = fstr(r.applocation) || fstr(r.locationname) || `Location ${fstr(r.locationid) || fstr(r.applocationid)}`; + if (locStr !== storeFilter) return false; + } + return true; + }).sort((a, b) => { + const d1 = new Date(fstr(b.orderdate) || fstr(b.deliverydate)).getTime(); + const d2 = new Date(fstr(a.orderdate) || fstr(a.deliverydate)).getTime(); + return d1 - d2; + }); + }, [completedOrders, locationid, storeFilter]); + + const uniqueStoreNames = useMemo(() => { + const stores = new Set(); + for (const r of completedOrders) { + if (locationid) continue; + const locStr = fstr(r.applocation) || fstr(r.locationname) || `Location ${fstr(r.locationid) || fstr(r.applocationid)}`; + if (locStr) stores.add(locStr); + } + return Array.from(stores).sort(); + }, [completedOrders, locationid]); + + const pageRows = useMemo( + () => tableRows.slice((pageno - 1) * PAGE_SIZE, pageno * PAGE_SIZE), + [tableRows, pageno] + ); + + const hasNext = tableRows.length > pageno * PAGE_SIZE; + + // CSV export + const exportCsv = () => { + const headers = ['#', 'Order ID', 'Branch', 'Date', 'Customer', 'Qty', 'Payment', 'Amount (₹)']; + const esc = (v: unknown) => `"${fstr(v).replace(/"/g, '""')}"`; + const lines = tableRows.map((r, i) => [ + i + 1, + fstr(r.orderid) || fstr(r.orderheaderid), + fstr(r.applocation) || fstr(r.locationname), + shortTime(r.orderdate || r.deliverydate), + fstr(r.deliverycustomer) || fstr(r.pickupcustomer) || fstr(r.tenantname), + fnum(r.quantity), + (fstr(r.paymentmode) || fstr(r.paymenttype) || 'CASH').toUpperCase(), + fnum(r.ordervalue) || fnum(r.orderamount) || fnum(r.deliveryamt), + ].map(esc).join(',')); + const blob = new Blob([[headers.join(','), ...lines].join('\n')], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); a.href = url; + a.download = `Sales_Revenue_${fromdate}_to_${todate}.csv`; a.click(); + URL.revokeObjectURL(url); + }; + + return ( +
+ + +
+ + +
+
+ + Period + + {presets.map((p) => ( + + { setFromdate(p.from); setTodate(p.to); setPageno(1); }}> + {p.label} + + + ))} +
+ +
+
+ { setFromdate(e.target.value); setPageno(1); }} className="rounded-full outline-none font-semibold text-xs transition-colors" style={{ padding: '6px 12px', border: `1.5px solid ${edge(BRAND)}`, background: tint(BRAND), color: BRAND }} /> + + { setTodate(e.target.value); setPageno(1); }} className="rounded-full outline-none font-semibold text-xs transition-colors" style={{ padding: '6px 12px', border: `1.5px solid ${edge(BRAND)}`, background: tint(BRAND), color: BRAND }} /> +
+ +
+
+
+ +
+
+
+ +

Revenue Trend

+
+
+ {allOrdersQ.isLoading ? ( +
+ +
+ ) : chartData.length === 0 ? ( +
No revenue data for this period.
+ ) : ( + + + + + + + + + + + `₹${v.toLocaleString('en-IN')}`} /> + { + if (active && payload && payload.length) { + return ( +
+

{formatLabel(label)}

+
+ + + Revenue + + + ₹{payload[0].value.toLocaleString('en-IN')} + + + {payload[0].payload.orders} orders + +
+
+ ); + } + return null; + }} + /> + +
+
+ )} +
+
+
+ +
+
+
+ +

Hourly Sales Distribution

+
+
+ {allOrdersQ.isLoading ? ( +
+ ) : hourlyData.length === 0 ? ( +
No data for this period.
+ ) : ( + + + + + + { + if (active && payload && payload.length) { + return ( +
+

{label}

+ {payload[0].value} orders +
+ ); + } + return null; + }} /> + +
+
+ )} +
+
+ +
+
+ +

Payment Methods

+
+
+ {allOrdersQ.isLoading ? ( +
+ ) : paymentData.length === 0 ? ( +
No payment data available.
+ ) : ( + + + + {paymentData.map((entry, index) => ( + + ))} + + { + if (active && payload && payload.length) { + return ( +
+
+ {payload[0].name}: + {payload[0].value} +
+ ); + } + return null; + }} /> +
+
+ )} +
+
+
+ + {!locationid && topLocations.length > 0 && ( +
+
+ +

Top Locations by Revenue

+
+
+ {topLocations.map((loc, i) => ( +
+ {loc.name} + ₹{loc.revenue.toLocaleString('en-IN')} +
+ ))} +
+
+ )} + +
+
+
+ +

Completed Order Ledger

+
+ {!locationid && uniqueStoreNames.length > 0 && ( + + )} +
+
+ + + + {['#', 'Order', 'Branch', 'Customer', 'Qty', 'Payment', 'Amount (₹)'].map((h, i) => ( + + ))} + + + + {allOrdersQ.isLoading ? ( + + ) : pageRows.length === 0 ? ( + + ) : ( + pageRows.map((r, i) => { + const amount = fnum(r.ordervalue) || fnum(r.orderamount) || fnum(r.deliveryamt); + return ( + (e.currentTarget.style.background = SURFACE_ALT)} + onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')} + > + + + + + + + + + ); + }) + )} + +
{h}
+ + Loading data… + +
+ No completed orders found for this date range. +
{(pageno - 1) * PAGE_SIZE + i + 1} +

{fstr(r.orderid) || `#${fstr(r.orderheaderid)}`}

+

{shortTime(r.orderdate || r.deliverydate)}

+
+ + {fstr(r.applocation) || fstr(r.locationname) || '—'} + + +

{fstr(r.deliverycustomer) || fstr(r.pickupcustomer) || fstr(r.tenantname) || '—'}

+
{fnum(r.quantity) || '—'} + + {(fstr(r.paymentmode) || fstr(r.paymenttype) || 'CASH').toUpperCase()} + + + {amount > 0 ? `₹${amount.toLocaleString('en-IN')}` : '—'} +
+
+ + {/* Pagination */} +
+ + Page {pageno} · {pageRows.length} of {tableRows.length} shown + +
+ + +
+
+
+
+ ); +} diff --git a/src/components/StoreCatalogView.tsx b/src/components/StoreCatalogView.tsx index 60ee8eb..c1f6daf 100644 --- a/src/components/StoreCatalogView.tsx +++ b/src/components/StoreCatalogView.tsx @@ -20,14 +20,14 @@ */ import React, { useEffect, useMemo, useState } from 'react'; -import { Search, Boxes, Layers, Plus, Minus, Check, CheckCircle2, X, Store, PackageSearch, Activity } from 'lucide-react'; -import { useFiestaStockStatement, FIESTA_TENANT_ID } from '../services/fiestaQueries'; -import { num as fnum, str as fstr, type Row } from '../services/fiestaApi'; +import { Search, Boxes, Layers, Plus, Minus, Check, CheckCircle2, X, Store, PackageSearch, Activity, Info, Inbox } from 'lucide-react'; +import { useFiestaStockStatement, useFiestaCreateStockRequest, FIESTA_TENANT_ID } from '../services/fiestaQueries'; +import { num as fnum, str as fstr, type Row, FIESTA_PRIMARY_LOCATION_ID } from '../services/fiestaApi'; import { categoryName } from '../services/fiestaMappers'; import { useStoreCatalogue } from '../services/storeCatalogue'; import AwaitingApi from './AwaitingApi'; -import { SlideDrawer } from './consoleUi'; -import { useCompare } from '../contexts/CompareContext'; +import { SlideDrawer, StatusChip, TH_STYLE, SURFACE_ALT, TEXT, TEXT_2, TEXT_3, BORDER, BRAND } from './consoleUi'; +import FMCGHoverOverlay from './FMCGHoverOverlay'; const PLACEHOLDER = 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&q=80&w=200'; @@ -46,7 +46,7 @@ function stockStatus(closing: number): { label: string; color: string } { /** Category → pill badge classes (mirrors the admin Global Catalogue card). */ function catBadgeClass(category: string): string { - const c = category.toLowerCase(); + const c = String(category || '').toLowerCase(); if (c.startsWith('staple')) return 'bg-amber-50 text-amber-600 border border-amber-100'; if (c.includes('grocer')) return 'bg-emerald-50 text-emerald-600 border border-emerald-100'; if (c.includes('beverage')) return 'bg-sky-50 text-sky-600 border border-sky-100'; @@ -54,13 +54,15 @@ function catBadgeClass(category: string): string { } export default function StoreCatalogView({ locationid, storeName = 'your store', tenantId = FIESTA_TENANT_ID }: StoreCatalogViewProps) { - const { selectedProducts, toggleProduct, setIsComparing } = useCompare(); const tenantid = tenantId; - const [view, setView] = useState<'catalogue' | 'inventory'>('catalogue'); + const [view, setView] = useState<'catalogue' | 'inventory' | 'requests'>('catalogue'); const [search, setSearch] = useState(''); const [selectedCategories, setSelectedCategories] = useState([]); const [stockHealthFilter, setStockHealthFilter] = useState([]); const [selectedProduct, setSelectedProduct] = useState(null); + const [hoveredProduct, setHoveredProduct] = useState(null); + const [activeQtyProduct, setActiveQtyProduct] = useState(null); + const [tempQty, setTempQty] = useState(1); const [notice, setNotice] = useState(false); // The admin-curated catalogue (what the user is allowed to pick from). @@ -80,38 +82,109 @@ export default function StoreCatalogView({ locationid, storeName = 'your store', [storeCat.items], ); - // The user's picks: productid → quantity they need. Persisted per store. + // The user's picks: productid → request data. Persisted per store. const storageKey = `nearledaily.catalogue.request.${locationid ?? 'na'}`; - const [picks, setPicks] = useState>(() => { + const [picks, setPicks] = useState>(() => { try { const raw = localStorage.getItem(storageKey); - return raw ? (JSON.parse(raw) as Record) : {}; + if (!raw) return {}; + const parsed = JSON.parse(raw); + // Migrate old format (Record) to new format + const migrated: Record = {}; + for (const [k, v] of Object.entries(parsed)) { + if (typeof v === 'number') { + migrated[k] = { qty: v, status: 'Pending', requestedAt: new Date().toISOString() }; + } else { + migrated[k] = v; + if (migrated[k].status === 'Approve') migrated[k].status = 'Approved'; + if (migrated[k].status === 'Reject') migrated[k].status = 'Rejected'; + } + } + return migrated; } catch { return {}; } }); + + // Listen for storage events so approvals from Admin update dynamically useEffect(() => { - try { localStorage.setItem(storageKey, JSON.stringify(picks)); } catch { /* ignore */ } + const handler = (e: StorageEvent) => { + if (e.key === storageKey) { + if (e.newValue) { + const parsed = JSON.parse(e.newValue); + for (const key of Object.keys(parsed)) { + if (parsed[key].status === 'Approve') parsed[key].status = 'Approved'; + if (parsed[key].status === 'Reject') parsed[key].status = 'Rejected'; + } + setPicks(parsed); + } + else setPicks({}); + } + }; + window.addEventListener('storage', handler); + return () => window.removeEventListener('storage', handler); + }, [storageKey]); + + useEffect(() => { + localStorage.setItem(storageKey, JSON.stringify(picks)); }, [picks, storageKey]); + const createRequestMutation = useFiestaCreateStockRequest(); + const togglePick = (id: string) => { setNotice(false); setPicks((prev) => { const next = { ...prev }; - if (next[id] != null) delete next[id]; - else next[id] = 1; + if (next[id] != null && next[id].status !== 'Cancelled') { + next[id] = { ...next[id], status: 'Cancelled', resolvedAt: new Date().toISOString() }; + } else { + next[id] = { qty: 1, status: 'Pending', requestedAt: new Date().toISOString() }; + createRequestMutation.mutate({ + tenantid, + locationid: locationid ?? FIESTA_PRIMARY_LOCATION_ID, + productid: Number(id), + qty: 1, + status: 'Pending' + }); + } return next; }); }; - const setPickQty = (id: string, qty: number) => setPicks((prev) => ({ ...prev, [id]: Math.max(1, Math.round(qty) || 1) })); + const setPickQty = (id: string, qty: number) => setPicks((prev) => { + const existing = prev[id] || {}; + const safeQty = Math.max(1, Math.round(qty) || 1); + createRequestMutation.mutate({ + tenantid, + locationid: locationid ?? FIESTA_PRIMARY_LOCATION_ID, + productid: Number(id), + qty: safeQty, + status: 'Pending' + }); + return { + ...prev, + [id]: { + ...existing, + qty: safeQty, + status: 'Pending', + requestedAt: new Date().toISOString() + } + }; + }); const pickCount = Object.keys(picks).length; // Store inventory (live stock) for the "My Store Inventory" tab + "In Store" tags. const stockQ = useFiestaStockStatement({ tenantid, locationid: locationid ?? 0, pagesize: 200 }); - const inStore = useMemo(() => new Set((stockQ.data ?? []).map((r) => fstr(r.productid))), [stockQ.data]); + + const inStore = useMemo(() => { + const set = new Set((stockQ.data ?? []).map((r) => fstr(r.productid))); + Object.entries(picks).forEach(([pid, data]: [string, any]) => { + if (data.status === 'Approved') set.add(pid); + }); + return set; + }, [stockQ.data, picks]); const inventory = useMemo( - () => - (stockQ.data ?? []).map((r: Row) => { + () => { + const baseInventory = (stockQ.data ?? []).map((r: Row) => { const closing = fnum(r.closing) ?? 0; return { id: fstr(r.productid), @@ -121,10 +194,45 @@ export default function StoreCatalogView({ locationid, storeName = 'your store', category: categoryName(fnum(r.categoryid)), closing, ...stockStatus(closing), + price: Math.floor(Math.random() * 50) + 10, // mock price + qty: closing, // fallback if actual qty not mapped }; - }), - [stockQ.data], + }); + + // Merge approved picks into the inventory + const inventoryMap = new Map(baseInventory.map(item => [item.id, item])); + + Object.entries(picks).forEach(([pid, data]: [string, any]) => { + if (data.status === 'Approved') { + if (inventoryMap.has(pid)) { + const item = inventoryMap.get(pid)!; + item.qty += data.qty; + item.closing += data.qty; + Object.assign(item, stockStatus(item.closing)); + } else { + // Find product info from catalogue + const prod = products.find(p => p.id === pid); + if (prod) { + inventoryMap.set(pid, { + id: prod.id, + name: prod.name, + sku: prod.sku, + image: prod.image, + category: prod.category, + ...stockStatus(data.qty), + price: prod.price, + qty: data.qty, + closing: data.qty + }); + } + } + } + }); + return Array.from(inventoryMap.values()); + }, + [stockQ.data, picks, products], ); + const filteredInventory = useMemo(() => { const term = search.toLowerCase(); if (!term) return inventory; @@ -160,9 +268,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store', }, [filteredInventory, selectedCategories, stockHealthFilter]); // ── Integration point ────────────────────────────────────────────────────────── - // Replace with the real request/stock POST (selected productids + quantities), - // then invalidate stockQ. - const commitSelectionToStore = () => setNotice(true); + // The request is saved to localStorage automatically via the useEffect on `picks`. return (
@@ -186,15 +292,24 @@ export default function StoreCatalogView({ locationid, storeName = 'your store', > My Store Inventory ({inventory.length}) +
{/* Sticky Sidebar Filter */} -
-
-

- Search -

+ {view !== 'requests' && ( +
+
+

+ Search +

)}
+ )} {/* Product Grid Area */}
@@ -297,11 +413,13 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',

- {view === 'catalogue' ? 'Global Catalogue' : 'My Inventory'} + {view === 'catalogue' ? 'Store Catalogue' : view === 'inventory' ? 'My Inventory' : 'My Requests'}

-

- Showing {view === 'catalogue' ? filtered.length : finalFilteredInventory.length} results -

+ {view !== 'requests' && ( +

+ Showing {view === 'catalogue' ? filtered.length : finalFilteredInventory.length} results +

+ )}
{/* Can add sorting dropdown here if needed */}
@@ -324,9 +442,10 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
{filtered.map((p) => { const stocked = inStore.has(p.id); - const picked = picks[p.id] != null; + const pick = picks[p.id]; + const picked = pick != null && pick.status !== 'Cancelled' && pick.status !== 'Approved'; return ( -
setSelectedProduct(p)} className="bg-white border border-slate-200 rounded-2xl flex flex-col shadow-sm hover:shadow-xl hover:border-purple-300 hover:-translate-y-1.5 transition-all duration-300 relative group cursor-pointer"> +
setSelectedProduct(p)} className="bg-white border border-slate-200 rounded-2xl flex flex-col shadow-sm hover:shadow-xl hover:border-purple-300 hover:-translate-y-1.5 transition-all duration-300 relative group cursor-pointer overflow-hidden"> {/* Image Bento Block */}
{p.name} @@ -353,31 +472,6 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',

{p.name}

{p.sku}

-
{ - e.stopPropagation(); - toggleProduct({ - id: p.id, - name: p.name, - sku: p.sku, - category: p.category.split(' / ')[0], - price: p.price, - image: p.image, - unit: p.unit - }); - }} - > - -
@@ -394,18 +488,35 @@ export default function StoreCatalogView({ locationid, storeName = 'your store', {/* Action Bento Block */}
{picked ? ( -
- -
- - {picks[p.id]} - +
+ + {picks[p.id].status === 'Approved' ? : + picks[p.id].status === 'Rejected' ? : +
} + {picks[p.id].status === 'Pending' ? 'Requested' : picks[p.id].status} ({picks[p.id].qty}) + +
+ {picks[p.id].status === 'Pending' && ( + + )} +
-
) : ( -
- +
+
+ {pickCount} product{pickCount > 1 ? 's' : ''} requested +
- ) : ( - - )} + +
)} - {/* ADD/EDIT PRODUCT MODAL (simulated) */} - setSelectedProduct(null)} - title={view === 'catalogue' ? 'Catalogue Product Details' : 'Store Inventory Status'} - > - {selectedProduct && ( -
-
- {selectedProduct.name} -
- - {selectedProduct.category.split(' / ')[0]} - + + {/* QUANTITY SELECTION CENTERED MODAL */} + {activeQtyProduct && ( +
+
setActiveQtyProduct(null)} + /> +
+ + {/* Header */} +
+
+

Request Stock

+

Bulk Order

-
- -
-

{selectedProduct.name}

-

{selectedProduct.sku}

-
- -
-
- Pricing - - {selectedProduct.price > 0 ? `₹${selectedProduct.price.toLocaleString('en-IN')}` : '—'} - -
-
- Unit - {selectedProduct.unit || 'Piece'} -
+
- {selectedProduct.closing !== undefined && ( -
-
-
- -
-
- Live Stock Level - - {selectedProduct.label} - -
-
- {selectedProduct.closing.toLocaleString('en-IN')} -
- )} + {/* Product Card summary */} +
+
+ {activeQtyProduct.name} +
+
+

{activeQtyProduct.name}

+

{activeQtyProduct.sku}

+
+ ₹{activeQtyProduct.price.toLocaleString('en-IN')} + / + {activeQtyProduct.unit || 'Pc'} +
+
+
+ + {/* Quantity Selector */} +
+
+ +
+ {[10, 25, 50, 100].map(qty => ( + + ))} +
+
+ +
+ +
+
+ +
+ setTempQty(Math.max(1, parseInt(e.target.value) || 1))} + className="w-full h-10 pl-9 pr-12 text-right text-lg font-bold font-mono text-[#662582] bg-slate-50/50 rounded-lg border border-slate-200 outline-none focus:border-[#662582] focus:bg-white transition-all shadow-inner" + /> +
+ Units +
+
+
+
+ + {/* Total & Submit */} +
+
+ Estimated Total + + ₹{(tempQty * activeQtyProduct.price).toLocaleString('en-IN')} + +
+ + +
-

- This is a shared product available in the Global Catalogue. Make sure to keep adequate stock to prevent customer order cancellations due to unavailability. -

- )} +
+ )} + + {/* Floating FMCG Details Panel */} + {hoveredProduct && ( +
+
+ +
+
+ )} + + {/* ── Slide Drawer for Product Details ── */} + setSelectedProduct(null)} + title="Product Details" + > + {selectedProduct && (() => { + const stocked = inStore.has(selectedProduct.id); + const pick = picks[selectedProduct.id]; + const isPending = pick != null && pick.status === 'Pending'; + const isApproved = pick != null && pick.status === 'Approved'; + const isRejected = pick != null && pick.status === 'Rejected'; + const isRequested = pick != null && pick.status !== 'Cancelled'; + + return ( +
+ + {/* Clean Image Container */} +
+ {selectedProduct.name} +
+ + {/* Title & Basics */} +
+

SKU: {selectedProduct.sku}

+

{selectedProduct.name}

+
+ + {String(selectedProduct.category || '').split(' / ')[0]} + + {stocked && ( + + In Store + + )} +
+
+ + {/* Clean Pricing Card */} +
+
+ Store Price +
+ ₹{selectedProduct.price?.toLocaleString('en-IN') || 0} + / {selectedProduct.unit || 'Pc'} +
+
+
+ + {/* Action Area */} +
+

Stock Management

+ + {isApproved ? ( +
+ + Approved and stocked in your inventory +
+ ) : isRejected ? ( +
+
+ + Stock request was rejected +
+ +
+ ) : isPending ? ( +
+
+
+
+ Requested ({pick.qty} units) +
+ +
+ +
+ ) : ( + + )} +
+ + {/* Retail Packaging Info */} +
+

Retail Packaging Info

+ +
+
+ ); + })()} +
); } diff --git a/src/components/TrialBatchDrawer.tsx b/src/components/TrialBatchDrawer.tsx index cf1f1b1..cd4c77a 100644 --- a/src/components/TrialBatchDrawer.tsx +++ b/src/components/TrialBatchDrawer.tsx @@ -17,9 +17,10 @@ interface TrialBatchDrawerProps { onClose: () => void; product: TrialProduct | null; tenantId?: number; + onSuccess?: (product: TrialProduct) => void; } -export default function TrialBatchDrawer({ isOpen, onClose, product, tenantId }: TrialBatchDrawerProps) { +export default function TrialBatchDrawer({ isOpen, onClose, product, tenantId, onSuccess }: TrialBatchDrawerProps) { const [qty, setQty] = useState(5); const [isSubmitting, setIsSubmitting] = useState(false); const [isSuccess, setIsSuccess] = useState(false); @@ -56,6 +57,9 @@ export default function TrialBatchDrawer({ isOpen, onClose, product, tenantId }: setTimeout(() => { setIsSubmitting(false); setIsSuccess(true); + if (onSuccess && product) { + onSuccess(product); + } // Auto close after showing success setTimeout(() => { onClose(); diff --git a/src/components/UserStorePage.tsx b/src/components/UserStorePage.tsx index 1e12813..1308990 100644 --- a/src/components/UserStorePage.tsx +++ b/src/components/UserStorePage.tsx @@ -18,6 +18,7 @@ import { ClipboardList, Layers, Users, + TrendingUp, X, } from 'lucide-react'; import { @@ -35,7 +36,6 @@ import DeliveryReportsView from './DeliveryReportsView'; import StoreQRView from './StoreQRView'; import UserStoreSidebar, { type UserNavItem } from './UserStoreSidebar'; import ComparisonModal from './ComparisonModal'; - interface UserStorePageProps { /** Returns to the login screen. */ onLogout: () => void; diff --git a/src/components/consoleUi.tsx b/src/components/consoleUi.tsx index ec05660..e2d1f04 100644 --- a/src/components/consoleUi.tsx +++ b/src/components/consoleUi.tsx @@ -16,6 +16,7 @@ */ import React, { useEffect } from 'react'; +import { createPortal } from 'react-dom'; import { X, Info } from 'lucide-react'; // ── Design tokens ──────────────────────────────────────────────────────────────── @@ -385,28 +386,32 @@ export function SlideDrawer({ if (!isOpen) return null; return ( -
- {/* Backdrop */} +
+ {/* Muted Backdrop */}
- {/* Drawer Panel */} -
- {/* Header */} -
-

{title}

+ {/* Pure, Clean Drawer Panel */} +
+ {/* Minimalist Header */} +
+

{title}

{/* Scrollable Content */} -
+
+ {/* Injecting CSS to hide webkit scrollbar */} +