diff --git a/src/components/DeliveriesView.tsx b/src/components/DeliveriesView.tsx index c3b852e..61506c4 100644 --- a/src/components/DeliveriesView.tsx +++ b/src/components/DeliveriesView.tsx @@ -27,7 +27,7 @@ import { DELIVERY_STATUS, statusColor, BRAND, BRAND_LIGHT, TEXT, TEXT_2, TEXT_3, BORDER, DIVIDER, SURFACE_ALT, tint, soft, edge, } from './consoleUi'; -interface DeliveriesViewProps { searchQuery?: string; locationid?: number; tenantId?: number; } +interface DeliveriesViewProps { searchQuery?: string; locationid?: number; tenantId?: number; headerTabs?: React.ReactNode; } type DeliveryStatus = 'pending' | 'accepted' | 'arrived' | 'picked' | 'active' | 'skipped' | 'delivered' | 'cancelled'; const STATUS_TABS: Array<{ key: DeliveryStatus; label: string }> = [ @@ -57,7 +57,7 @@ function inBatch(r: Row, b: BatchId): boolean { return h >= 16 && h < 19; } -export default function DeliveriesView({ searchQuery = '', locationid, tenantId = FIESTA_TENANT_ID }: DeliveriesViewProps) { +export default function DeliveriesView({ searchQuery = '', locationid, tenantId = FIESTA_TENANT_ID, headerTabs }: DeliveriesViewProps) { const today = new Date(); const monthStart = new Date(today.getFullYear(), today.getMonth(), 1); const dayOffset = (n: number) => { const d = new Date(); d.setDate(d.getDate() - n); return ymd(d); }; @@ -66,6 +66,7 @@ export default function DeliveriesView({ searchQuery = '', locationid, tenantId const [todate, setTodate] = useState(ymd(today)); const presets = [ { key: 'today', label: 'Today', from: ymd(today), to: ymd(today) }, + { key: 'yesterday', label: 'Yesterday', from: dayOffset(1), to: dayOffset(1) }, { key: '7d', label: 'Last 7 Days', from: dayOffset(6), to: ymd(today) }, { key: 'month', label: 'This Month', from: ymd(monthStart), to: dayAhead(7) }, ]; @@ -122,9 +123,12 @@ export default function DeliveriesView({ searchQuery = '', locationid, tenantId : } right={ - - Coimbatore - +
+ {headerTabs} + + Coimbatore + +
} /> diff --git a/src/components/DispatchHubView.tsx b/src/components/DispatchHubView.tsx new file mode 100644 index 0000000..a627d0c --- /dev/null +++ b/src/components/DispatchHubView.tsx @@ -0,0 +1,61 @@ +/** + * @license + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useState } from 'react'; +import { Route, ShoppingBag, Truck } from 'lucide-react'; +import DispatchView from './DispatchView'; +import OrdersView from './OrdersView'; +import DeliveriesView from './DeliveriesView'; + +interface DispatchHubViewProps { + locationid?: number; + tenantId?: number; +} + +export default function DispatchHubView({ locationid, tenantId }: DispatchHubViewProps) { + const [activeTab, setActiveTab] = useState<'map' | 'orders' | 'deliveries'>('map'); + + const renderTabs = () => ( +
+ + + + + +
+ ); + + return ( +
+ {/* Content Area - Header removed as requested, tabs passed down */} +
+ {activeTab === 'map' && } + {activeTab === 'orders' && } + {activeTab === 'deliveries' && } +
+
+ ); +} diff --git a/src/components/DispatchView.tsx b/src/components/DispatchView.tsx index c367aa0..ca38948 100644 --- a/src/components/DispatchView.tsx +++ b/src/components/DispatchView.tsx @@ -87,13 +87,11 @@ function pickupLatLon(r: Row): [number, number] | null { } // ── View modes (match #strat-row tabs) ─────────────────────────────────────────── -type ViewMode = 'kitchens' | 'zones' | 'riders' | 'orders' | 'deliveries'; +type ViewMode = 'kitchens' | 'zones' | 'riders'; const VIEW_TABS: Array<{ id: ViewMode; label: string; icon: typeof MapIcon }> = [ { id: 'kitchens', label: 'By Location', icon: MapPin }, { id: 'zones', label: 'By Zone', icon: MapIcon }, { id: 'riders', label: 'By Rider', icon: Bike }, - { id: 'orders', label: 'By Orders', icon: ShoppingBag }, - { id: 'deliveries', label: 'By Deliveries', icon: Truck }, ]; interface Group { @@ -112,12 +110,13 @@ interface Group { interface DispatchViewProps { locationid?: number; tenantId?: number; + headerTabs?: React.ReactNode; } const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; -export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID }: DispatchViewProps) { +export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID, headerTabs }: DispatchViewProps) { const today = new Date(); const [date, setDate] = useState(ymd(today)); const [viewMode, setViewMode] = useState('riders'); @@ -164,16 +163,6 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID } const name = fstr(r.pickupcustomer) || fstr(r.pickuplocation) || 'Pickup'; return { id: name.toLowerCase(), name }; } - if (viewMode === 'orders') { - // Bucket by ORDER status (created / pending / processing / delivered / cancelled). - const s = fstr(r.orderstatus).toLowerCase() || 'unknown'; - return { id: `o:${s}`, name: titleCase(s) }; - } - if (viewMode === 'deliveries') { - // Bucket by DELIVERY/dispatch status (falls back to order status, then unassigned). - const s = (fstr(r.deliverystatus) || fstr(r.orderstatus)).toLowerCase() || 'unassigned'; - return { id: `d:${s}`, name: titleCase(s) }; - } const name = fstr(r.deliverysuburb) || fstr(r.zone_name) || 'Unzoned'; return { id: name.toLowerCase(), name }; }; @@ -294,15 +283,18 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID }
{/* ── Header ── */}
-
-
D
-
Dispatch
-
- - - Coimbatore - +
+
+
D
+
Dispatch
+
+ + + Coimbatore + +
+ {headerTabs}
@@ -430,7 +422,7 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID } ) : ( <>
- {viewMode === 'riders' ? 'Riders' : viewMode === 'kitchens' ? 'Pickup points' : viewMode === 'orders' ? 'Order statuses' : viewMode === 'deliveries' ? 'Delivery statuses' : 'Zones'} ({groups.length}) + {viewMode === 'riders' ? 'Riders' : viewMode === 'kitchens' ? 'Pickup points' : 'Zones'} ({groups.length})
{groups.map((g) => ( diff --git a/src/components/OrdersView.tsx b/src/components/OrdersView.tsx index fb9be6b..c57cdb3 100644 --- a/src/components/OrdersView.tsx +++ b/src/components/OrdersView.tsx @@ -27,6 +27,7 @@ interface OrdersViewProps { locationid?: number; /** Merchant tenant to scope to; defaults to the shared constant. */ tenantId?: number; + headerTabs?: React.ReactNode; } type StatusKey = 'created' | 'pending' | 'processing' | 'delivered' | 'cancelled'; @@ -39,7 +40,7 @@ const STATUS_TABS: Array<{ key: StatusKey; label: string }> = [ ]; const PAGE_SIZE = 25; -export default function OrdersView({ searchQuery = '', locationid, tenantId = FIESTA_TENANT_ID }: OrdersViewProps) { +export default function OrdersView({ searchQuery = '', locationid, tenantId = FIESTA_TENANT_ID, headerTabs }: OrdersViewProps) { const today = new Date(); const monthStart = new Date(today.getFullYear(), today.getMonth(), 1); const [fromdate, setFromdate] = useState(ymd(today)); @@ -54,6 +55,7 @@ export default function OrdersView({ searchQuery = '', locationid, tenantId = FI // a wide window — from the platform's earliest plausible data to a year ahead. const presets = [ { key: 'today', label: 'Today', from: ymd(today), to: ymd(today) }, + { key: 'yesterday', label: 'Yesterday', from: dayOffset(1), to: dayOffset(1) }, { key: '7d', label: 'Last 7 Days', from: dayOffset(6), to: ymd(today) }, { key: 'month', label: 'This Month', from: ymd(monthStart), to: dayAhead(7) }, ]; @@ -252,8 +254,8 @@ export default function OrdersView({ searchQuery = '', locationid, tenantId = FI return (
@@ -262,9 +264,12 @@ export default function OrdersView({ searchQuery = '', locationid, tenantId = FI : } right={ - - Coimbatore - +
+ {headerTabs} + + Coimbatore + +
} /> diff --git a/src/components/SettingsView.tsx b/src/components/SettingsView.tsx index a39d8e4..ac7ea44 100644 --- a/src/components/SettingsView.tsx +++ b/src/components/SettingsView.tsx @@ -120,6 +120,10 @@ function Row({ ); } +export interface SettingsViewProps { + tenantId?: number; +} + export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsViewProps) { const [activeTab, setActiveTab] = useState('profile'); diff --git a/src/components/StoreDetailView.tsx b/src/components/StoreDetailView.tsx index 1c6d0bc..af9cfc7 100644 --- a/src/components/StoreDetailView.tsx +++ b/src/components/StoreDetailView.tsx @@ -344,8 +344,9 @@ export default function StoreDetailView({ store, onBack, canManage = true, only, )} {/* ── Subheader Navigation Bar ── */} -
- {onBack && ( + {/* ── Subheader Navigation Bar ── */} + {onBack && ( +
- )} - -
- - System Sync Active
-
+ )} {/* ── Immersive Analytics Banner — hidden on the standalone Inventory & Customers pages ── */} {showHero && ( @@ -378,6 +374,12 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
+ {/* System Sync Active Indicator */} +
+ + System Sync Active +
+
@@ -888,124 +890,139 @@ export default function StoreDetailView({ store, onBack, canManage = true, only, return (
- {/* Page heading */} -
-
-

Customers

-

- {customersList.length} {customersList.length === 1 ? 'person orders' : 'people order'} from{' '} - {store.name} - · - {withPhone} with phone · {withEmail} with email -

-
-
- - setCustomerSearch(e.target.value)} - className="w-full pl-9 pr-3 py-2.5 border border-[#e6e8ee] rounded-full text-[13px] text-[#0f172a] placeholder:text-zinc-400 outline-none bg-white focus:border-[#581c87] focus:ring-4 focus:ring-[#581c87]/8 transition-all" - /> + {/* Page heading - Immersive Banner (Light mode to match Reports) */} +
+ {/* Background glowing circles */} +
+
+ +
+
+
+
+ Store Audience Registry +
+
+ + System Sync Active +
+
+

+ Customer CRM +

+
+ + {customersList.length} {customersList.length === 1 ? 'customer orders' : 'customers order'} from {store.name} + + · + {withPhone} with phone + · + {withEmail} with email +
+
+ +
+ + setCustomerSearch(e.target.value)} + className="w-full pl-8 pr-3 py-2 border border-[#662582]/20 rounded-xl text-[12px] text-[#0f172a] placeholder:text-zinc-400 outline-none bg-white/60 backdrop-blur-md focus:border-[#662582] focus:ring-4 focus:ring-[#662582]/10 transition-all shadow-[inset_0_2px_4px_rgba(0,0,0,0.02)]" + /> +
{/* Profile cards */} {customersList.length === 0 ? ( -
- +
+
-

No customers yet

-

- {customerSearch ? 'Nothing matches your search.' : 'Customers will appear here once they place their first order.'} +

No customers yet

+

+ {customerSearch ? 'Nothing matches your search criteria.' : 'Customers will appear here once they place their first order.'}

{customerSearch && ( - + )}
) : ( -
-
- - - - - - - - - - - - {customersList.map((c: any, idx: number) => { - const tone = toneFor(c.name || `c${idx}`); - const locality = localityOf(c.address); - return ( - - {/* Customer */} - - {/* Phone */} - - {/* Email */} - - {/* Address */} - - {/* Action */} - - - ); - })} - -
CustomerPhoneEmailDelivery addressProfile
-
- - {initialsOf(c.name)} - -
-

{c.name}

- {locality && ( -

- {locality} -

- )} -
-
-
- {c.phone} - - {c.email - ? {c.email} - : } - - {c.address} - -
- {canManage && ( - - )} - -
-
-
-
- Showing {customersList.length} {customersList.length === 1 ? 'customer' : 'customers'} -
+
+ {customersList.map((c: any, idx: number) => { + const tone = toneFor(c.name || `c${idx}`); + const locality = localityOf(c.address); + return ( +
+
+
+ + {initialsOf(c.name)} + +
+

{c.name}

+ {locality && ( +

+ {locality} +

+ )} +
+
+ {c.ordersCount > 5 && ( + + Loyal + + )} +
+ +
+
+ Dispatches + {c.ordersCount} +
+
+ Gross Spend + {c.totalSpent} +
+
+ +
+
+
+ {c.phone} +
+ {c.email && c.email !== '—' && ( +
+
+ {c.email} +
+ )} +
+ +
+ {canManage && ( + + )} + +
+
+ ); + })}
)}
diff --git a/src/components/UserStorePage.tsx b/src/components/UserStorePage.tsx index 818663d..8df9dcf 100644 --- a/src/components/UserStorePage.tsx +++ b/src/components/UserStorePage.tsx @@ -30,9 +30,7 @@ import type { AuthUser } from '../services/auth'; import Header from './Header'; import StoreDetailView from './StoreDetailView'; import StoreCatalogView from './StoreCatalogView'; -import OrdersView from './OrdersView'; -import DeliveriesView from './DeliveriesView'; -import DispatchView from './DispatchView'; +import DispatchHubView from './DispatchHubView'; import DeliveryReportsView from './DeliveryReportsView'; import StoreQRView from './StoreQRView'; import UserStoreSidebar, { type UserNavItem } from './UserStoreSidebar'; @@ -50,8 +48,6 @@ const NAV_ITEMS: UserNavItem[] = [ { id: 'console', label: 'Store Console', icon: LayoutDashboard }, { id: 'inventory', label: 'Product Catalogue', icon: Layers }, { id: 'customers', label: 'Customers', icon: Users }, - { id: 'orders', label: 'Orders', icon: ShoppingBag }, - { id: 'deliveries', label: 'Deliveries', icon: Truck }, { id: 'dispatch', label: 'Dispatch', icon: Route }, { id: 'reports', label: 'Reports', icon: ClipboardList }, ]; @@ -194,9 +190,7 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) { // Logistics console — scoped to this user's store. These views own their // loading/error states, so they don't need the store-console load gating below. - if (activeSection === 'orders') return ; - if (activeSection === 'deliveries') return ; - if (activeSection === 'dispatch') return ; + if (activeSection === 'dispatch') return ; if (activeSection === 'reports') return ; // Inventory & Catalog is its own page: the manager-curated catalog the user // stocks from (the catalog query is tenant-level, so it doesn't need the store diff --git a/src/components/UsersPanel.tsx b/src/components/UsersPanel.tsx index 1524c5c..37597ae 100644 --- a/src/components/UsersPanel.tsx +++ b/src/components/UsersPanel.tsx @@ -26,7 +26,8 @@ import { SlidersHorizontal, Coins, Store, - Bike + Bike, + Clock } from 'lucide-react'; import { useFiestaUsers, useFiestaCreateUser, useFiestaRiderShifts, useFiestaTenantLocations } from '../services/fiestaQueries'; import { useAppRoles } from '../services/queries';