diff --git a/src/App.tsx b/src/App.tsx index 253bbdb..8051983 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,13 +25,19 @@ import { Phone, Activity, TrendingUp, - Award + Award, + Layers, + Store, + Settings, + LayoutDashboard } from 'lucide-react'; import { MainSection } from './types'; import { useFiestaTenantLocations, useFiestaLocationSummary, + useFiestaUpdateLocation, + useFiestaOrderSummary, } from './services/fiestaQueries'; import { FIESTA_TENANT_ID, str as fstr } from './services/fiestaApi'; import Sidebar from './components/Sidebar'; @@ -113,6 +119,15 @@ export default function App() { // under Settings → Users & Access (see UsersPanel). const locationsQ = useFiestaTenantLocations(tenantId); const locSummaryQ = useFiestaLocationSummary(tenantId); + const updateLocation = useFiestaUpdateLocation(); + + const today = new Date(); + const monthStart = new Date(today); + monthStart.setDate(today.getDate() - 30); + const ymd = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; + const fromdate = ymd(monthStart); + const todate = ymd(today); + const summaryQ = useFiestaOrderSummary(tenantId, fromdate, todate); const STORE_COVERS = [ 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&w=600&q=80', @@ -143,7 +158,7 @@ export default function App() { const [storesList, setStoresList] = useState>([]); const [storesSearch, setStoresSearch] = useState(''); - const [storesFilter, setStoresFilter] = useState<'ALL' | 'ACTIVE' | 'CRITICAL'>('ALL'); + const [storesFilter, setStoresFilter] = useState<'ALL' | 'ACTIVE' | 'INACTIVE'>('ALL'); const filteredStoresList = storesList.filter((st) => { const q = storesSearch.trim().toLowerCase(); @@ -158,10 +173,10 @@ export default function App() { if (storesFilter === 'ACTIVE') { return matchesSearch && st.status.toLowerCase() === 'active'; } - if (storesFilter === 'CRITICAL') { + if (storesFilter === 'INACTIVE') { return ( matchesSearch && - (st.status.toLowerCase() !== 'active' || st.deliveries > 40) + (st.status.toLowerCase() !== 'active') ); } return matchesSearch; @@ -257,13 +272,6 @@ export default function App() { This merchant operates a single store. Add a branch to manage multiple outlets.

- )} - - {/* Filter control bar */} @@ -309,7 +309,7 @@ export default function App() { placeholder="Search stores by name, zone, or manager..." value={storesSearch} onChange={(e) => setStoresSearch(e.target.value)} - className="w-full pl-10 pr-4 py-2 bg-zinc-50 border border-zinc-200 rounded-lg text-xs font-medium text-zinc-800 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-[#581c87]/20 focus:border-[#581c87] transition-all" + className="w-full pl-10 pr-4 py-2 bg-zinc-50 border border-zinc-200 rounded-lg text-xs font-medium text-zinc-800 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-[#662582]/20 focus:border-[#662582] transition-all" /> {storesSearch && ( @@ -361,7 +361,7 @@ export default function App() {
{locationsQ.isLoading ? (
-
+
Loading live store locations…
) : ( @@ -435,7 +435,7 @@ export default function App() {
Total Orders -

{totalOrders.toLocaleString()}

+

{totalOrders.toLocaleString()}

Incoming Volume
@@ -479,7 +479,7 @@ export default function App() {
Speed Index (Live Feed) - + Live
@@ -492,7 +492,7 @@ export default function App() { st.status.toLowerCase() === 'active' ? st.deliveries > 40 ? 'bg-rose-500/80 group-hover/bar:bg-rose-500' - : 'bg-[#581c87]/70 group-hover/bar:bg-[#581c87]' + : 'bg-[#662582]/70 group-hover/bar:bg-[#662582]' : 'bg-amber-500/70 group-hover/bar:bg-amber-500' }`} /> @@ -518,7 +518,7 @@ export default function App() { e.stopPropagation(); alert(`Routing communications channel directly to manager ${st.staff}...`); }} - className="p-1.5 rounded-lg bg-white border border-zinc-200 hover:border-[#581c87] hover:text-[#581c87] text-zinc-500 transition-colors shadow-sm" + className="p-1.5 rounded-lg bg-white border border-zinc-200 hover:border-[#662582] hover:text-[#662582] text-zinc-500 transition-colors shadow-sm" title="Communicate with Node Lead" > @@ -526,9 +526,26 @@ export default function App() {
{/* Card footer - enter console */} -
- Enter Terminal Console - +
+ + +
+ Enter Terminal Console + +
@@ -571,10 +588,28 @@ export default function App() { onHelpClick={handleHelp} onLogoutClick={handleLogout} profile={currentUser} + storeContext={{ + storeName: currentSection === 'dashboard' + ? (summaryQ.data?.tenantname ? `${summaryQ.data.tenantname} Admin` : 'Admin Console') + : currentSection === 'inventory' + ? 'Products' + : currentSection.charAt(0).toUpperCase() + currentSection.slice(1), + icon: currentSection === 'dashboard' + ? LayoutDashboard + : currentSection === 'inventory' + ? Layers + : currentSection === 'stores' + ? Store + : currentSection === 'reports' + ? TrendingUp + : currentSection === 'settings' + ? Settings + : undefined + }} /> {/* Main Container workspace layout splits */} -
+
{/* Interactive Left Rail */} {/* Main core pages payload area */} -
+
{/* Nav content routing */} {currentSection === 'dashboard' && ( @@ -629,7 +664,7 @@ export default function App() {

- + Scheduled Reports Calendar

@@ -702,7 +737,7 @@ export default function App() { placeholder="e.g. Coimbatore North" value={newStore.zone} onChange={(e) => setNewStore({ ...newStore, zone: e.target.value })} - className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87]" + className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]" required />
@@ -714,7 +749,7 @@ export default function App() { placeholder="e.g. Sridhar Sundaram" value={newStore.lead} onChange={(e) => setNewStore({ ...newStore, lead: e.target.value })} - className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87]" + className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]" required />
@@ -726,7 +761,7 @@ export default function App() { placeholder="₹1,50,000" value={newStore.sales} onChange={(e) => setNewStore({ ...newStore, sales: e.target.value })} - className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87]" + className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]" />
@@ -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()); @@ -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

); 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({ nearledaily logo @@ -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 */} -
- Catalogue Command Center Banner -
- - {/* 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 */} -
- - - -
-
- - {/* Small card metrics grid - 4 dynamic columns inside the banner */} -
- + {/* Header and Metrics */} +
+ {/* Small card metrics grid */} +
{/* Card 1: Total SKUs */} -
+
- Total SKUs -
- + Total SKUs +
+
-
-

+
+

{products.length}

-

Master catalogue

+

Master catalogue

{/* Card 2: Synced Outlets */} -
+
- Active Outlets -
- + Active Outlets +
+
-
-

+
+

{locations.length}

-

Synced locations

+

Synced locations

{/* Card 3: Total On-Hand Volume */} -
+
- Total Stock -
- + 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({
-
- Revenue - ₹{prod.revenue.toLocaleString()} -
-
+ ) : ( + + )}
-
- - {/* Exposure toggle row */} -
- - - {prod.verified ? 'Active Portfolio' : 'Under Inspection'} - - - -
- - {/* Store-catalogue curation: pick the product + quantity to show to store users */} - {storeCat.has(prod.id) ? ( -
- In Store Catalogue -
- - {storeCat.getQty(prod.id)} - - -
-
- ) : ( - - )} + ))}
- ))} +
+ + {/* 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.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'} + + + +
+ + {/* Store-catalogue curation: pick the product to show to store users */} + {storeCat.has(prod.id) ? ( +
+ In 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({