weekend update

This commit is contained in:
2026-06-12 18:04:10 +05:30
parent 0519e3b19c
commit 896561245d
8 changed files with 235 additions and 157 deletions

View File

@@ -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<string>(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
: <LiveStatus state="live" label={`Live · ${batchRows.length} in this wave · ${activeFleet} riders on duty`} />
}
right={
<span className="inline-flex items-center gap-1.5 rounded-full font-extrabold" style={{ padding: '6px 12px', fontSize: 12, background: tint(BRAND), border: `1.5px solid ${edge(BRAND)}`, color: BRAND }}>
<MapPin size={13} /> Coimbatore
</span>
<div className="flex items-center gap-3">
{headerTabs}
<span className="inline-flex items-center gap-1.5 rounded-full font-extrabold" style={{ padding: '6px 12px', fontSize: 12, background: tint(BRAND), border: `1.5px solid ${edge(BRAND)}`, color: BRAND }}>
<MapPin size={13} /> Coimbatore
</span>
</div>
}
/>

View File

@@ -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 = () => (
<div className="flex items-center !p-1 bg-slate-100/80 !rounded-lg border border-slate-200/80 shadow-inner">
<button
onClick={() => 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'
}`}
>
<Route size={13} className={activeTab === 'map' ? 'text-[#581c87]' : 'text-slate-400'} /> Map
</button>
<button
onClick={() => 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'
}`}
>
<ShoppingBag size={13} className={activeTab === 'orders' ? 'text-[#581c87]' : 'text-slate-400'} /> Orders
</button>
<button
onClick={() => 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'
}`}
>
<Truck size={13} className={activeTab === 'deliveries' ? 'text-[#581c87]' : 'text-slate-400'} /> Deliveries
</button>
</div>
);
return (
<div className="flex flex-col h-[calc(100vh-80px)] w-full">
{/* Content Area - Header removed as requested, tabs passed down */}
<div className={`flex-1 min-h-0 bg-[#f8fafc] ${activeTab !== 'map' ? 'overflow-y-auto w-full p-4 md:p-8 relative' : 'relative'}`}>
{activeTab === 'map' && <DispatchView locationid={locationid} tenantId={tenantId} headerTabs={renderTabs()} />}
{activeTab === 'orders' && <OrdersView locationid={locationid} tenantId={tenantId} headerTabs={renderTabs()} />}
{activeTab === 'deliveries' && <DeliveriesView locationid={locationid} tenantId={tenantId} headerTabs={renderTabs()} />}
</div>
</div>
);
}

View File

