@@ -742,7 +777,7 @@ export default function App() {
diff --git a/src/components/AdminConsole.tsx b/src/components/AdminConsole.tsx
index 23de294..996e559 100644
--- a/src/components/AdminConsole.tsx
+++ b/src/components/AdminConsole.tsx
@@ -378,7 +378,7 @@ VALUES (${newUserId}, 1, 'Active', NOW());
-
+
Add Store Outlet Location
@@ -635,7 +635,7 @@ VALUES (${newUserId}, 1, 'Active', NOW());
{createLocationMut.isPending ? (
<>
@@ -914,11 +914,11 @@ VALUES (${newUserId}, 1, 'Active', NOW());
onClick={() => { setActiveTab('tenant'); setTenantSuccess(null); }}
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all duration-200 whitespace-nowrap cursor-pointer border-none w-full ${
activeTab === 'tenant'
- ? 'bg-purple-50 text-[#581c87] shadow-sm border-l-2 border-purple-650'
+ ? 'bg-purple-50 text-[#662582] shadow-sm border-l-2 border-purple-650'
: 'text-slate-600 hover:text-slate-900 hover:bg-white bg-transparent'
}`}
>
-
+
Tenant Onboarding
@@ -926,11 +926,11 @@ VALUES (${newUserId}, 1, 'Active', NOW());
onClick={() => { setActiveTab('store'); setStoreSuccess(null); }}
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all duration-200 whitespace-nowrap cursor-pointer border-none w-full ${
activeTab === 'store'
- ? 'bg-purple-50 text-[#581c87] shadow-sm border-l-2 border-purple-650'
+ ? 'bg-purple-50 text-[#662582] shadow-sm border-l-2 border-purple-650'
: 'text-slate-600 hover:text-slate-900 hover:bg-white bg-transparent'
}`}
>
-
+
Store Branch Onboarding
@@ -938,11 +938,11 @@ VALUES (${newUserId}, 1, 'Active', NOW());
onClick={() => { setActiveTab('rider'); setRiderSuccess(null); }}
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all duration-200 whitespace-nowrap cursor-pointer border-none w-full ${
activeTab === 'rider'
- ? 'bg-purple-50 text-[#581c87] shadow-sm border-l-2 border-purple-650'
+ ? 'bg-purple-50 text-[#662582] shadow-sm border-l-2 border-purple-650'
: 'text-slate-600 hover:text-slate-900 hover:bg-white bg-transparent'
}`}
>
-
+
Rider Onboarding
diff --git a/src/components/DashboardView.tsx b/src/components/DashboardView.tsx
index c8da7dc..3be248e 100644
--- a/src/components/DashboardView.tsx
+++ b/src/components/DashboardView.tsx
@@ -15,9 +15,7 @@ import {
Clock,
ArrowUpRight,
} from 'lucide-react';
-import { useOrderSummary, useTenantInfo, useInvoiceInsight } from '../services/queries';
-import { DEFAULT_CONFIG_ID } from '../services/api';
-import { useFiestaLocationSummary, useFiestaTenantLocations } from '../services/fiestaQueries';
+import { useFiestaLocationSummary, useFiestaTenantLocations, useFiestaRevenueSummary, useFiestaOrderSummary } from '../services/fiestaQueries';
import { FIESTA_TENANT_ID } from '../services/fiestaApi';
interface DashboardViewProps {
@@ -43,19 +41,18 @@ export default function DashboardView({ searchQuery, tenantId = FIESTA_TENANT_ID
// All scoped to the signed-in merchant's tenant. Store locations come from the
// Fiesta source (the single source of truth used across the app) — it's already
// deduped and stripped of test rows, unlike the raw Hasura tenant-locations feed.
- const summaryQ = useOrderSummary(tenantId, fromdate, todate, DEFAULT_CONFIG_ID);
- const tenantQ = useTenantInfo(tenantId);
+ const summaryQ = useFiestaOrderSummary(tenantId, fromdate, todate);
const locationsQ = useFiestaTenantLocations(tenantId);
- const insightQ = useInvoiceInsight(tenantId);
+ const revenueQ = useFiestaRevenueSummary({ tenantid: tenantId, fromdate, todate });
const s = summaryQ.data;
- const tenantName = str(tenantQ.data?.tenantname) || s?.tenantname || `Tenant ${tenantId}`;
+ const tenantName = s?.tenantname || `Tenant ${tenantId}`;
// Revenue + profit come from the live invoice/financial insight. The endpoint
// returns two distinct figures (revenue and profit); we surface both rather than
// repeating one. When the tenant has no invoice records we show "—" instead of a
// misleading ₹0.
- const insight = insightQ.data as any;
+ const insight = revenueQ.data as any;
const money = (v: number | null) => (v == null ? '—' : `₹${Math.round(v).toLocaleString('en-IN')}`);
const monthlyRevenue = insight ? Number(insight.grossrevenue || insight.overallrevenue || insight.revenue || 0) : null;
const monthlyProfit = insight ? Number(insight.profit || insight.netrevenue || insight.margin || 0) : null;
@@ -112,7 +109,7 @@ export default function DashboardView({ searchQuery, tenantId = FIESTA_TENANT_ID
icon: Wallet,
bar: 'from-sky-500 to-cyan-500',
chip: 'bg-sky-50 text-sky-600 ring-sky-100',
- loading: insightQ.isLoading,
+ loading: revenueQ.isLoading,
},
{
title: 'MONTHLY PROFIT',
@@ -121,7 +118,7 @@ export default function DashboardView({ searchQuery, tenantId = FIESTA_TENANT_ID
icon: TrendingUp,
bar: 'from-emerald-500 to-teal-500',
chip: 'bg-emerald-50 text-emerald-600 ring-emerald-100',
- loading: insightQ.isLoading,
+ loading: revenueQ.isLoading,
},
];
diff --git a/src/components/DispatchHubView.tsx b/src/components/DispatchHubView.tsx
index a627d0c..8d7df20 100644
--- a/src/components/DispatchHubView.tsx
+++ b/src/components/DispatchHubView.tsx
@@ -22,28 +22,28 @@ export default function DispatchHubView({ locationid, tenantId }: DispatchHubVie
setActiveTab('map')}
className={`flex items-center !gap-1.5 !px-3 !py-1.5 text-[11px] font-extrabold !rounded-md transition-all duration-300 uppercase tracking-wide ${
- activeTab === 'map' ? 'bg-white text-[#581c87] shadow-sm ring-1 ring-black/5' : 'text-slate-500 hover:text-slate-800 hover:bg-slate-200/50'
+ activeTab === 'map' ? 'bg-white text-[#662582] shadow-sm ring-1 ring-black/5' : 'text-slate-500 hover:text-slate-800 hover:bg-slate-200/50'
}`}
>
- Map
+ Map
setActiveTab('orders')}
className={`flex items-center !gap-1.5 !px-3 !py-1.5 text-[11px] font-extrabold !rounded-md transition-all duration-300 uppercase tracking-wide ${
- activeTab === 'orders' ? 'bg-white text-[#581c87] shadow-sm ring-1 ring-black/5' : 'text-slate-500 hover:text-slate-800 hover:bg-slate-200/50'
+ activeTab === 'orders' ? 'bg-white text-[#662582] shadow-sm ring-1 ring-black/5' : 'text-slate-500 hover:text-slate-800 hover:bg-slate-200/50'
}`}
>
- Orders
+ Orders
setActiveTab('deliveries')}
className={`flex items-center !gap-1.5 !px-3 !py-1.5 text-[11px] font-extrabold !rounded-md transition-all duration-300 uppercase tracking-wide ${
- activeTab === 'deliveries' ? 'bg-white text-[#581c87] shadow-sm ring-1 ring-black/5' : 'text-slate-500 hover:text-slate-800 hover:bg-slate-200/50'
+ activeTab === 'deliveries' ? 'bg-white text-[#662582] shadow-sm ring-1 ring-black/5' : 'text-slate-500 hover:text-slate-800 hover:bg-slate-200/50'
}`}
>
- Deliveries
+ Deliveries
);
diff --git a/src/components/DispatchMap.tsx b/src/components/DispatchMap.tsx
index 135e3df..7e491f1 100644
--- a/src/components/DispatchMap.tsx
+++ b/src/components/DispatchMap.tsx
@@ -225,7 +225,7 @@ function MapController({ points, resizeKey }: { points: MapPoint[]; resizeKey: u
export default function DispatchMap({
points,
route,
- routeColor = '#581c87',
+ routeColor = '#662582',
start,
resizeKey,
animateNonce = 0,
diff --git a/src/components/DispatchView.css b/src/components/DispatchView.css
index edbd91a..e7c3063 100644
--- a/src/components/DispatchView.css
+++ b/src/components/DispatchView.css
@@ -10878,44 +10878,44 @@
/* ──────────────────────────────────────────────────────────────────────────────
Merchant theme override: the source console is blue-accented (#3b82f6); this
- app's other pages (Orders/Deliveries/Reports) use purple #581c87 with purple
+ app's other pages (Orders/Deliveries/Reports) use purple #662582 with purple
selected buttons. Retint the accent so the Dispatch buttons + selected state
match the rest of the console. Appended last so it wins the cascade.
────────────────────────────────────────────────────────────────────────────── */
.dispatch-container {
- --accent: #581c87;
+ --accent: #662582;
--accent-soft: rgba(88, 28, 135, 0.08);
- --border-active: #581c87;
+ --border-active: #662582;
}
/* Selected view-mode tab (.sbt.active uses var(--accent) for bg/border) */
.dispatch-container .sbt.active {
box-shadow: 0 4px 12px rgba(88, 28, 135, 0.25);
}
.dispatch-container .sbt:hover:not(.active) {
- border-color: #581c87;
- color: #581c87;
+ border-color: #662582;
+ color: #662582;
}
/* Selected batch / wave chip (was a hardcoded blue gradient) */
.dispatch-container .batch-btn.batch-slot.active,
.dispatch-container .batch-btn.active {
- background: linear-gradient(135deg, #581c87, #7c3aed);
+ background: linear-gradient(135deg, #662582, #7c3aed);
}
.dispatch-container .batch-btn:hover:not(.active) {
- border-color: #581c87;
- color: #581c87;
+ border-color: #662582;
+ color: #662582;
}
/* Brand badge + wordmark accent */
.dispatch-container .logo-badge {
- background: linear-gradient(135deg, #581c87, #6b21a8);
+ background: linear-gradient(135deg, #662582, #6b21a8);
}
.dispatch-container .logo-name em {
- color: #581c87;
+ color: #662582;
}
/* Operating-city pill → brand purple to match the rest of the console */
.dispatch-container .logo-city {
background: rgba(88, 28, 135, 0.08);
border-color: rgba(88, 28, 135, 0.25);
- color: #581c87;
+ color: #662582;
}
.dispatch-container .logo-city:hover {
background: rgba(88, 28, 135, 0.14);
@@ -10940,7 +10940,7 @@
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
- color: #581c87;
+ color: #662582;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
@@ -10985,7 +10985,7 @@
display: flex;
align-items: center;
justify-content: center;
- color: #581c87;
+ color: #662582;
opacity: 0.7;
}
diff --git a/src/components/DispatchView.tsx b/src/components/DispatchView.tsx
index c9c3464..3a66786 100644
--- a/src/components/DispatchView.tsx
+++ b/src/components/DispatchView.tsx
@@ -456,7 +456,7 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
void;
/** Signed-in user shown in the profile dropdown. */
profile: { name: string; role: string; email: string };
+ /** Optional store context to display next to the sidebar toggle. */
+ storeContext?: {
+ storeName: string;
+ branchName?: string;
+ icon?: React.ElementType;
+ };
}
export default function Header({
@@ -32,7 +38,8 @@ export default function Header({
onLogoutClick,
onAccountClick,
onQrClick,
- profile
+ profile,
+ storeContext
}: HeaderProps) {
const [showProfileDropdown, setShowProfileDropdown] = useState(false);
const profileRef = useRef(null);
@@ -63,7 +70,7 @@ export default function Header({
.toUpperCase() || 'NA';
return (
-
+
{/* Brand & Desktop Navigation Tabs */}
{/* Brand Logo — full wordmark when sidebar open, icon only when collapsed */}
@@ -71,7 +78,7 @@ export default function Header({
@@ -83,6 +90,21 @@ export default function Header({
>
+
+ {/* Dynamic Store Name Context */}
+ {storeContext && (
+
+
+ {storeContext.icon && }
+ {storeContext.storeName}
+
+ {storeContext.branchName && (
+
+ {storeContext.branchName}
+
+ )}
+
+ )}
{/* Global Actions Bar */}
@@ -110,7 +132,7 @@ export default function Header({
{initials}
-
+
{/* Identity (hidden on small screens) */}
@@ -128,7 +150,7 @@ export default function Header({
{showProfileDropdown && (
{/* Gradient profile header */}
-
+
@@ -157,7 +179,7 @@ export default function Header({
onClick={() => { setShowProfileDropdown(false); onAccountClick(); }}
className="w-full flex items-center gap-2.5 px-2.5 py-2 rounded-xl text-xs font-semibold text-slate-700 hover:bg-slate-50 cursor-pointer transition-colors group/item"
>
-
+
My Account
diff --git a/src/components/InventoryView.tsx b/src/components/InventoryView.tsx
index 38ccbe9..e7b95b9 100644
--- a/src/components/InventoryView.tsx
+++ b/src/components/InventoryView.tsx
@@ -92,7 +92,56 @@ export default function InventoryView({
const id = rowId(r);
if (id && !byId.has(id)) byId.set(id, r);
});
- setProducts(Array.from(byId.values()).map(stockRowToProduct));
+
+ const initialProducts = Array.from(byId.values()).map(stockRowToProduct);
+
+ // Add 3 mock products with isNew: true
+ const mockProducts: ProductMatrixItem[] = [
+ {
+ id: 'mock-1',
+ name: 'Organic Honey 500g',
+ sku: 'GRO-HON-500G',
+ unitsSold: 0,
+ revenue: 0,
+ stockStatus: 'Healthy',
+ trend: 'flat',
+ image: 'https://images.unsplash.com/photo-1587049352847-4d4b1240c5f2?auto=format&fit=crop&q=80&w=200',
+ category: 'Groceries / Pantry',
+ exposure: 'All Outlets',
+ verified: true,
+ isNew: true
+ },
+ {
+ 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]);
setSeeded(true);
}, [allStoreRows, seeded]);
@@ -217,7 +266,8 @@ export default function InventoryView({
image: newProduct.image,
category: newProduct.category,
exposure: 'All Outlets',
- verified: true
+ verified: true,
+ isNew: true
};
setProducts([createdProd, ...products]);
@@ -270,7 +320,8 @@ export default function InventoryView({
image: 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&q=80&w=200',
category,
exposure: 'All Outlets',
- verified: true
+ verified: true,
+ isNew: true
});
parsedCount++;
}
@@ -292,110 +343,52 @@ export default function InventoryView({
- {/* ── Immersive Analytics Banner (With Catalog Cover Image & Slate Gradient Overlay) ── */}
-
- {/* Cover Image Background & Decor */}
-
-
-
-
- {/* Background decorative glowing circles */}
-
-
-
-
- {/* Content Row */}
-
-
-
-
- Product Catalogue
-
- Global Sync
-
-
-
- Master catalogue registry with regional assortment presets, brand styling studio, and live stock synchronization feeds.
-
-
-
- {/* Navigation Tab pills styled like a modern control unit */}
-
-
setActiveTab('catalog')}
- className={`px-4 py-2 rounded-lg text-xs font-bold transition-all duration-300 flex items-center gap-2 cursor-pointer ${
- activeTab === 'catalog'
- ? 'bg-purple-900/50 text-purple-200 border border-purple-500/40 shadow-[0_0_15px_rgba(168,85,247,0.2)]'
- : 'text-zinc-400 hover:text-white border border-transparent'
- }`}
- >
-
- Catalogue & Stocks
-
-
-
setActiveTab('import_branding')}
- className={`px-4 py-2 rounded-lg text-xs font-bold transition-all duration-300 flex items-center gap-2 cursor-pointer ${
- activeTab === 'import_branding'
- ? 'bg-purple-900/50 text-purple-200 border border-purple-500/40 shadow-[0_0_15px_rgba(168,85,247,0.2)]'
- : 'text-zinc-400 hover:text-white border border-transparent'
- }`}
- >
-
- Import & Brand Studio
-
-
-
-
- {/* Small card metrics grid - 4 dynamic columns inside the banner */}
-
-
+ {/* Header and Metrics */}
+
+ {/* Small card metrics grid */}
+
{/* Card 1: Total SKUs */}
-
+
-
Total SKUs
-
-
-
+
+
{products.length}
-
Master catalogue
+
Master catalogue
{/* Card 2: Synced Outlets */}
-
+
-
Active Outlets
-
-
-
+
+
{locations.length}
-
Synced locations
+
Synced locations
{/* Card 3: Total On-Hand Volume */}
-
+
-
Total Stock
-
-
-
+
+
{storesStockWithOverrides.reduce((total, store) => {
return total + (store.rows || []).reduce((subTotal, r) => {
const inv = stockRowToInventory(r, store.locationname);
@@ -403,26 +396,25 @@ export default function InventoryView({
}, 0);
}, 0).toLocaleString('en-IN')}
-
Units on hand
+
Units on hand
{/* Card 4: Catalog Health */}
-
+
-
Catalogue Sync Ratio
-
-
+
Catalogue Sync Ratio
+
+
-
-
+
+
{products.length > 0 ? `${Math.round((products.filter(p => p.verified).length / products.length) * 100)}%` : '100%'}
-
Active Portfolio
+
Active Portfolio
-
@@ -476,14 +468,14 @@ export default function InventoryView({
setActiveTab('import_branding')}
- className="bg-white text-[#581c87] border border-purple-200 px-4 py-2 rounded-lg text-xs font-bold uppercase tracking-wider flex items-center justify-center gap-xs cursor-pointer hover:bg-[#faf5ff] transition shadow-sm"
+ className="bg-white text-[#662582] border border-purple-200 px-4 py-2 rounded-lg text-xs font-bold uppercase tracking-wider flex items-center justify-center gap-xs cursor-pointer hover:bg-[#faf5ff] transition shadow-sm"
>
Import SKUs
setShowAddProductModal(true)}
- className="bg-[#581c87] text-white px-xl py-2 rounded-lg text-xs font-bold uppercase tracking-wider flex items-center justify-center gap-xs cursor-pointer hover:bg-purple-800 transition shadow-sm"
+ className="bg-[#662582] text-white px-xl py-2 rounded-lg text-xs font-bold uppercase tracking-wider flex items-center justify-center gap-xs cursor-pointer hover:bg-purple-800 transition shadow-sm"
>
Add SKU
@@ -496,7 +488,7 @@ export default function InventoryView({
- Global Catalogue Assortment
+ Global Catalogue Assortment
Pick products & set quantities — selected items appear in every store's catalogue.
@@ -504,7 +496,7 @@ export default function InventoryView({
{storeCat.items.length} in store catalogue
-
+
{filteredProducts.length} item{filteredProducts.length === 1 ? '' : 's'} loaded
@@ -515,91 +507,202 @@ export default function InventoryView({
) : filteredProducts.length === 0 ? (
No catalogue products match your selection.
) : (
-
- {filteredProducts.map((prod) => (
-
-
- {/* Image zoom effect on hover */}
-
-
-
-
-
-
-
{prod.name}
-
{prod.sku}
+
+ {/* Left Side: Normal Catalogue */}
+
+
+ {filteredProducts.filter(p => !p.isNew).map((prod) => (
+
+
+ {/* Image zoom effect on hover */}
+
+
-
- {/* Categorized pill badge */}
-
+
+
+
{prod.name}
+
+
+ {prod.sku}
+
+
+ {/* Categorized pill badge */}
+
+ {prod.category.split(' / ')[0]}
+
+
+
+
+
+ Units Sold
+ {prod.unitsSold.toLocaleString()}
+
+
+ Revenue
+ ₹{prod.revenue.toLocaleString()}
+
+
+
+
+
+ {/* Exposure toggle row */}
+
+
- {prod.category.split(' / ')[0]}
+
+ {prod.verified ? 'Active Portfolio' : 'Under Inspection'}
+
+
+ handleToggleProductExposure(prod.id)}
+ className="sr-only peer"
+ />
+
+
-
-
-
Units Sold
-
{prod.unitsSold.toLocaleString()}
+ {/* Store-catalogue curation: pick the product to show to store users */}
+ {storeCat.has(prod.id) ? (
+
+ In Store Catalogue
+ storeCat.remove(prod.id)} title="Remove from store catalogue" className="flex items-center gap-1 px-2 py-1 rounded-lg text-rose-500 hover:bg-rose-50 font-bold text-[10px] cursor-pointer transition-colors">
+ Remove
+
-
- Revenue
- ₹{prod.revenue.toLocaleString()}
-
-
+ ) : (
+
storeCat.add({ productid: prod.id, name: prod.name, image: prod.image, category: prod.category, sku: prod.sku, price: prod.unitsSold > 0 ? Math.round(prod.revenue / prod.unitsSold) : 0, unit: prod.exposure, qty: 1 })}
+ disabled={!prod.verified}
+ className={`w-full flex items-center justify-center gap-1.5 pt-2.5 mt-1 border-t border-[#f1f5f9] text-[11px] font-bold transition-colors ${
+ prod.verified
+ ? 'text-[#662582] hover:text-purple-800 cursor-pointer'
+ : 'text-zinc-400 cursor-not-allowed opacity-70'
+ }`}
+ title={!prod.verified ? "Product must be Active to be added" : "Add to Store Catalogue"}
+ >
+ Add to Store Catalogue
+
+ )}
-
-
- {/* Exposure toggle row */}
-
-
-
- {prod.verified ? 'Active Portfolio' : 'Under Inspection'}
-
-
-
- handleToggleProductExposure(prod.id)}
- className="sr-only peer"
- />
-
-
-
-
- {/* Store-catalogue curation: pick the product + quantity to show to store users */}
- {storeCat.has(prod.id) ? (
-
-
In Store Catalogue
-
- storeCat.setQty(prod.id, storeCat.getQty(prod.id) - 1)} className="w-6 h-6 rounded-lg border border-[#e2e8f0] text-zinc-500 hover:bg-zinc-50 font-bold cursor-pointer leading-none">−
- {storeCat.getQty(prod.id)}
- storeCat.setQty(prod.id, storeCat.getQty(prod.id) + 1)} className="w-6 h-6 rounded-lg border border-[#e2e8f0] text-zinc-500 hover:bg-zinc-50 font-bold cursor-pointer leading-none">+
- storeCat.remove(prod.id)} title="Remove from store catalogue" className="ml-1 w-6 h-6 rounded-lg text-rose-500 hover:bg-rose-50 flex items-center justify-center cursor-pointer">
-
-
- ) : (
-
storeCat.add({ productid: prod.id, name: prod.name, image: prod.image, category: prod.category, sku: prod.sku, price: prod.unitsSold > 0 ? Math.round(prod.revenue / prod.unitsSold) : 0, unit: prod.exposure, qty: 1 })}
- className="w-full flex items-center justify-center gap-1.5 pt-2.5 mt-1 border-t border-[#f1f5f9] text-[11px] font-bold text-[#581c87] hover:text-purple-800 cursor-pointer"
- >
- Add to Store Catalogue
-
- )}
+ ))}
- ))}
+
+
+ {/* Right Side: Newly Added Items */}
+ {filteredProducts.filter(p => p.isNew).length > 0 && (
+
+
+
+
Newly Added
+
+
+ {filteredProducts.filter(p => p.isNew).map((prod) => (
+
+
+ {/* Image zoom effect on hover */}
+
+
+
+
+
+
+
{prod.name}
+
+
+ {prod.sku}
+
+
+ {/* Categorized pill badge */}
+
+ {prod.category.split(' / ')[0]}
+
+
+
+
+
+ Units Sold
+ {prod.unitsSold.toLocaleString()}
+
+
+ Revenue
+ ₹{prod.revenue.toLocaleString()}
+
+
+
+
+
+ {/* Exposure toggle row */}
+
+
+
+ {prod.verified ? 'Active Portfolio' : 'Under Inspection'}
+
+
+
+ handleToggleProductExposure(prod.id)}
+ className="sr-only peer"
+ />
+
+
+
+
+ {/* Store-catalogue curation: pick the product to show to store users */}
+ {storeCat.has(prod.id) ? (
+
+ In Store Catalogue
+ storeCat.remove(prod.id)} title="Remove from store catalogue" className="flex items-center gap-1 px-2 py-1 rounded-lg text-rose-500 hover:bg-rose-50 font-bold text-[10px] cursor-pointer transition-colors">
+ Remove
+
+
+ ) : (
+
storeCat.add({ productid: prod.id, name: prod.name, image: prod.image, category: prod.category, sku: prod.sku, price: prod.unitsSold > 0 ? Math.round(prod.revenue / prod.unitsSold) : 0, unit: prod.exposure, qty: 1 })}
+ disabled={!prod.verified}
+ className={`w-full flex items-center justify-center gap-1.5 pt-2.5 mt-1 border-t border-[#f1f5f9] text-[11px] font-bold transition-colors ${
+ prod.verified
+ ? 'text-[#662582] hover:text-purple-800 cursor-pointer'
+ : 'text-zinc-400 cursor-not-allowed opacity-70'
+ }`}
+ title={!prod.verified ? "Product must be Active to be added" : "Add to Store Catalogue"}
+ >
+ Add to Store Catalogue
+
+ )}
+
+ ))}
+
+
+ )}
)}
@@ -916,7 +1019,7 @@ export default function InventoryView({
{/* Custom CSV Parsing Box */}
-
+
Manual CSV Direct-Entry Console
@@ -930,7 +1033,7 @@ export default function InventoryView({
@@ -938,7 +1041,7 @@ export default function InventoryView({
Header row ignored on parse
Parse CSV Data & Sync
@@ -966,7 +1069,7 @@ export default function InventoryView({
-
+
Packaging Branding Studio
@@ -987,7 +1090,7 @@ export default function InventoryView({
-
+
Introduce New Grocery Catalogue SKU
{/* ── Left brand / hero panel (desktop only) ── */}
-
+
{/* Layered ambient glows */}
@@ -269,7 +269,7 @@ export default function LoginView({ onLogin }: LoginViewProps) {
{loading || checkingEmail ? (
<>
diff --git a/src/components/OperationsView.tsx b/src/components/OperationsView.tsx
index 7c433e3..38866bf 100644
--- a/src/components/OperationsView.tsx
+++ b/src/components/OperationsView.tsx
@@ -194,13 +194,13 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
onClick={() => setActiveSubTab(tab)}
className={`font-sans text-xs font-semibold uppercase tracking-wider pb-2 cursor-pointer transition-colors relative ${
activeSubTab === tab
- ? 'text-[#581c87] font-bold'
+ ? 'text-[#662582] font-bold'
: 'text-zinc-400 hover:text-zinc-600'
}`}
>
{tab}
{activeSubTab === tab && (
-
+
)}
))}
@@ -225,7 +225,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
{productList.length} SKUs · {locationName}
-
@@ -312,7 +312,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
@@ -341,7 +341,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
});
setShowTransferModal(true);
}}
- className="text-xs font-semibold text-[#581c87] hover:underline cursor-pointer"
+ className="text-xs font-semibold text-[#662582] hover:underline cursor-pointer"
>
Transfer
@@ -393,9 +393,9 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
setShowAddSkuModal(true)}
- className="bg-[#f8fafc] border border-[#e2e8f0] p-sm rounded-lg flex flex-col items-center gap-xs text-zinc-700 hover:bg-[#faf5ff] hover:text-[#581c87] hover:border-[#581c87] transition-all cursor-pointer group"
+ className="bg-[#f8fafc] border border-[#e2e8f0] p-sm rounded-lg flex flex-col items-center gap-xs text-zinc-700 hover:bg-[#faf5ff] hover:text-[#662582] hover:border-[#662582] transition-all cursor-pointer group"
>
-
+
Add SKU
@@ -404,9 +404,9 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
setTransferData({ sku: 'PRO-9920-X1', origin: 'RS Puram Hub (CBE-01)', destination: '', quantity: 100 });
setShowTransferModal(true);
}}
- className="bg-[#f8fafc] border border-[#e2e8f0] p-sm rounded-lg flex flex-col items-center gap-xs text-zinc-700 hover:bg-[#faf5ff] hover:text-[#581c87] hover:border-[#581c87] transition-all cursor-pointer group"
+ className="bg-[#f8fafc] border border-[#e2e8f0] p-sm rounded-lg flex flex-col items-center gap-xs text-zinc-700 hover:bg-[#faf5ff] hover:text-[#662582] hover:border-[#662582] transition-all cursor-pointer group"
>
-
+
Transfer
@@ -417,9 +417,9 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
alert(`Returns logged successfully for target SKU code ${amount}. Waiting for physical hub clearance inspection.`);
}
}}
- className="bg-[#f8fafc] border border-[#e2e8f0] p-sm rounded-lg flex flex-col items-center gap-xs text-zinc-700 hover:bg-[#faf5ff] hover:text-[#581c87] hover:border-[#581c87] transition-all cursor-pointer group"
+ className="bg-[#f8fafc] border border-[#e2e8f0] p-sm rounded-lg flex flex-col items-center gap-xs text-zinc-700 hover:bg-[#faf5ff] hover:text-[#662582] hover:border-[#662582] transition-all cursor-pointer group"
>
-
+
Returns
@@ -427,9 +427,9 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
onClick={() => {
alert('Generating automated physical audit compliance report draft sheets... Download started background.');
}}
- className="bg-[#f8fafc] border border-[#e2e8f0] p-sm rounded-lg flex flex-col items-center gap-xs text-zinc-700 hover:bg-[#faf5ff] hover:text-[#581c87] hover:border-[#581c87] transition-all cursor-pointer group"
+ className="bg-[#f8fafc] border border-[#e2e8f0] p-sm rounded-lg flex flex-col items-center gap-xs text-zinc-700 hover:bg-[#faf5ff] hover:text-[#662582] hover:border-[#662582] transition-all cursor-pointer group"
>
-
+
Audit CSV
@@ -476,7 +476,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
alert(`${title} added successfully to unreleased draft portfolio.`);
}
}}
- className="bg-[#581c87] hover:bg-purple-800 active:bg-purple-900 text-white font-sans text-xs font-semibold px-4 py-2 rounded-lg cursor-pointer transition-colors shadow-sm"
+ className="bg-[#662582] hover:bg-purple-800 active:bg-purple-900 text-white font-sans text-xs font-semibold px-4 py-2 rounded-lg cursor-pointer transition-colors shadow-sm"
>
Add Brand Product
@@ -531,7 +531,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
onChange={() => handleToggleProductExposure(prod.id)}
className="sr-only peer"
/>
-
+
@@ -552,9 +552,9 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
alert('Uploaded successfully. Metadata schema verification committed.');
}
}}
- className="bg-white border-2 border-dashed border-zinc-300 rounded-xl p-xl flex flex-col items-center justify-center text-center cursor-pointer hover:bg-[#faf5ff]/30 hover:border-[#581c87] transition-all duration-200"
+ className="bg-white border-2 border-dashed border-zinc-300 rounded-xl p-xl flex flex-col items-center justify-center text-center cursor-pointer hover:bg-[#faf5ff]/30 hover:border-[#662582] transition-all duration-200"
>
-
+
@@ -620,7 +620,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
placeholder="e.g., SKU-1290-A"
value={newSku.sku}
onChange={(e) => setNewSku({ ...newSku, sku: e.target.value })}
- className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87]"
+ className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
required
/>
@@ -632,7 +632,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
placeholder="e.g., Thermal Printer"
value={newSku.name}
onChange={(e) => setNewSku({ ...newSku, name: e.target.value })}
- className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87]"
+ className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
required
/>
@@ -645,7 +645,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
placeholder="e.g., Coimbatore main logistics CBE"
value={newSku.warehouse}
onChange={(e) => setNewSku({ ...newSku, warehouse: e.target.value })}
- className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87]"
+ className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
required
/>
@@ -657,7 +657,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
type="number"
value={newSku.stockLevel}
onChange={(e) => setNewSku({ ...newSku, stockLevel: Number(e.target.value) })}
- className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87]"
+ className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
required
/>
@@ -667,7 +667,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
setNewSku({ ...newSku, region: e.target.value as any })}
- className="w-full border border-[#e2e8f0] rounded-lg p-2 bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87]"
+ className="w-full border border-[#e2e8f0] rounded-lg p-2 bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
>
Coimbatore North (CBE-NORTH)
Coimbatore South (CBE-SOUTH)
@@ -688,7 +688,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
Commit Ledger SKU
@@ -721,7 +721,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
type="text"
value={transferData.sku}
onChange={(e) => setTransferData({ ...transferData, sku: e.target.value })}
- className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] font-mono focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87]"
+ className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] font-mono focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
required
/>
@@ -732,7 +732,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
type="text"
value={transferData.origin}
onChange={(e) => setTransferData({ ...transferData, origin: e.target.value })}
- className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] focus:bg-white outline-none text-zinc-800 font-medium focus:ring-1 focus:ring-[#581c87]"
+ className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] focus:bg-white outline-none text-zinc-800 font-medium focus:ring-1 focus:ring-[#662582]"
required
/>
@@ -744,7 +744,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
placeholder="e.g., Coimbatore South CBE-03"
value={transferData.destination}
onChange={(e) => setTransferData({ ...transferData, destination: e.target.value })}
- className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87]"
+ className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
required
/>
@@ -755,7 +755,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
type="number"
value={transferData.quantity}
onChange={(e) => setTransferData({ ...transferData, quantity: Number(e.target.value) })}
- className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87]"
+ className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
required
/>
@@ -770,7 +770,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
Approve Routing
diff --git a/src/components/ReportsView.tsx b/src/components/ReportsView.tsx
index b67a0f9..3eaaf24 100644
--- a/src/components/ReportsView.tsx
+++ b/src/components/ReportsView.tsx
@@ -21,14 +21,16 @@ import {
ShoppingBag,
AlertTriangle,
MapPin,
- Calendar
+ Calendar,
+ Trophy,
+ Store
} from 'lucide-react';
import { LeaderboardNode } from '../types';
import {
- useFiestaOrderSummary,
useFiestaLocationSummary,
useFiestaOrderInsight,
- useFiestaStockStatement,
+ useFiestaStoresStock,
+ useFiestaOrderSummary,
useFiestaRevenueSummary,
useFiestaTimeSeries,
} from '../services/fiestaQueries';
@@ -91,35 +93,52 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
const prevEnd = new Date(yearStart.getTime() - 86400000);
const prevStart = new Date(prevEnd.getTime() - periodDays * 86400000);
+ // Helper matching local hubs belonging to Coimbatore
+ const isCoimbatoreNode = (name: string) => {
+ const coimbatoreZones = ['gandhipuram', 'rs puram', 'peelamedu', 'saravanampatti', 'coimbatore'];
+ return coimbatoreZones.some(zone => name.toLowerCase().includes(zone));
+ };
+
+ const locSummaryQ = useFiestaLocationSummary(tenantId);
+
+ const filteredLocations = useMemo(() => {
+ const rawLocations = [...(locSummaryQ.data ?? [])];
+ if (selectedRegion === 'coimbatore') {
+ return rawLocations.filter(r => isCoimbatoreNode(r.locationname || ''));
+ }
+ if (selectedRegion === 'chennai' || selectedRegion === 'bangalore') {
+ return [];
+ }
+ return rawLocations;
+ }, [locSummaryQ.data, selectedRegion]);
+
+ const mappedLocations = useMemo(() => {
+ return filteredLocations.map(l => ({ locationid: Number(l.locationid), locationname: l.locationname || '' }));
+ }, [filteredLocations]);
+
const summaryQ = useFiestaOrderSummary(tenantId, ymd(yearStart), todate);
const prevSummaryQ = useFiestaOrderSummary(tenantId, ymd(prevStart), ymd(prevEnd));
- const locSummaryQ = useFiestaLocationSummary(tenantId);
const insightQ = useFiestaOrderInsight(tenantId);
const revSummaryQ = useFiestaRevenueSummary({ tenantid: tenantId, fromdate: ymd(yearStart), todate });
const prevRevSummaryQ = useFiestaRevenueSummary({ tenantid: tenantId, fromdate: ymd(prevStart), todate: ymd(prevEnd) });
- const stockQ = useFiestaStockStatement({
- tenantid: tenantId,
- locationid: FIESTA_PRIMARY_LOCATION_ID,
- keyword: '',
- pageno: 1,
- pagesize: 100,
- });
+
+ const storesStock = useFiestaStoresStock(tenantId, mappedLocations);
const s = summaryQ.data;
const prevS = prevSummaryQ.data;
- const activeSkus = (stockQ.data ?? []).length;
+ const revS = revSummaryQ.data;
+ const prevRevS = prevRevSummaryQ.data;
// Real period-over-period % change (null when we can't compute it yet).
const pctChange = (current: number, previous: number): number | null => {
if (previous <= 0) return null;
return ((current - previous) / previous) * 100;
};
- const ordersDelta = s && prevS ? pctChange(s.total, prevS.total) : null;
- const cancelledDelta = s && prevS ? pctChange(s.cancelled, prevS.cancelled) : null;
-
- const revS = revSummaryQ.data;
- const prevRevS = prevRevSummaryQ.data;
- const revenueDelta = revS && prevRevS ? pctChange(revS.grossrevenue, prevRevS.grossrevenue) : null;
+
+ const isAllRegions = selectedRegion === 'all';
+ const ordersDelta = (s && prevS && isAllRegions) ? pctChange(s.total, prevS.total) : null;
+ const cancelledDelta = (s && prevS && isAllRegions) ? pctChange(s.cancelled, prevS.cancelled) : null;
+ const revenueDelta = (revS && prevRevS && isAllRegions) ? pctChange(revS.grossrevenue, prevRevS.grossrevenue) : null;
const fmtDelta = (d: number) => `${d >= 0 ? '+' : ''}${d.toFixed(1)}%`;
@@ -174,11 +193,42 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
};
const theme = getChartColors();
- // Live KPI values (tenant-wide; region scaling removed — no per-region API).
- const totalOrdersVal = s?.total ?? 0;
- const deliveredVal = s?.delivered ?? 0;
- const cancelledVal = s?.cancelled ?? 0;
- const activeSkusVal = activeSkus;
+ let totalOrdersVal = 0;
+ let deliveredVal = 0;
+ let cancelledVal = 0;
+ let grossRevenueVal = 0;
+ let avgOrderVal = 0;
+ let activeSkusVal = 0;
+
+ if (isAllRegions) {
+ totalOrdersVal = s?.total ?? 0;
+ deliveredVal = s?.delivered ?? 0;
+ cancelledVal = s?.cancelled ?? 0;
+ grossRevenueVal = revS?.grossrevenue ?? 0;
+ avgOrderVal = revS?.avgordervalue ?? 0;
+ } else {
+ const locIds = new Set(filteredLocations.map(l => Number(l.locationid)));
+
+ (locSummaryQ.data ?? []).forEach(loc => {
+ if (locIds.has(Number(loc.locationid))) {
+ totalOrdersVal += Number(loc.total || 0);
+ deliveredVal += Number(loc.delivered || 0);
+ cancelledVal += Number(loc.cancelled || 0);
+ }
+ });
+
+ // We do not have location-level revenue from the API yet.
+ grossRevenueVal = 0;
+ avgOrderVal = 0;
+ }
+
+ const skuSet = new Set
();
+ storesStock.forEach(store => {
+ if (store.rows) {
+ store.rows.forEach(row => skuSet.add(`SKU-${row.productid}`));
+ }
+ });
+ activeSkusVal = skuSet.size;
// KPI Row Configuration. `awaiting` cards have no live value (rendered via
// AwaitingApi). `trend` is only set where a REAL delta could be derived.
@@ -197,13 +247,13 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
{
id: 'revenue' as const,
title: 'Revenue',
- value: `₹${(revS?.grossrevenue ?? 0).toLocaleString('en-IN')}`,
+ value: `₹${grossRevenueVal.toLocaleString('en-IN')}`,
trend: revenueDelta !== null ? fmtDelta(revenueDelta) : null,
- status: `₹${(revS?.avgordervalue ?? 0).toLocaleString('en-IN')} avg. order`,
+ status: `₹${avgOrderVal.toLocaleString('en-IN')} avg. order`,
isPositive: revenueDelta === null ? true : revenueDelta >= 0,
spark: [20, 30, 25, 45, 40, 55, 50, 68],
color: 'emerald',
- awaiting: false,
+ awaiting: !isAllRegions,
},
{
id: 'cancelled' as const,
@@ -231,30 +281,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
},
];
- // Helper matching local hubs belonging to Coimbatore
- const isCoimbatoreNode = (name: string) => {
- const coimbatoreZones = ['gandhipuram', 'rs puram', 'peelamedu', 'saravanampatti', 'coimbatore'];
- return coimbatoreZones.some(zone => name.toLowerCase().includes(zone));
- };
- const getFilteredLocations = () => {
- const rawLocations = [...(locSummaryQ.data ?? [])];
-
- // Only Coimbatore can be filtered from live data; Chennai/Bangalore have no
- // live tenant locations (their stub data was removed). Selecting them yields
- // an empty list rather than fabricated hubs.
- if (selectedRegion === 'coimbatore') {
- return rawLocations.filter(r => isCoimbatoreNode(r.locationname || ''));
- }
-
- if (selectedRegion === 'chennai' || selectedRegion === 'bangalore') {
- return [];
- }
-
- return rawLocations;
- };
-
- const filteredLocations = getFilteredLocations();
// Leaderboard — outlets ranked by total live orders.
const leaderboard: LeaderboardNode[] = (() => {
@@ -294,7 +321,30 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
);
// Live product performance matrix.
- const liveProducts = (stockQ.data ?? []).map(stockRowToProduct);
+ const activeRegionStock = storesStock;
+
+ const liveProducts = useMemo(() => {
+ const agg = new Map();
+
+ activeRegionStock.forEach(store => {
+ if (store.rows) {
+ store.rows.forEach(row => {
+ const sku = `SKU-${row.productid}`;
+ if (!agg.has(sku)) {
+ agg.set(sku, { ...row, closing: 0, debit: 0, credit: 0, opening: 0 });
+ }
+ const existing = agg.get(sku);
+ existing.closing += Number(row.closing || 0);
+ existing.debit += Number(row.debit || 0);
+ existing.credit += Number(row.credit || 0);
+ existing.opening += Number(row.opening || 0);
+ });
+ }
+ });
+
+ return Array.from(agg.values()).map(stockRowToProduct);
+ }, [activeRegionStock]);
+
const filteredProducts = liveProducts.filter((prod) => {
const matchesSearch =
prod.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
@@ -303,6 +353,32 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
return matchesSearch && matchesStock;
});
+ const ITEMS_PER_PAGE = 10;
+ const totalPages = Math.max(1, Math.ceil(filteredProducts.length / ITEMS_PER_PAGE));
+
+ useEffect(() => {
+ if (currentPage > totalPages) {
+ setCurrentPage(1);
+ }
+ }, [totalPages, currentPage]);
+
+ const paginatedProducts = filteredProducts.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE);
+
+ // Derived Top Products
+ const topOverallProducts = useMemo(() => {
+ return [...liveProducts].sort((a, b) => b.unitsSold - a.unitsSold).slice(0, 3);
+ }, [liveProducts]);
+
+ const topProductsByStore = useMemo(() => {
+ return activeRegionStock.map(store => {
+ const prods = (store.rows || []).map(stockRowToProduct);
+ return {
+ locationname: store.locationname || `Location ${store.locationid}`,
+ topProducts: prods.sort((a, b) => b.unitsSold - a.unitsSold).slice(0, 3)
+ };
+ }).filter(s => s.topProducts.length > 0);
+ }, [activeRegionStock]);
+
// Heatmap cell color gradient scale (multi-stop violet theme)
const getHeatmapColorClass = (val: number) => {
const ratio = val / heatmapMax;
@@ -318,7 +394,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
if (ratio <= 0.9) {
return 'bg-[#7c3aed]/20 text-[#7c3aed] border border-[#7c3aed]/20 hover:bg-[#7c3aed]/30 hover:scale-105';
}
- return 'bg-gradient-to-br from-[#7c3aed] to-[#581c87] text-white border-none shadow-sm hover:scale-108 hover:shadow-md hover:brightness-110';
+ return 'bg-gradient-to-br from-[#7c3aed] to-[#662582] text-white border-none shadow-sm hover:scale-108 hover:shadow-md hover:brightness-110';
};
// Triggers progress bar simulated exporting
@@ -424,170 +500,9 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
- {/* Action picker filters inside the banner */}
-
- {/* Custom Timeframe Dropdown */}
-
-
{
- setShowTimeframeDropdown(!showTimeframeDropdown);
- setShowRegionDropdown(false);
- }}
- className="bg-slate-900/40 backdrop-blur-md border border-white/10 rounded-xl px-sm py-2 flex items-center justify-between gap-md shadow-sm font-bold text-white min-w-[135px] cursor-pointer hover:bg-slate-900/60 transition-colors"
- >
- {selectedTimeframe}
-
-
-
- {showTimeframeDropdown && (
- <>
-
setShowTimeframeDropdown(false)} />
-
- {['This Month', 'This Year (YTD)', 'Last 12 Months', 'All Time'].map((opt) => (
- {
- setSelectedTimeframe(opt);
- setShowTimeframeDropdown(false);
- }}
- className={`w-full text-left px-md py-2 text-xs font-semibold hover:bg-zinc-900 transition-colors ${selectedTimeframe === opt ? 'text-purple-400 bg-purple-950/40' : 'text-zinc-300'}`}
- >
- {opt}
-
- ))}
-
- >
- )}
-
-
- {/* Custom Region Dropdown */}
-
-
{
- setShowRegionDropdown(!showRegionDropdown);
- setShowTimeframeDropdown(false);
- }}
- className="bg-slate-900/40 backdrop-blur-md border border-white/10 rounded-xl px-sm py-2 flex items-center justify-between gap-md shadow-sm font-bold text-white min-w-[155px] cursor-pointer hover:bg-slate-900/60 transition-colors"
- >
-
-
-
- {selectedRegion === 'all' ? 'All Regions (12)' :
- selectedRegion === 'coimbatore' ? 'Coimbatore (5)' :
- selectedRegion === 'chennai' ? 'Chennai (4)' : 'Bangalore (3)'}
-
-
-
-
-
- {showRegionDropdown && (
- <>
-
setShowRegionDropdown(false)} />
-
- {[
- { id: 'all' as const, label: 'All Regions (12 Hubs)' },
- { id: 'coimbatore' as const, label: 'Coimbatore (5 Hubs)' },
- { id: 'chennai' as const, label: 'Chennai (4 Hubs)' },
- { id: 'bangalore' as const, label: 'Bangalore (3 Hubs)' }
- ].map((opt) => (
- {
- handleRegionChange(opt.id);
- setShowRegionDropdown(false);
- }}
- className={`w-full text-left px-md py-2 text-xs font-semibold hover:bg-zinc-900 transition-colors ${selectedRegion === opt.id ? 'text-purple-400 bg-purple-950/40' : 'text-zinc-300'}`}
- >
- {opt.label}
-
- ))}
-
- >
- )}
-
-
- {/* Export PDF action */}
-
startExportSim('PDF')}
- className="bg-[#581c87] border border-purple-500/30 text-white font-sans font-bold px-4 py-2 rounded-xl flex items-center gap-sm cursor-pointer transition-all hover:bg-purple-800 hover:shadow-lg active:bg-purple-900 shadow-sm text-xs hover:-translate-y-0.5 animate-in"
- >
-
- Export PDF
-
-
- {/* Small cards metrics grid relative to reports (similar to StoreDetailView) */}
-
- {/* Card 1: Active Region */}
-
-
-
-
- {selectedRegion === 'all' ? 'All Regions' :
- selectedRegion === 'coimbatore' ? 'Coimbatore' :
- selectedRegion === 'chennai' ? 'Chennai' : 'Bangalore'}
-
-
- {filteredLocations.length} hubs active
-
-
-
-
- {/* Card 2: Selected Horizon */}
-
-
-
-
- {selectedTimeframe}
-
-
Historical Period
-
-
-
- {/* Card 3: Total Segment Orders */}
-
-
-
-
- {totalOrdersVal.toLocaleString('en-IN')}
-
-
Segment Volume
-
-
-
- {/* Card 4: Gross Revenue — no revenue API ([R1]) */}
-
-
-
-
- ₹{(revS?.grossrevenue ?? 0).toLocaleString('en-IN')}
-
-
₹{(revS?.netrevenue ?? 0).toLocaleString('en-IN')} net
-
-
-
{/* Primary KPI Row - 4 Key Tab buttons with Sparklines */}
@@ -823,7 +738,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
-
+
Busiest Month
@@ -932,12 +847,12 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
{node.name}
- {node.revenue}
+ {node.revenue}
@@ -950,14 +865,121 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
+ {/* Top Products Section */}
+
+ {/* Top 3 Overall Products - 4 Cols */}
+
+ {/* Subtle glow background */}
+
+
+
+
+
+
+
+ Overall Top 3
+
+
+
+
+ {topOverallProducts.length === 0 ? (
+
No sales data available.
+ ) : (
+ topOverallProducts.map((prod, index) => {
+ const isFirst = index === 0;
+ return (
+
+
+ {/* Rank Badge */}
+
+ #{index + 1}
+
+
+ {/* Image */}
+
+
+
+
+
+
{prod.name}
+
+
+ {prod.unitsSold.toLocaleString()} units
+
+
+ {prod.category}
+
+
+
+ {isFirst && (
+
+ )}
+
+ );
+ })
+ )}
+
+
+
+ {/* Store-wise Top Products - 8 Cols */}
+
+
+
+
+
+
+ Store-Wise Top Performers
+
+
+
+ {topProductsByStore.length === 0 ? (
+
No store data available.
+ ) : (
+
+ {topProductsByStore.map(store => (
+
+
+
+
{store.locationname}
+
+
+
+ {store.topProducts.map((prod, idx) => (
+
+
+ {idx + 1}
+
+
+
+
+
+
+
+
{prod.name}
+
+ {prod.unitsSold.toLocaleString()} units
+
+
+
+ ))}
+
+
+ ))}
+
+ )}
+
+
+
+
{/* Detailed Performance Matrix table */}
+
{/* Table header with filters control */}
- Product Performance Matrix
+ Product Performance Matrix
@@ -968,7 +990,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
key={filter}
onClick={() => setStockFilter(filter)}
className={`px-3 py-1.5 rounded-lg cursor-pointer transition-colors border ${stockFilter === filter
- ? 'bg-[#581c87] text-white border-[#581c87] shadow-sm'
+ ? 'bg-[#662582] text-white border-[#662582] shadow-sm'
: 'bg-white border-[#e2e8f0] text-zinc-655 hover:bg-zinc-55'
}`}
>
@@ -1000,14 +1022,14 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
- {filteredProducts.length === 0 ? (
+ {paginatedProducts.length === 0 ? (
No matching items matching stock filter criteria.
) : (
- filteredProducts.map((prod) => {
+ paginatedProducts.map((prod) => {
const isExpanded = expandedProductId === prod.id;
return (
@@ -1091,33 +1113,25 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
{/* Matrix table pagination */}
-
Showing 1-{filteredProducts.length} of {liveProducts.length} live products
+
Showing {Math.min((currentPage - 1) * ITEMS_PER_PAGE + 1, filteredProducts.length)}-{Math.min(currentPage * ITEMS_PER_PAGE, filteredProducts.length)} of {filteredProducts.length} filtered products (from {liveProducts.length} total)
setCurrentPage(prev => Math.max(1, prev - 1))}
- className="w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center hover:bg-white active:bg-[#f8fafc] cursor-pointer"
+ disabled={currentPage === 1}
+ className={`w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center cursor-pointer transition-colors ${currentPage === 1 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-white active:bg-[#f8fafc]'}`}
>
-
- 1
-
+
+
+ Page {currentPage} of {totalPages}
+
+
alert('Proceeding to page 2 details representation')}
- className="w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center hover:bg-white text-zinc-550 font-bold font-mono text-[10px] cursor-pointer"
- >
- 2
-
-
alert('Proceeding to page 3 details representation')}
- className="w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center hover:bg-white text-zinc-555 font-bold font-mono text-[10px] cursor-pointer"
- >
- 3
-
-
setCurrentPage(prev => prev + 1)}
- className="w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center hover:bg-white active:bg-[#f8fafc] cursor-pointer"
+ onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
+ disabled={currentPage === totalPages}
+ className={`w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center cursor-pointer transition-colors ${currentPage === totalPages ? 'opacity-50 cursor-not-allowed' : 'hover:bg-white active:bg-[#f8fafc]'}`}
>
diff --git a/src/components/SettingsView.tsx b/src/components/SettingsView.tsx
index 1fc613a..a806fd8 100644
--- a/src/components/SettingsView.tsx
+++ b/src/components/SettingsView.tsx
@@ -130,7 +130,8 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett
const [activeTab, setActiveTab] = useState
('profile');
// Live tenant profile + outlets.
- const tenantsQ = useFiestaAllTenants({ pagesize: 50 });
+ // Fetch a larger page size to ensure we find our specific tenant (ID 1087).
+ const tenantsQ = useFiestaAllTenants({ pagesize: 5000, status: '' });
const tenant = (tenantsQ.data ?? []).find((t) => Number(t.tenantid) === tenantId) || null;
const locationsQ = useFiestaTenantLocations(tenantId);
const outlets = locationsQ.data ?? [];
@@ -272,11 +273,11 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett
onClick={() => setActiveTab(t.key)}
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all duration-200 whitespace-nowrap cursor-pointer border-none ${
active
- ? 'bg-purple-50 text-[#581c87] shadow-sm border-l-2 border-purple-650'
+ ? 'bg-purple-50 text-[#662582] shadow-sm border-l-2 border-purple-650'
: 'text-slate-600 hover:text-slate-900 hover:bg-white bg-transparent'
}`}
>
-
+
{t.label}
);
@@ -411,7 +412,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett
setShowStoreOnboarding(!showStoreOnboarding)}
- className="bg-[#581c87] hover:bg-purple-800 text-white px-4 py-2.5 rounded-xl text-xs font-bold uppercase tracking-wider flex items-center gap-1.5 cursor-pointer shadow-sm active:scale-95 transition-all border-none"
+ className="bg-[#662582] hover:bg-purple-800 text-white px-4 py-2.5 rounded-xl text-xs font-bold uppercase tracking-wider flex items-center gap-1.5 cursor-pointer shadow-sm active:scale-95 transition-all border-none"
>
{showStoreOnboarding ? 'View Store Directory' : '+ Add Store Branch'}
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
index c4e53fc..221b808 100644
--- a/src/components/Sidebar.tsx
+++ b/src/components/Sidebar.tsx
@@ -35,14 +35,14 @@ export default function Sidebar({
const navItems = [
{ id: 'dashboard' as MainSection, label: 'Dashboard', icon: LayoutDashboard },
{ id: 'stores' as MainSection, label: 'Stores', icon: Store },
- { id: 'inventory' as MainSection, label: 'Product Catalogue', icon: Layers },
+ { id: 'inventory' as MainSection, label: 'Products', icon: Layers },
{ id: 'reports' as MainSection, label: 'Reports', icon: TrendingUp },
{ id: 'settings' as MainSection, label: 'Settings', icon: Settings }
];
return (
{view === 'catalogue' && categories.length > 0 && (
-
Filter
+
Filter
setCategory(e.target.value)}
- className="border border-[#e2e8f0] rounded-lg px-3 py-2 text-xs font-semibold text-zinc-700 bg-[#f8fafc] outline-none focus:ring-1 focus:ring-[#581c87] cursor-pointer"
+ className="border border-[#e2e8f0] rounded-lg px-3 py-2 text-xs font-semibold text-zinc-700 bg-[#f8fafc] outline-none focus:ring-1 focus:ring-[#662582] cursor-pointer"
>
All categories
{categories.map((c) => {c} )}
@@ -220,7 +220,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
action={
{ setSearch(''); setCategory('ALL'); }}
- className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-bold text-white bg-[#581c87] hover:bg-purple-800 transition shadow-sm cursor-pointer"
+ className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-bold text-white bg-[#662582] hover:bg-purple-800 transition shadow-sm cursor-pointer"
>
Clear filters
@@ -235,7 +235,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
{/* Thumbnail with hover zoom */}
-
+
{stocked && (
@@ -244,7 +244,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
-
{p.name}
+ {p.name}
{p.sku}
{/* Category pill badge */}
@@ -258,10 +258,6 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
Price
{p.price > 0 ? `₹${p.price.toLocaleString('en-IN')}` : '—'}
-
- Admin Stock
- {p.adminQty}{p.unit ? ` ${p.unit}` : ''}
-
@@ -278,7 +274,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
{/* Pick action: quantity stepper when selected, else add button */}
{picked ? (
-
Selected
+
Selected
setPickQty(p.id, picks[p.id] - 1)} className="w-6 h-6 rounded-lg border border-[#e2e8f0] text-zinc-500 hover:bg-zinc-50 font-bold cursor-pointer leading-none flex items-center justify-center">
{picks[p.id]}
@@ -289,7 +285,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
) : (
togglePick(p.id)}
- className="w-full flex items-center justify-center gap-1.5 pt-2.5 mt-1 border-t border-[#f1f5f9] text-[11px] font-bold text-[#581c87] hover:text-purple-800 cursor-pointer"
+ className="w-full flex items-center justify-center gap-1.5 pt-2.5 mt-1 border-t border-[#f1f5f9] text-[11px] font-bold text-[#662582] hover:text-purple-800 cursor-pointer"
>
Add to Store
@@ -315,7 +311,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
title="No stock matches your search"
sub="Try a different keyword to find an item in your store."
action={
-
setSearch('')} className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-bold text-white bg-[#581c87] hover:bg-purple-800 transition shadow-sm cursor-pointer">
+ setSearch('')} className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-bold text-white bg-[#662582] hover:bg-purple-800 transition shadow-sm cursor-pointer">
Clear search
}
@@ -326,14 +322,14 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
{/* Thumbnail with status corner dot */}
-
+
-
{it.name}
+ {it.name}
{it.sku}
@@ -391,7 +387,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
setPicks({})} className="px-3 py-2 rounded-xl text-[11px] font-bold text-purple-200 hover:text-white hover:bg-white/10 transition cursor-pointer">Clear
-
+
Request for Store
@@ -415,7 +411,7 @@ function CenterState({ icon, title, sub, action }: { icon: React.ReactNode; titl
{/* Icon with halo */}
-
+
{icon}
diff --git a/src/components/StoreDetailView.tsx b/src/components/StoreDetailView.tsx
index 5ae3ff8..badba9b 100644
--- a/src/components/StoreDetailView.tsx
+++ b/src/components/StoreDetailView.tsx
@@ -386,7 +386,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
Back to Registry
@@ -505,7 +505,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
onClick={() => setActiveTab('overview')}
className={`flex items-center gap-xs px-md pb-sm text-xs font-bold uppercase tracking-wider cursor-pointer border-b-2 transition-all ${
activeTab === 'overview'
- ? 'border-b-[#581c87] text-[#581c87]'
+ ? 'border-b-[#662582] text-[#662582]'
: 'border-b-transparent text-zinc-400 hover:text-zinc-600'
}`}
>
@@ -516,7 +516,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
onClick={() => setActiveTab('inventory')}
className={`flex items-center gap-xs px-md pb-sm text-xs font-bold uppercase tracking-wider cursor-pointer border-b-2 transition-all ${
activeTab === 'inventory'
- ? 'border-b-[#581c87] text-[#581c87]'
+ ? 'border-b-[#662582] text-[#662582]'
: 'border-b-transparent text-zinc-400 hover:text-zinc-600'
}`}
>
@@ -530,7 +530,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
onClick={() => setActiveTab('customers')}
className={`flex items-center gap-xs px-md pb-sm text-xs font-bold uppercase tracking-wider cursor-pointer border-b-2 transition-all ${
activeTab === 'customers'
- ? 'border-b-[#581c87] text-[#581c87]'
+ ? 'border-b-[#662582] text-[#662582]'
: 'border-b-transparent text-zinc-400 hover:text-zinc-600'
}`}
>
@@ -541,7 +541,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
onClick={() => setActiveTab('orders')}
className={`flex items-center gap-xs px-md pb-sm text-xs font-bold uppercase tracking-wider cursor-pointer border-b-2 transition-all ${
activeTab === 'orders'
- ? 'border-b-[#581c87] text-[#581c87]'
+ ? 'border-b-[#662582] text-[#662582]'
: 'border-b-transparent text-zinc-400 hover:text-zinc-600'
}`}
>
@@ -552,7 +552,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
onClick={() => setActiveTab('qr')}
className={`flex items-center gap-xs px-md pb-sm text-xs font-bold uppercase tracking-wider cursor-pointer border-b-2 transition-all ${
activeTab === 'qr'
- ? 'border-b-[#581c87] text-[#581c87]'
+ ? 'border-b-[#662582] text-[#662582]'
: 'border-b-transparent text-zinc-400 hover:text-zinc-600'
}`}
>
@@ -569,7 +569,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
{/* Top Metric Cards */}
-
+
Monthly Revenue
@@ -628,7 +628,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
- Dispatch Flow Pipeline
+ Dispatch Flow Pipeline
Audit orders & revenue progression by selecting nodes along the daily operational path.
@@ -645,8 +645,8 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
-
-
+
+
@@ -660,7 +660,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
{label}
-
+
Revenue: ₹{payload[0].value.toLocaleString('en-IN')}
@@ -674,7 +674,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
return null;
}}
/>
-
+
)}
@@ -686,7 +686,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
- Node Operations Command
+ Node Operations Command
Automated actions for local outlet hubs.
@@ -694,17 +694,17 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
setActiveTab('inventory')}
- className="w-full flex items-center justify-between p-sm border border-[#e2e8f0] rounded-xl hover:border-purple-300 hover:bg-purple-50/20 text-left text-xs font-semibold text-zinc-700 hover:text-[#581c87] transition cursor-pointer"
+ className="w-full flex items-center justify-between p-sm border border-[#e2e8f0] rounded-xl hover:border-purple-300 hover:bg-purple-50/20 text-left text-xs font-semibold text-zinc-700 hover:text-[#662582] transition cursor-pointer"
>
- Replenish Critical Stock
+ Replenish Critical Stock
ALERTS
Export Compliance Ledger
@@ -714,12 +714,12 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
Broadcast Terminal SMS
- SMS Blast
+ SMS Blast
@@ -744,7 +744,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
placeholder="Search inventory by product or SKU..."
value={stockSearch}
onChange={(e) => setStockSearch(e.target.value)}
- className="w-full pl-9 pr-4 py-2.5 border border-[#e2e8f0] rounded-xl text-xs outline-none bg-[#f8fafc] focus:bg-white focus:ring-1 focus:ring-[#581c87] transition-all"
+ className="w-full pl-9 pr-4 py-2.5 border border-[#e2e8f0] rounded-xl text-xs outline-none bg-[#f8fafc] focus:bg-white focus:ring-1 focus:ring-[#662582] transition-all"
/>
@@ -754,7 +754,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
<>
{ setImportState('idle'); setShowImportModal(true); }}
- className="px-3 py-2 bg-purple-50 text-[#581c87] hover:bg-purple-100/80 border border-purple-100 rounded-xl font-bold flex items-center gap-xs cursor-pointer transition shadow-sm"
+ className="px-3 py-2 bg-purple-50 text-[#662582] hover:bg-purple-100/80 border border-purple-100 rounded-xl font-bold flex items-center gap-xs cursor-pointer transition shadow-sm"
>
Import Manual (CSV)
@@ -779,7 +779,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
Product Stock Levels
- Live list
+ Live list
@@ -870,7 +870,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
className={`px-3 py-1 rounded-lg text-[10px] font-bold hover:shadow-sm transition cursor-pointer ${
item.status === 'Critical'
? 'bg-rose-500 text-white hover:bg-rose-600'
- : 'border border-zinc-200 text-zinc-700 hover:border-[#581c87] hover:text-[#581c87]'
+ : 'border border-zinc-200 text-zinc-700 hover:border-[#662582] hover:text-[#662582]'
}`}
>
Replenish
@@ -971,7 +971,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
{customerSearch && (
-
setCustomerSearch('')} className="mt-2 text-[13px] font-bold text-[#581c87] hover:text-[#4c1d95] bg-purple-50 px-4 py-2 rounded-lg transition-colors cursor-pointer">Clear Search
+
setCustomerSearch('')} className="mt-2 text-[13px] font-bold text-[#662582] hover:text-[#4c1d95] bg-purple-50 px-4 py-2 rounded-lg transition-colors cursor-pointer">Clear Search
)}
) : (
@@ -980,7 +980,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
const tone = toneFor(c.name || `c${idx}`);
const locality = localityOf(c.address);
return (
-
+
-
{c.name}
+
{c.name}
{locality && (
{locality}
diff --git a/src/components/StoreQRView.tsx b/src/components/StoreQRView.tsx
index 5b0256c..90c9067 100644
--- a/src/components/StoreQRView.tsx
+++ b/src/components/StoreQRView.tsx
@@ -78,7 +78,7 @@ export default function StoreQRView({
{/* Gradient Header banner */}
-
+
Scan & Shop
@@ -121,7 +121,7 @@ export default function StoreQRView({
Download QR Code
diff --git a/src/components/UserStorePage.tsx b/src/components/UserStorePage.tsx
index 09ae019..284e757 100644
--- a/src/components/UserStorePage.tsx
+++ b/src/components/UserStorePage.tsx
@@ -46,7 +46,7 @@ interface UserStorePageProps {
// gets a matching branch in `renderSection` below.
const NAV_ITEMS: UserNavItem[] = [
{ id: 'console', label: 'Store Console', icon: LayoutDashboard },
- { id: 'inventory', label: 'Product Catalogue', icon: Layers },
+ { id: 'inventory', label: 'Products', icon: Layers },
{ id: 'customers', label: 'Customers', icon: Users },
{ id: 'dispatch', label: 'Dispatch', icon: Route },
{ id: 'reports', label: 'Reports', icon: ClipboardList },
@@ -145,7 +145,7 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) {
Your profile and the store you’re assigned to.
-
+
{initials}
@@ -201,7 +201,7 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) {
if (locationsQ.isLoading || locSummaryQ.isLoading) {
return (
);
@@ -225,7 +225,7 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) {
locationsQ.refetch();
locSummaryQ.refetch();
}}
- className="px-5 py-2.5 bg-[#581c87] hover:bg-purple-800 text-white text-sm font-bold rounded-xl cursor-pointer transition-colors shadow-sm"
+ className="px-5 py-2.5 bg-[#662582] hover:bg-purple-800 text-white text-sm font-bold rounded-xl cursor-pointer transition-colors shadow-sm"
>
Retry
@@ -272,9 +272,29 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) {
onAccountClick={() => setActiveSection('account')}
onQrClick={() => setShowQrModal(true)}
profile={profile}
+ storeContext={{
+ storeName: activeSection === 'console'
+ ? storeName
+ : activeSection === 'inventory'
+ ? 'Products'
+ : activeSection === 'account'
+ ? 'My Account'
+ : activeSection.charAt(0).toUpperCase() + activeSection.slice(1),
+ icon: activeSection === 'console'
+ ? LayoutDashboard
+ : activeSection === 'inventory'
+ ? Layers
+ : activeSection === 'customers'
+ ? Users
+ : activeSection === 'dispatch'
+ ? Route
+ : activeSection === 'reports'
+ ? ClipboardList
+ : undefined
+ }}
/>
-
+
{activeSection === 'dispatch' ? (
diff --git a/src/components/UserStoreSidebar.tsx b/src/components/UserStoreSidebar.tsx
index 7635c8d..b58e994 100644
--- a/src/components/UserStoreSidebar.tsx
+++ b/src/components/UserStoreSidebar.tsx
@@ -29,7 +29,7 @@ interface UserStoreSidebarProps {
export default function UserStoreSidebar({ items, activeId, onSelect, isOpen }: UserStoreSidebarProps) {
return (
@@ -47,8 +47,8 @@ export default function UserStoreSidebar({ items, activeId, onSelect, isOpen }:
isOpen ? 'gap-md px-md' : 'justify-center px-0'
} ${
isActive
- ? 'bg-purple-800 text-white font-semibold' + (isOpen ? ' border-l-4 border-white' : '')
- : 'text-purple-200 hover:bg-purple-800/60 hover:text-white'
+ ? 'bg-black/20 text-white font-semibold' + (isOpen ? ' border-l-4 border-white' : '')
+ : 'text-purple-200 hover:bg-white/10 hover:text-white'
}`}
>
diff --git a/src/services/fiestaApi.ts b/src/services/fiestaApi.ts
index a562253..28b7d93 100644
--- a/src/services/fiestaApi.ts
+++ b/src/services/fiestaApi.ts
@@ -973,6 +973,20 @@ export async function createTenantLocation(input: CreateTenantLocationInput): Pr
return fiestaSend('tenants/createtenantlocation', 'POST', input);
}
+export interface UpdateTenantLocationInput {
+ locationid: number;
+ tenantid?: number;
+ locationname?: string;
+ contactno?: string;
+ email?: string;
+ status?: string;
+}
+
+/** PUT /tenants/updatetenantlocation — Update store location details/status. */
+export async function updateTenantLocation(input: UpdateTenantLocationInput): Promise {
+ return fiestaSend('tenants/updatetenantlocation', 'PUT', input);
+}
+
// ════════════════════════════════════════════════════════════════════════════
// RIDERS / DISPATCH
// ════════════════════════════════════════════════════════════════════════════
diff --git a/src/services/fiestaQueries.ts b/src/services/fiestaQueries.ts
index 8b16aaa..f532199 100644
--- a/src/services/fiestaQueries.ts
+++ b/src/services/fiestaQueries.ts
@@ -59,6 +59,8 @@ import {
createTenantLocation,
CreateTenantInput,
CreateTenantLocationInput,
+ updateTenantLocation,
+ UpdateTenantLocationInput,
} from './fiestaApi';
export const fiestaKeys = {
@@ -466,6 +468,112 @@ export function useFiestaStoresStock(
}));
}
+export function useFiestaStoresOrderSummary(
+ tenantid: number,
+ locations: Array<{ locationid: number; locationname: string }>,
+ fromdate: string,
+ todate: string
+) {
+ const results = useQueries({
+ queries: locations.map((loc) => ({
+ queryKey: fiestaKeys.orderSummary(tenantid, fromdate, todate, loc.locationid),
+ queryFn: () => getOrderSummary(tenantid, fromdate, todate, loc.locationid),
+ enabled: Boolean(tenantid && loc.locationid && fromdate && todate),
+ })),
+ });
+
+ const agg = { total: 0, created: 0, pending: 0, processing: 0, delivered: 0, cancelled: 0 };
+ let isLoading = false;
+ results.forEach(res => {
+ if (res.isLoading) isLoading = true;
+ if (res.data) {
+ agg.total += res.data.total;
+ agg.created += res.data.created;
+ agg.pending += res.data.pending;
+ agg.processing += res.data.processing;
+ agg.delivered += res.data.delivered;
+ agg.cancelled += res.data.cancelled;
+ }
+ });
+
+ return { isLoading, data: agg };
+}
+
+export function useFiestaStoresRevenueSummary(
+ tenantid: number,
+ locations: Array<{ locationid: number; locationname: string }>,
+ fromdate: string,
+ todate: string
+) {
+ const results = useQueries({
+ queries: locations.map((loc) => ({
+ queryKey: fiestaKeys.revenueSummary({ scope: 'stores', tenantid, fromdate, todate, locationid: loc.locationid }),
+ queryFn: () => getRevenueSummary({ tenantid, fromdate, todate, locationid: loc.locationid }),
+ enabled: Boolean(tenantid && loc.locationid && fromdate && todate),
+ })),
+ });
+
+ const agg = { grossrevenue: 0, netrevenue: 0, profit: 0, ordercount: 0, avgordervalue: 0 };
+ let isLoading = false;
+ results.forEach(res => {
+ if (res.isLoading) isLoading = true;
+ if (res.data) {
+ agg.grossrevenue += res.data.grossrevenue;
+ agg.netrevenue += res.data.netrevenue;
+ agg.profit += res.data.profit;
+ agg.ordercount += res.data.ordercount;
+ }
+ });
+ if (agg.ordercount > 0) {
+ agg.avgordervalue = Math.round(agg.grossrevenue / agg.ordercount);
+ }
+
+ return { isLoading, data: agg };
+}
+
+export function useFiestaStoresTimeSeries(
+ tenantid: number,
+ locations: Array<{ locationid: number; locationname: string }>,
+ granularity: 'day' | 'month' | 'year',
+ fromdate: string,
+ todate: string
+) {
+ const results = useQueries({
+ queries: locations.map((loc) => ({
+ queryKey: fiestaKeys.timeSeries({ scope: 'stores', tenantid, granularity, fromdate, todate, locationid: loc.locationid }),
+ queryFn: () => getTimeSeries({ tenantid, granularity, fromdate, todate, locationid: loc.locationid }),
+ enabled: Boolean(tenantid && loc.locationid && fromdate && todate),
+ })),
+ });
+
+ const map = new Map();
+ let isLoading = false;
+ results.forEach(res => {
+ if (res.isLoading) isLoading = true;
+ if (res.data) {
+ res.data.forEach((d: any) => {
+ const label = d.label || d.date || d.createdat || 'Unknown';
+ if (!map.has(label)) {
+ map.set(label, { label, orders: 0, revenue: 0, cancelled: 0, skus: 0 });
+ }
+ const existing = map.get(label);
+ existing.orders += Number(d.orders || d.totalorders || d.total || 0);
+ existing.revenue += Number(d.revenue || d.grossrevenue || d.overallrevenue || 0);
+ existing.cancelled += Number(d.cancelled || 0);
+ existing.skus += Number(d.activeskus || d.skus || 0);
+ });
+ }
+ });
+
+ const sortedData = Array.from(map.values()).sort((a, b) => {
+ if (a.label === 'Unknown') return 1;
+ if (b.label === 'Unknown') return -1;
+ return new Date(a.label).getTime() - new Date(b.label).getTime();
+ });
+
+ return { isLoading, data: sortedData };
+}
+
// ── Order details / customer history ───────────────────────────────────────────
export function useFiestaOrderDetails(orderheaderid: number | string | null | undefined) {
return useQuery({
@@ -639,6 +747,18 @@ export function useFiestaCreateLocation() {
});
}
+/** Update a tenant location, then refresh tenant locations list on success. */
+export function useFiestaUpdateLocation() {
+ const qc = useQueryClient();
+ return useMutation({
+ mutationFn: (input: UpdateTenantLocationInput) => updateTenantLocation(input),
+ onSuccess: () => {
+ qc.invalidateQueries({ queryKey: ['fiesta', 'tenantLocations'] });
+ qc.invalidateQueries({ queryKey: ['fiesta', 'locationSummary'] });
+ },
+ });
+}
+
// ── Auth ──────────────────────────────────────────────────────────────────────
/**
diff --git a/src/types.ts b/src/types.ts
index 5d7dccb..12e8728 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -47,6 +47,7 @@ export interface ProductMatrixItem {
category: string;
exposure: string;
verified: boolean;
+ isNew?: boolean;
}
export interface InventoryItem {