@@ -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<string>(ymd(today));
const [viewMode, setViewMode] = useState<ViewMode>('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 }
<div className="dispatch-container embedded">
{/* ── Header ── */}
<div id="hdr">
<div className="logo">
<div className="logo-badge">D</div>
<div className="logo-name">Dispatch</div>
<div className="logo-city-wrap">
<span className="logo-city" style={{ cursor: 'default' }}>
<MapPin size={13} />
<span className="logo-city-text">Coimbatore</span>
</span>
<div className="logo flex items-center gap-5">
<div className="flex items-center gap-3">
<div className="logo-badge shadow-sm">D</div>
<div className="logo-name tracking-tight">Dispatch</div>
<div className="logo-city-wrap ml-1">
<span className="logo-city" style={{ cursor: 'default' }}>
<MapPin size={13} className="text-purple-500" />
<span className="logo-city-text font-semibold text-slate-700">Coimbatore</span>
</span>
</div>
</div>
{headerTabs}
</div>
<div className="hdr-stats">
@@ -430,7 +422,7 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID }
) : (
<>
<div className="ph">
{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})
</div>
{groups.map((g) => (
<React.Fragment key={g.id}>

View File

@@ -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<string>(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 (
<div className="animate-in fade-in duration-300">
<GradientHeader
title="Orders"
subtitle="Live order board across the lifecycle — created, pending, processing, delivered, and cancelled."
title="Orders Board"
subtitle="End-to-end lifecycle tracking for current operational period."
status={
ordersQ.isLoading
? <LiveStatus state="loading" label="Loading live orders…" />
@@ -262,9 +264,12 @@ export default function OrdersView({ searchQuery = '', locationid, tenantId = FI
: <LiveStatus state="live" label={`Live · ${total.toLocaleString('en-IN')} orders in range`} />
}
right={
<span className="inline-flex items-center gap-1.5 rounded-full font-extrabold" style={{ padding: '6px 12px', fontSize: 12, background: tint(BRAND), border: `1.5px solid ${edge(BRAND)}`, color: BRAND }}>
<MapPin size={13} /> Coimbatore
</span>
<div className="flex items-center gap-3">
{headerTabs}
<span className="inline-flex items-center gap-1.5 rounded-full font-extrabold" style={{ padding: '6px 12px', fontSize: 12, background: tint(BRAND), border: `1.5px solid ${edge(BRAND)}`, color: BRAND }}>
<MapPin size={13} /> Coimbatore
</span>
</div>
}
/>

View File

@@ -120,6 +120,10 @@ function Row({
);
}
export interface SettingsViewProps {
tenantId?: number;
}
export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsViewProps) {
const [activeTab, setActiveTab] = useState<TabKey>('profile');

View File

@@ -344,8 +344,9 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
)}
{/* ── Subheader Navigation Bar ── */}
<div className={`flex items-center ${onBack ? 'justify-between' : 'justify-end'}`}>
{onBack && (
{/* ── Subheader Navigation Bar ── */}
{onBack && (
<div className="flex items-center justify-between mb-4">
<button
onClick={onBack}
className="flex items-center gap-xs text-xs font-bold text-[#581c87] hover:text-[#4c1d95] bg-purple-50 hover:bg-purple-100/80 px-xl py-2 rounded-lg transition-all shadow-sm border border-purple-100 cursor-pointer"
@@ -353,13 +354,8 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
<ArrowLeft size={14} />
<span>Back to Registry</span>
</button>
)}
<div className="flex items-center gap-xs">
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
<span className="text-[10px] font-bold tracking-widest text-emerald-600 uppercase">System Sync Active</span>
</div>
</div>
)}
{/* ── Immersive Analytics Banner — hidden on the standalone Inventory & Customers pages ── */}
{showHero && (
@@ -378,6 +374,12 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
<div className="absolute top-0 right-0 w-72 h-72 bg-purple-500/10 rounded-full blur-3xl -mr-20 -mt-20 pointer-events-none z-0" />
<div className="absolute bottom-0 left-0 w-56 h-56 bg-slate-500/5 rounded-full blur-2xl -ml-20 -mb-20 pointer-events-none z-0" />
{/* System Sync Active Indicator */}
<div className="absolute top-4 right-5 sm:top-6 sm:right-8 z-20 flex items-center gap-1.5 bg-slate-950/40 backdrop-blur-md px-3 py-1.5 rounded-full border border-white/5 shadow-sm">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
<span className="text-[9px] font-bold tracking-widest text-emerald-400 uppercase">System Sync Active</span>
</div>
<div className="relative flex flex-col md:flex-row md:items-center justify-between gap-6 z-10">
<div className="space-y-2">
<div className="inline-flex items-center gap-2 px-2.5 py-1 rounded-full bg-purple-500/20 border border-purple-400/30 text-purple-200 text-[10px] font-bold uppercase tracking-widest">
@@ -888,124 +890,139 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
return (
<div className="animate-in fade-in duration-300">
{/* Page heading */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
<div>
<h2 className="text-[22px] font-semibold tracking-tight text-[#0f172a]">Customers</h2>
<p className="text-[13px] text-zinc-500 mt-1">
{customersList.length} {customersList.length === 1 ? 'person orders' : 'people order'} from{' '}
<span className="font-medium text-zinc-700">{store.name}</span>
<span className="text-zinc-300"> · </span>
{withPhone} with phone<span className="text-zinc-300"> · </span>{withEmail} with email
</p>
</div>
<div className="relative w-full sm:w-64 shrink-0">
<Search size={15} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-zinc-400" />
<input
type="text"
placeholder="Search customers"
value={customerSearch}
onChange={(e) => 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) */}
<div className="relative overflow-hidden rounded-2xl p-4 md:p-5 border mb-5 animate-in fade-in duration-300 shadow-[0_8px_24px_rgba(15,23,42,0.08)]" style={{ borderColor: '#e2e8f0', background: `linear-gradient(135deg, #66258208 0%, #9255AB08 100%)` }}>
{/* Background glowing circles */}
<div className="absolute top-0 right-0 w-80 h-80 bg-[#662582]/5 rounded-full blur-3xl -mr-20 -mt-20 pointer-events-none z-0" />
<div className="absolute bottom-0 left-0 w-64 h-64 bg-[#9255AB]/5 rounded-full blur-2xl -ml-20 -mb-20 pointer-events-none z-0" />
<div className="relative flex flex-col md:flex-row md:items-center justify-between gap-4 z-10">
<div className="space-y-1">
<div className="flex items-center gap-3 mb-1">
<div className="inline-flex items-center gap-2 px-2.5 py-0.5 rounded-full bg-[#662582]/10 border border-[#662582]/20 text-[#662582] text-[9px] font-bold uppercase tracking-widest">
Store Audience Registry
</div>
<div className="flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
<span className="text-[9px] font-bold tracking-widest text-emerald-600 uppercase">System Sync Active</span>
</div>
</div>
<h2 className="font-sans font-extrabold text-2xl md:text-[28px] tracking-tight text-[#0f172a]">
Customer CRM
</h2>
<div className="flex flex-wrap items-center gap-2 text-[12px] text-zinc-500 mt-1 font-medium">
<span>
{customersList.length} {customersList.length === 1 ? 'customer orders' : 'customers order'} from <strong className="text-[#0f172a] font-bold">{store.name}</strong>
</span>
<span className="text-zinc-300 hidden sm:inline">·</span>
<span className="flex items-center gap-1.5"><Phone size={11} className="text-zinc-400" /> {withPhone} with phone</span>
<span className="text-zinc-300 hidden sm:inline">·</span>
<span className="flex items-center gap-1.5"><Mail size={11} className="text-zinc-400" /> {withEmail} with email</span>
</div>
</div>
<div className="relative w-full md:w-64 shrink-0">
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-[#662582]" />
<input
type="text"
placeholder="Search customers..."
value={customerSearch}
onChange={(e) => 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)]"
/>
</div>
</div>
</div>
{/* Profile cards */}
{customersList.length === 0 ? (
<div className="bg-white border border-[#e8e9ee] rounded-3xl py-20 flex flex-col items-center gap-3 text-center px-6">
<span className="flex items-center justify-center w-14 h-14 rounded-full bg-[#f4f0fb] text-[#6d28d9]"><Users size={22} /></span>
<div className="bg-white/60 backdrop-blur-md border border-[#e2e8f0] rounded-3xl py-24 flex flex-col items-center gap-4 text-center px-6 shadow-sm">
<span className="flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-br from-[#f3e8ff] to-[#e9d5ff] text-[#6d28d9] shadow-sm"><Users size={28} /></span>
<div>
<p className="text-[15px] font-semibold text-[#0f172a]">No customers yet</p>
<p className="text-[13px] text-zinc-500 mt-1 max-w-xs leading-relaxed">
{customerSearch ? 'Nothing matches your search.' : 'Customers will appear here once they place their first order.'}
<p className="text-[18px] font-bold text-[#0f172a]">No customers yet</p>
<p className="text-[14px] text-zinc-500 mt-1 max-w-sm leading-relaxed">
{customerSearch ? 'Nothing matches your search criteria.' : 'Customers will appear here once they place their first order.'}
</p>
</div>
{customerSearch && (
<button onClick={() => setCustomerSearch('')} className="mt-1 text-[13px] font-medium text-[#581c87] hover:underline cursor-pointer">Clear search</button>
<button onClick={() => 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</button>
)}
</div>
) : (
<div className="bg-white border border-[#e8e9ee] rounded-2xl shadow-[0_1px_3px_rgba(16,24,40,0.04)] overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse" style={{ minWidth: 860 }}>
<thead>
<tr className="bg-[#fafbfc] border-b border-[#eceef2] text-[11px] font-semibold uppercase tracking-wider text-zinc-400">
<th className="px-5 py-3.5 font-semibold">Customer</th>
<th className="px-5 py-3.5 font-semibold">Phone</th>
<th className="px-5 py-3.5 font-semibold">Email</th>
<th className="px-5 py-3.5 font-semibold">Delivery address</th>
<th className="px-5 py-3.5 font-semibold text-right">Profile</th>
</tr>
</thead>
<tbody className="divide-y divide-[#f1f2f5]">
{customersList.map((c: any, idx: number) => {
const tone = toneFor(c.name || `c${idx}`);
const locality = localityOf(c.address);
return (
<tr key={c.id ?? idx} className="group hover:bg-[#fbfaff] transition-colors">
{/* Customer */}
<td className="px-5 py-3.5">
<div className="flex items-center gap-3">
<span
className="w-9 h-9 rounded-xl flex items-center justify-center font-bold text-[12px] shrink-0 ring-1 ring-black/[0.04]"
style={{ background: tone.soft, color: tone.fg }}
>
{initialsOf(c.name)}
</span>
<div className="min-w-0">
<p className="font-semibold text-[14px] text-[#0f172a] truncate leading-tight" title={c.name}>{c.name}</p>
{locality && (
<p className="text-[12px] text-zinc-400 mt-0.5 inline-flex items-center gap-1 truncate max-w-[180px]">
<MapPin size={11} className="shrink-0" /> {locality}
</p>
)}
</div>
</div>
</td>
{/* Phone */}
<td className="px-5 py-3.5">
<span className="text-[13px] text-zinc-700 tabular-nums">{c.phone}</span>
</td>
{/* Email */}
<td className="px-5 py-3.5">
{c.email
? <span className="text-[13px] text-zinc-600 truncate inline-block max-w-[220px] align-middle" title={c.email}>{c.email}</span>
: <span className="text-[13px] text-zinc-300"></span>}
</td>
{/* Address */}
<td className="px-5 py-3.5">
<span className="text-[13px] text-zinc-500 truncate inline-block max-w-[280px] align-middle" title={c.address}>{c.address}</span>
</td>
{/* Action */}
<td className="px-5 py-3.5 text-right whitespace-nowrap">
<div className="inline-flex items-center justify-end gap-2">
{canManage && (
<button
onClick={() => showToast(`Promo code sent to ${c.phone}.`, 'success')}
title="Send promo SMS"
className="w-8 h-8 inline-flex items-center justify-center rounded-lg border border-[#e6e8ee] text-zinc-500 hover:text-[#581c87] hover:border-[#d6bcf0] hover:bg-[#faf5ff] transition-colors cursor-pointer"
>
<Send size={14} />
</button>
)}
<button
onClick={() => setSelectedCustomer(c)}
className="inline-flex items-center gap-1.5 rounded-lg border border-[#e6e8ee] px-3 py-1.5 text-[12.5px] font-semibold text-zinc-700 hover:bg-[#581c87] hover:border-[#581c87] hover:text-white transition-colors cursor-pointer"
>
View <ChevronRight size={14} />
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
<div className="px-5 py-3 border-t border-[#eceef2] bg-[#fafbfc] text-[12px] text-zinc-400">
Showing {customersList.length} {customersList.length === 1 ? 'customer' : 'customers'}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 pb-8">
{customersList.map((c: any, idx: number) => {
const tone = toneFor(c.name || `c${idx}`);
const locality = localityOf(c.address);
return (
<div key={c.id ?? idx} className="group relative bg-white/70 backdrop-blur-md border border-[#e2e8f0] rounded-2xl p-5 hover:shadow-[0_12px_40px_rgba(88,28,135,0.08)] hover:border-[#581c87]/30 transition-all duration-300 flex flex-col">
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-3 min-w-0">
<span
className="w-12 h-12 rounded-2xl flex items-center justify-center font-bold text-sm shrink-0 shadow-sm transition-transform group-hover:scale-105 border border-white/50"
style={{ background: tone.soft, color: tone.fg }}
>
{initialsOf(c.name)}
</span>
<div className="min-w-0">
<h3 className="font-extrabold text-[#0f172a] text-[15px] leading-tight group-hover:text-[#581c87] transition-colors truncate" title={c.name}>{c.name}</h3>
{locality && (
<p className="text-[11px] text-zinc-500 mt-0.5 flex items-center gap-1 font-medium truncate" title={locality}>
<MapPin size={10} className="shrink-0 text-zinc-400" /> {locality}
</p>
)}
</div>
</div>
{c.ordersCount > 5 && (
<span className="shrink-0 ml-2 px-2 py-1 bg-gradient-to-r from-amber-100 to-amber-50 text-amber-700 text-[9px] font-black uppercase tracking-widest rounded-lg border border-amber-200 flex items-center gap-1 shadow-sm">
<Award size={10} /> Loyal
</span>
)}
</div>
<div className="grid grid-cols-2 gap-3 mb-5">
<div className="bg-gradient-to-br from-[#f8fafc] to-white rounded-xl p-3 border border-[#f1f5f9] shadow-[inset_0_2px_4px_rgba(0,0,0,0.01)] group-hover:border-[#581c87]/10 transition-colors">
<span className="text-[9px] font-bold text-zinc-400 uppercase tracking-widest block">Dispatches</span>
<span className="font-black text-[16px] text-[#0f172a] mt-0.5 block">{c.ordersCount}</span>
</div>
<div className="bg-gradient-to-br from-[#f8fafc] to-white rounded-xl p-3 border border-[#f1f5f9] shadow-[inset_0_2px_4px_rgba(0,0,0,0.01)] group-hover:border-emerald-500/10 transition-colors">
<span className="text-[9px] font-bold text-zinc-400 uppercase tracking-widest block">Gross Spend</span>
<span className="font-black text-[16px] text-emerald-600 mt-0.5 block">{c.totalSpent}</span>
</div>
</div>
<div className="space-y-2.5 mb-6 flex-1">
<div className="flex items-center gap-2.5 text-[12px] text-zinc-600 font-medium">
<div className="w-6 h-6 rounded-lg bg-zinc-50 flex items-center justify-center shrink-0 border border-zinc-100"><Phone size={12} className="text-zinc-400" /></div>
<span className="tabular-nums">{c.phone}</span>
</div>
{c.email && c.email !== '—' && (
<div className="flex items-center gap-2.5 text-[12px] text-zinc-600 font-medium">
<div className="w-6 h-6 rounded-lg bg-zinc-50 flex items-center justify-center shrink-0 border border-zinc-100"><Mail size={12} className="text-zinc-400" /></div>
<span className="truncate" title={c.email}>{c.email}</span>
</div>
)}
</div>
<div className="pt-4 border-t border-[#f1f5f9] flex items-center justify-between gap-3 mt-auto">
{canManage && (
<button
onClick={(e) => { e.stopPropagation(); showToast(`Promo code sent to ${c.phone}.`, 'success'); }}
className="w-10 h-10 flex items-center justify-center rounded-xl bg-purple-50 border border-purple-100 text-[#581c87] hover:bg-[#581c87] hover:border-[#581c87] hover:text-white transition-all cursor-pointer shrink-0 shadow-sm"
title="Send promo SMS"
>
<Send size={15} />
</button>
)}
<button
onClick={() => setSelectedCustomer(c)}
className="flex-1 h-10 flex items-center justify-center gap-2 rounded-xl bg-white border border-[#e2e8f0] text-[12px] font-bold text-zinc-700 hover:border-[#581c87] hover:bg-purple-50/30 hover:text-[#581c87] transition-all cursor-pointer shadow-sm group/btn"
>
View Profile <ChevronRight size={14} className="group-hover/btn:translate-x-1 transition-transform" />
</button>
</div>
</div>
);
})}
</div>
)}
</div>

View File

@@ -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 <OrdersView locationid={resolvedLocationId || undefined} tenantId={tenantId} />;
if (activeSection === 'deliveries') return <DeliveriesView locationid={resolvedLocationId || undefined} tenantId={tenantId} />;
if (activeSection === 'dispatch') return <DispatchView locationid={resolvedLocationId || undefined} tenantId={tenantId} />;
if (activeSection === 'dispatch') return <DispatchHubView locationid={resolvedLocationId || undefined} tenantId={tenantId} />;
if (activeSection === 'reports') return <DeliveryReportsView tenantId={tenantId} />;
// 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

View File

@@ -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';