feat: relocate orders and deliveries to store console & polish store cover images

This commit is contained in:
Suriya
2026-06-03 18:20:43 +05:30
commit 6eaeb5c4a7
32 changed files with 13430 additions and 0 deletions

View File

@@ -0,0 +1,286 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import React from 'react';
import {
ShoppingBag,
PackageCheck,
Wallet,
TrendingUp,
Store,
MapPin,
Phone,
Sparkles,
AlertTriangle,
} from 'lucide-react';
import { useOrderSummary, useTenantInfo, useTenantLocations, useInvoiceInsight } from '../services/queries';
import { DEFAULT_TENANT_ID, DEFAULT_CONFIG_ID } from '../services/api';
import { useFiestaLocationSummary } from '../services/fiestaQueries';
import { FIESTA_TENANT_ID } from '../services/fiestaApi';
interface DashboardViewProps {
searchQuery: string;
isCoimbatoreView: boolean;
}
const ymd = (d: Date) =>
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
const str = (v: unknown): string => (v == null ? '' : String(v));
export default function DashboardView({ searchQuery }: DashboardViewProps) {
// Live data — month-to-date order summary + tenant identity + store locations.
const today = new Date();
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
const fromdate = ymd(monthStart);
const todate = ymd(today);
const summaryQ = useOrderSummary(DEFAULT_TENANT_ID, fromdate, todate, DEFAULT_CONFIG_ID);
const tenantQ = useTenantInfo(DEFAULT_TENANT_ID);
const locationsQ = useTenantLocations(DEFAULT_TENANT_ID);
const insightQ = useInvoiceInsight(DEFAULT_TENANT_ID);
const s = summaryQ.data;
const tenantName = str(tenantQ.data?.tenantname) || s?.tenantname || `Tenant ${DEFAULT_TENANT_ID}`;
// Profit comes from the live invoice/financial insight. When the tenant has no
// invoice records we show "—" rather than a misleading ₹0.
const insight = insightQ.data;
const money = (v: number | null) => (v == null ? '—' : `${Math.round(v).toLocaleString('en-IN')}`);
const todaysProfit = insight ? insight.profit : null;
const monthlyProfit = insight ? insight.profit : null;
const locSummaryQ = useFiestaLocationSummary(FIESTA_TENANT_ID);
const summaries = locSummaryQ.data ?? [];
const locations = (locationsQ.data ?? []).filter((loc) => {
if (!searchQuery) return true;
const q = searchQuery.toLowerCase();
return (
str(loc.locationname).toLowerCase().includes(q) ||
str(loc.city).toLowerCase().includes(q) ||
str(loc.suburb).toLowerCase().includes(q)
);
});
// KPI cards — orders from getordersummary, profit from getinvoiceinsight.
const totalStoresCount = locations.length;
const activeStoresCount = locations.filter(l => str(l.status).toLowerCase() === 'active').length;
const inactiveStoresCount = totalStoresCount - activeStoresCount;
const activePct = totalStoresCount > 0 ? Math.round((activeStoresCount / totalStoresCount) * 100) : 0;
const circumference = 251.2;
const dashOffset = circumference - (circumference * activePct) / 100;
const kpis = [
{ title: 'ACTIVE OUTLETS', display: `${activeStoresCount} / ${totalStoresCount}`, icon: Store, chip: 'bg-purple-50 text-[#581c87]', loading: locationsQ.isLoading },
{ title: 'REGION FULFILLMENT', display: '98.2%', icon: Sparkles, chip: 'bg-emerald-50 text-emerald-600', loading: false },
{ title: "TODAY'S PROFIT", display: money(todaysProfit), icon: Wallet, chip: 'bg-sky-50 text-sky-600', loading: insightQ.isLoading },
{ title: 'MONTHLY PROFIT', display: money(monthlyProfit), icon: TrendingUp, chip: 'bg-emerald-50 text-emerald-600', loading: insightQ.isLoading },
];
const statusRows = [
{ label: 'Active Outlets', value: activeStoresCount, dot: 'bg-emerald-500' },
{ label: 'Inactive / Maintenance', value: inactiveStoresCount, dot: 'bg-zinc-400' },
];
const loading = summaryQ.isLoading;
const errored = summaryQ.isError;
return (
<div className="space-y-lg animate-in fade-in duration-500 relative">
{/* Scope banner */}
<div className="bg-[#faf5ff] border border-purple-100 rounded-xl p-md flex items-center justify-between shadow-[0_1px_3px_rgba(0,0,0,0.05)]">
<div className="flex items-center gap-sm">
<Sparkles size={16} className="text-[#581c87]" />
<span className="font-sans text-xs text-zinc-700 font-medium">
Live operations data for <strong>{tenantName}</strong> · {fromdate} {todate}
</span>
</div>
</div>
{/* Header */}
<div className="flex justify-between items-end">
<div>
<h1 className="font-sans font-bold text-3xl tracking-tight text-[#0f172a]">Executive Command Center</h1>
<div className="flex items-center gap-2 mt-1">
<p className="text-zinc-500 font-sans text-sm">Month-to-date order operations, pulled live from the API.</p>
{loading ? (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-zinc-400 uppercase tracking-wide">
<span className="w-1.5 h-1.5 rounded-full bg-zinc-300 animate-pulse" /> Loading
</span>
) : errored ? (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-rose-600 uppercase tracking-wide" title="Restart the dev server so the /hasura proxy is active.">
<span className="w-1.5 h-1.5 rounded-full bg-rose-500" /> Live data unavailable
</span>
) : (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-emerald-600 uppercase tracking-wide">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" /> Live · {tenantName}
</span>
)}
</div>
</div>
</div>
{/* Error hint */}
{errored && (
<div className="bg-rose-50 border border-rose-200 rounded-xl p-md flex items-start gap-sm text-xs text-rose-800">
<AlertTriangle size={16} className="shrink-0 mt-0.5 text-rose-500" />
<div>
<p className="font-semibold">Couldn't reach the live API.</p>
<p className="mt-0.5 text-rose-700">
The <code>/hasura</code> dev proxy loads at server start — stop and re-run <code>npm run dev</code> so the
secret and proxy are active.
</p>
</div>
</div>
)}
{/* KPI cards — all live from getordersummary */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-gutter">
{kpis.map((kpi) => {
const Icon = kpi.icon;
return (
<div
key={kpi.title}
className="group relative flex flex-col bg-white border border-[#eceef2] rounded-xl p-3 shadow-[0_1px_2px_rgba(16,24,40,0.04)] transition-all duration-200 hover:-translate-y-0.5 hover:border-purple-300 hover:shadow-[0_8px_22px_rgba(16,24,40,0.08)]"
>
<div className={`h-7 w-7 rounded-lg flex items-center justify-center ${kpi.chip}`}>
<Icon size={14} />
</div>
<p className="text-[10px] font-semibold text-zinc-400 tracking-wider uppercase font-sans mt-3">
{kpi.title}
</p>
<p className="font-sans font-bold text-2xl leading-tight text-[#0f172a] tracking-tight mt-0.5">
{kpi.loading ? <span className="text-zinc-300">…</span> : kpi.display}
</p>
</div>
);
})}
</div>
{/* Order status + store locations */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-gutter">
{/* Store Node Status donut (live) */}
<div className="bg-white border border-[#e2e8f0] rounded-xl p-md flex flex-col shadow-[0_1px_3px_rgba(0,0,0,0.05)]">
<div>
<h3 className="font-sans font-bold text-base text-[#0f172a]">Store Outlet Status</h3>
<p className="text-zinc-500 text-xs font-sans mt-0.5">Active share of all registered store nodes.</p>
</div>
<div className="my-md flex justify-center items-center">
<div className="relative w-40 h-40 flex items-center justify-center">
<svg className="w-full h-full transform -rotate-90" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="transparent" stroke="#eceef0" strokeWidth="8" />
<circle
cx="50"
cy="50"
r="40"
fill="transparent"
stroke="#10b981"
strokeWidth="8"
strokeDasharray={circumference}
strokeDashoffset={dashOffset}
strokeLinecap="round"
className="transition-all duration-700"
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="font-sans font-bold text-3xl text-[#0f172a] tracking-tight">{activePct}%</span>
<span className="text-[10px] text-emerald-600 uppercase tracking-widest font-semibold mt-1">Active</span>
</div>
</div>
</div>
<div className="divide-y divide-[#f1f5f9] text-xs">
{statusRows.map((r) => (
<div key={r.label} className="flex justify-between items-center py-2">
<span className="flex items-center gap-1.5 text-zinc-500">
<span className={`w-2.5 h-2.5 rounded-full ${r.dot}`} />
{r.label}
</span>
<span className="font-mono font-bold text-zinc-700">{r.value.toLocaleString('en-IN')}</span>
</div>
))}
<div className="flex justify-between items-center py-2">
<span className="text-zinc-500 font-semibold">Total Nodes</span>
<span className="font-mono font-bold text-[#581c87]">{totalStoresCount.toLocaleString('en-IN')}</span>
</div>
</div>
</div>
{/* Store locations (live) */}
<div className="lg:col-span-2 bg-white border border-[#e2e8f0] rounded-xl p-md shadow-[0_1px_3px_rgba(0,0,0,0.05)]">
<div className="flex justify-between items-center mb-md pb-xs border-b border-[#f1f5f9]">
<h3 className="font-sans font-bold text-base text-[#0f172a] flex items-center gap-2">
<Store size={16} className="text-[#581c87]" /> Store Locations
</h3>
<span className="text-[10px] text-[#581c87] uppercase font-bold bg-purple-50 px-2 py-0.5 rounded tracking-wide border border-purple-100">
{locationsQ.isLoading ? 'Loading' : `${locations.length} Outlet${locations.length === 1 ? '' : 's'}`}
</span>
</div>
{locationsQ.isLoading ? (
<div className="text-center py-xl text-zinc-400 text-xs">Loading store locations…</div>
) : locations.length === 0 ? (
<div className="text-center py-xl text-zinc-400 text-xs">No store locations found for this tenant.</div>
) : (
<div className="space-y-sm max-h-80 overflow-y-auto">
{locations.map((loc, i) => {
const sum = summaries.find((s) => s.locationid === Number(loc.locationid));
const deliveries = sum?.delivered ?? 0;
const orders = Math.max(sum?.delivered ?? 0, sum?.total ?? 0);
return (
<div
key={str(loc.locationid) || i}
className="p-sm border border-[#e2e8f0] rounded-lg bg-[#f8fafc]/40 flex justify-between items-start gap-md hover:border-purple-200 transition-colors animate-in fade-in"
>
<div className="min-w-0">
<p className="font-sans font-semibold text-sm text-[#0f172a] truncate">{str(loc.locationname)}</p>
<p className="text-[11px] text-zinc-500 mt-0.5 flex items-center gap-1">
<MapPin size={11} className="shrink-0 text-zinc-400" />
<span className="truncate">{str(loc.address) || `${str(loc.suburb)}, ${str(loc.city)}`}</span>
</p>
{str(loc.contactno) && (
<p className="text-[11px] text-zinc-500 mt-0.5 flex items-center gap-1">
<Phone size={11} className="shrink-0 text-zinc-400" />
{str(loc.contactno)}
</p>
)}
{/* Node-specific Orders and Dispatches */}
<div className="flex items-center gap-3 mt-2.5">
<span className="text-[10px] bg-purple-50 text-[#581c87] font-semibold px-2 py-0.5 rounded border border-purple-100/50">
{orders} Orders
</span>
<span className="text-[10px] bg-emerald-50 text-emerald-700 font-semibold px-2 py-0.5 rounded border border-emerald-100/50">
{deliveries} Dispatched
</span>
{orders > 0 && (
<span className="text-[10px] text-zinc-400 font-medium">
{Math.round((deliveries / orders) * 100)}% Fulfilled
</span>
)}
</div>
</div>
<span
className={`shrink-0 px-1.5 py-0.5 rounded text-[9px] font-bold uppercase ${
str(loc.status).toLowerCase() === 'active'
? 'text-emerald-600 bg-emerald-50 border border-emerald-100'
: 'text-zinc-500 bg-zinc-100'
}`}
>
{str(loc.status) || ''}
</span>
</div>
);
})}
</div>
)}
</div>
</div>
</div>
);
}

174
src/components/Header.tsx Normal file
View File

@@ -0,0 +1,174 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useState, useRef, useEffect } from 'react';
import { Menu, Plus, HelpCircle, LogOut } from 'lucide-react';
import { MainSection } from '../types';
interface HeaderProps {
currentSection: MainSection;
setCurrentSection: (section: MainSection) => void;
isCoimbatoreView: boolean;
onToggleSidebar: () => void;
isSidebarOpen: boolean;
onNewReportClick: () => void;
onHelpClick: () => void;
onLogoutClick: () => void;
}
export default function Header({
currentSection,
setCurrentSection,
isCoimbatoreView,
onToggleSidebar,
isSidebarOpen,
onNewReportClick,
onHelpClick,
onLogoutClick
}: HeaderProps) {
const [showProfileDropdown, setShowProfileDropdown] = useState(false);
const profileRef = useRef<HTMLDivElement>(null);
// Close the profile dropdown on any click/tap outside of it.
useEffect(() => {
if (!showProfileDropdown) return;
const handlePointerDown = (e: MouseEvent | TouchEvent) => {
if (profileRef.current && !profileRef.current.contains(e.target as Node)) {
setShowProfileDropdown(false);
}
};
document.addEventListener('mousedown', handlePointerDown);
document.addEventListener('touchstart', handlePointerDown);
return () => {
document.removeEventListener('mousedown', handlePointerDown);
document.removeEventListener('touchstart', handlePointerDown);
};
}, [showProfileDropdown]);
const profile = {
name: 'Suresh Kumar',
role: 'Operations Director',
email: 'suresh.k@nearledaily.com',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=150&q=80'
};
return (
<header className="bg-[#581c87] border-b border-[#4c1d95] flex justify-between items-center w-full px-container-margin py-md fixed top-0 right-0 left-0 z-50 h-20 text-white shadow-sm">
{/* Brand & Desktop Navigation Tabs */}
<div className="flex items-center gap-md md:pl-0 pl-1">
{/* Brand Logo — full wordmark when sidebar open, icon only when collapsed */}
<span className="select-none flex items-center shrink-0">
<img
src={isSidebarOpen ? '/logo.png' : '/favicon.png'}
alt="nearledaily logo"
className="h-9 w-auto object-contain"
/>
</span>
{/* Sidebar toggle (Burger Menu) */}
<button
onClick={onToggleSidebar}
title="Toggle sidebar"
className="p-2 rounded-full hover:bg-purple-800 transition-colors cursor-pointer text-white"
>
<Menu size={18} />
</button>
<nav className="hidden md:flex gap-lg items-center ml-2">
<button
onClick={() => setCurrentSection('dashboard')}
className={`font-sans text-sm font-semibold cursor-pointer pb-1 transition-all ${
currentSection === 'dashboard'
? 'text-white border-b-2 border-white'
: 'text-purple-200 hover:text-white'
}`}
>
Dashboard
</button>
<button
onClick={() => setCurrentSection('operations')}
className={`font-sans text-sm font-semibold cursor-pointer pb-1 transition-all ${
currentSection === 'operations'
? 'text-white border-b-2 border-white'
: 'text-purple-200 hover:text-white'
}`}
>
Operations
</button>
<button
onClick={() => setCurrentSection('reports')}
className={`font-sans text-sm font-semibold cursor-pointer pb-1 transition-all ${
currentSection === 'reports'
? 'text-white border-b-2 border-white'
: 'text-purple-200 hover:text-white'
}`}
>
Reports
</button>
</nav>
</div>
{/* Global Actions Bar */}
<div className="flex items-center gap-md">
{/* User profile with dropdown */}
<div className="relative" ref={profileRef}>
<button
onClick={() => setShowProfileDropdown(!showProfileDropdown)}
className="w-10 h-10 rounded-full overflow-hidden border border-purple-400 focus:ring-2 focus:ring-purple-300 outline-none cursor-pointer transition-transform duration-100 active:scale-95 flex items-center justify-center"
>
<img
src={profile.avatar}
alt="Executive Profile"
referrerPolicy="no-referrer"
className="w-full h-full object-cover"
/>
</button>
{showProfileDropdown && (
<div className="absolute right-0 mt-2 w-56 bg-white border border-[#e2e8f0] rounded-lg shadow-xl py-2 z-50 text-zinc-700 animate-in fade-in duration-200">
<div className="px-4 py-2 border-b border-[#f1f5f9] bg-[#f8fafc]">
<p className="font-bold text-xs text-[#0f172a]">{profile.name}</p>
<p className="text-[10px] text-zinc-400 font-medium">{profile.role}</p>
</div>
<div className="p-2 divide-y divide-[#f1f5f9]">
<div className="py-1">
<p className="px-2 text-[10px] text-zinc-400 font-semibold uppercase tracking-wider">Email</p>
<p className="px-2 py-0.5 text-xs text-purple-600 font-sans font-medium truncate">{profile.email}</p>
</div>
{/* Account actions (moved here from the sidebar) */}
<div className="py-1 pt-2 flex flex-col gap-0.5">
<button
onClick={() => { setShowProfileDropdown(false); onNewReportClick(); }}
className="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-xs font-semibold text-zinc-700 hover:bg-zinc-50 cursor-pointer transition-colors"
>
<Plus size={14} className="text-[#581c87]" />
New Report
</button>
<button
onClick={() => { setShowProfileDropdown(false); onHelpClick(); }}
className="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-xs font-semibold text-zinc-700 hover:bg-zinc-50 cursor-pointer transition-colors"
>
<HelpCircle size={14} className="text-zinc-400" />
Help Center
</button>
<button
onClick={() => { setShowProfileDropdown(false); onLogoutClick(); }}
className="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-xs font-semibold text-rose-600 hover:bg-rose-50 cursor-pointer transition-colors"
>
<LogOut size={14} className="text-rose-500" />
Log Out
</button>
</div>
</div>
</div>
)}
</div>
</div>
</header>
);
}

View File

@@ -0,0 +1,901 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useState, useEffect } from 'react';
import {
Layers,
Search,
Plus,
RefreshCw,
AlertTriangle,
TrendingUp,
Sparkles,
Check,
Package,
ChevronRight,
TrendingDown,
Trash2,
PackageCheck,
Zap,
Tag,
UploadCloud,
FileSpreadsheet,
Palette,
ShoppingBag,
Info,
X
} from 'lucide-react';
import { ProductMatrixItem, InventoryItem, ImportLog } from '../types';
import { initialImportLogs } from '../data';
import { useFiestaStockStatement, useFiestaTenantLocations } from '../services/fiestaQueries';
import { FIESTA_TENANT_ID, FIESTA_PRIMARY_LOCATION_ID, str as fstr } from '../services/fiestaApi';
import { stockRowToProduct, stockRowToInventory } from '../services/fiestaMappers';
interface InventoryViewProps {
searchQuery: string;
isCoimbatoreView: boolean;
}
export default function InventoryView({
searchQuery,
isCoimbatoreView
}: InventoryViewProps) {
// ── Live stock data (Fiesta) ─────────────────────────────────────────────
// The catalog grid and the hub-balance ledger are both derived from the live
// stock statement for the tenant's primary outlet. We seed local state from
// it once it loads so the existing add / CSV / replenish interactions keep
// mutating in-session without losing the live baseline.
const locationsQ = useFiestaTenantLocations(FIESTA_TENANT_ID);
const primaryLocation =
(locationsQ.data ?? []).find((l) => Number(l.locationid) === FIESTA_PRIMARY_LOCATION_ID) ||
(locationsQ.data ?? [])[0];
const locationId = primaryLocation ? Number(primaryLocation.locationid) : FIESTA_PRIMARY_LOCATION_ID;
const locationName = fstr(primaryLocation?.locationname) || 'Primary Outlet';
const stockQ = useFiestaStockStatement({
tenantid: FIESTA_TENANT_ID,
locationid: locationId,
keyword: '',
pageno: 1,
pagesize: 100,
});
const [products, setProducts] = useState<ProductMatrixItem[]>([]);
const [inventory, setInventory] = useState<InventoryItem[]>([]);
const [importLogs, setImportLogs] = useState<ImportLog[]>(initialImportLogs);
useEffect(() => {
if (stockQ.data) {
setProducts(stockQ.data.map(stockRowToProduct));
setInventory(stockQ.data.map((r) => stockRowToInventory(r, locationName)));
}
// locationName is derived from the same query chain; safe to depend on data.
}, [stockQ.data, locationName]);
const [activeTab, setActiveTab] = useState<'catalog' | 'import_branding'>('catalog');
const [selectedCategory, setSelectedCategory] = useState<string>('ALL');
const [showAddProductModal, setShowAddProductModal] = useState(false);
const [replenishmentList, setReplenishmentList] = useState<string[]>([]);
// CSV Textarea input
const [csvText, setCsvText] = useState(
"Name, SKU, Category, Price, InitialStock\nAmma Ghee Pure Butter, GHEE-AMMA-1L, Groceries / Oils, 640, 200\nBhavani Ponni Sona Rice, ST-SONA-25K, Staples / Rice, 1350, 150"
);
// Brand designs state
const [brandStyle, setBrandStyle] = useState({
themeName: 'Coimbatore Kaveri Org',
primaryColor: '#16a34a', // Emerald
secondaryColor: '#f59e0b', // Amber
bagLabel: 'Freshly Harvested from Tamil Soil',
isEcoVerified: true,
stickerPattern: 'radial'
});
// Form state for individual adding
const [newProduct, setNewProduct] = useState({
name: '',
sku: '',
category: 'Staples / Rice',
price: 150,
initialStock: 250,
image: 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&q=80&w=200'
});
// Categories derived from the live catalog (falls back to ALL only).
const categorySet = new Set<string>();
products.forEach((p) => categorySet.add(p.category));
const categories: string[] = ['ALL', ...Array.from(categorySet)];
// Handle SKU quantity change
const handleUpdateStock = (sku: string, delta: number) => {
setInventory(prev => prev.map(item => {
if (item.sku === sku) {
const newLevel = Math.max(0, item.stockLevel + delta);
const status = newLevel < 25 ? 'Critical' : newLevel < 120 ? 'Low Stock' : 'Optimal';
return { ...item, stockLevel: newLevel, status };
}
return item;
}));
};
// Trigger quick reorder recommendation
const handleReplenishSku = (sku: string) => {
if (replenishmentList.includes(sku)) return;
setReplenishmentList(prev => [...prev, sku]);
handleUpdateStock(sku, 500); // Add 500 units to stock
setTimeout(() => {
alert(`Auto-Replenish complete! 500 units ordered and allocated directly to corresponding hub for SKU ${sku}`);
}, 100);
};
// Filter criteria
const filteredProducts = products.filter(p => {
const matchesSearch = p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
p.sku.toLowerCase().includes(searchQuery.toLowerCase()) ||
p.category.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCat = selectedCategory === 'ALL' || p.category.startsWith(selectedCategory.split(' / ')[0]);
return matchesSearch && matchesCat;
});
const handleAddNewProduct = (e: React.FormEvent) => {
e.preventDefault();
if (!newProduct.name || !newProduct.sku) {
alert('Kindly supply correct product specifications and catalog SKU code.');
return;
}
const createdProd: ProductMatrixItem = {
id: String(products.length + 1),
name: newProduct.name,
sku: newProduct.sku,
unitsSold: 0,
revenue: 0,
stockStatus: 'Healthy',
trend: 'flat',
image: newProduct.image,
category: newProduct.category,
exposure: 'All Outlets',
verified: true
};
const createdInv: InventoryItem = {
sku: newProduct.sku,
name: newProduct.name,
warehouse: 'RS Puram Hub (CBE-01)',
stockLevel: newProduct.initialStock,
maxCapacity: 1000,
status: 'Optimal',
region: 'CBE-NORTH'
};
setProducts([createdProd, ...products]);
setInventory([createdInv, ...inventory]);
setShowAddProductModal(false);
alert(`Fresh product "${createdProd.name}" incorporated into Master Grocery Catalog and standard ledger!`);
setNewProduct({
name: '',
sku: '',
category: 'Staples / Rice',
price: 150,
initialStock: 250,
image: 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&q=80&w=200'
});
};
// Custom Raw CSV import
const handleCSVImport = () => {
const lines = csvText.split('\n').map(l => l.trim()).filter(l => l.length > 0 && !l.startsWith('Name'));
if (lines.length === 0) {
alert('CSV sequence contains no importable entries.');
return;
}
let parsedCount = 0;
const newProds: ProductMatrixItem[] = [];
const newInvs: InventoryItem[] = [];
lines.forEach(line => {
const parts = line.split(',').map(p => p.trim());
if (parts.length >= 2) {
const name = parts[0];
const sku = parts[1];
const category = parts[2] || 'Staples / Rice';
const price = Number(parts[3]) || 120;
const initialStock = Number(parts[4]) || 150;
if (!products.some(p => p.sku === sku)) {
newProds.push({
id: String(products.length + newProds.length + 1),
name,
sku,
unitsSold: 0,
revenue: 0,
stockStatus: 'Healthy',
trend: 'flat',
image: 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&q=80&w=200',
category,
exposure: 'All Outlets',
verified: true
});
newInvs.push({
sku,
name,
warehouse: 'RS Puram Hub (CBE-01)',
stockLevel: initialStock,
maxCapacity: 1000,
status: 'Optimal',
region: 'CBE-NORTH'
});
parsedCount++;
}
}
});
if (parsedCount > 0) {
setProducts(prev => [...newProds, ...prev]);
setInventory(prev => [...newInvs, ...prev]);
const logEntry: ImportLog = {
timestamp: new Date().toLocaleTimeString() + ' (IST)',
batchRef: `#IMP_CSV_${Math.floor(1000 + Math.random() * 9000)}`,
type: 'CSV Catalogue Import',
source: 'Console Upload',
result: `SUCCESS (Parsed ${parsedCount} rows)`,
status: 'SUCCESS'
};
setImportLogs([logEntry, ...importLogs]);
alert(`Synchronized ${parsedCount} regional products into Catalog database successfully!`);
} else {
alert('All the specified SKU codes are already active in the catalog ledger.');
}
};
// Preset import trigger
const handleImportPreset = (presetName: string, itemsList: Array<{name: string, sku: string, cat: string, price: number, stock: number, img: string}>) => {
let imported = 0;
const newProds: ProductMatrixItem[] = [];
const newInvs: InventoryItem[] = [];
itemsList.forEach((itm) => {
if (!products.some(p => p.sku === itm.sku)) {
newProds.push({
id: String(products.length + newProds.length + 20),
name: itm.name,
sku: itm.sku,
unitsSold: Math.floor(Math.random() * 45 + 15),
revenue: Math.floor(Math.random() * 20000 + 4000),
stockStatus: 'Healthy',
trend: 'up',
image: itm.img,
category: itm.cat,
exposure: 'All Outlets',
verified: true
});
newInvs.push({
sku: itm.sku,
name: itm.name,
warehouse: 'Peelamedu Sort Center',
stockLevel: itm.stock,
maxCapacity: 800,
status: 'Optimal',
region: 'CBE-EAST'
});
imported++;
}
});
if (imported > 0) {
setProducts(prev => [...newProds, ...prev]);
setInventory(prev => [...newInvs, ...prev]);
const logEntry: ImportLog = {
timestamp: new Date().toLocaleTimeString() + ' (IST)',
batchRef: `#IMP_PST_${Math.floor(1000 + Math.random() * 9000)}`,
type: `${presetName} Import`,
source: 'Corporate Cloud Feed',
result: `SUCCESS Onboarded (${imported} SKUs)`,
status: 'SUCCESS'
};
setImportLogs([logEntry, ...importLogs]);
alert(`Successfully mapped and onboarded ${imported} brand SKUs from "${presetName}"!`);
} else {
alert('All elements of this retail catalog preset are already assigned.');
}
};
// Nilgiris Presets
const nilgirisDairy = [
{ name: 'Ooty Hills Creamery Butter 500g', sku: 'DY-OOT-BTR', cat: 'Groceries / Oils', price: 340, stock: 210, img: 'https://images.unsplash.com/photo-1589985270826-4b7bb135bc9d?auto=format&fit=crop&q=80&w=200' },
{ name: 'Nilgiris Mountain Farm Cheese 250g', sku: 'DY-NIL-CHS', cat: 'Groceries / Oils', price: 460, stock: 120, img: 'https://images.unsplash.com/photo-1486887396153-fa416525c108?auto=format&fit=crop&q=80&w=200' },
{ name: 'Aavin Premium Ghee Tin 1L', sku: 'DY-AAV-GHEE', cat: 'Groceries / Oils', price: 680, stock: 180, img: 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&q=80&w=200' }
];
// Coimbatore Crops
const cbeHeritage = [
{ name: 'Bhavani Premium Boiled Rice 10kg', sku: 'ST-BHV-RICE', cat: 'Staples / Rice', price: 740, stock: 350, img: 'https://images.unsplash.com/photo-1586201375761-83865001e31c?auto=format&fit=crop&q=80&w=200' },
{ name: 'Pollachi Clean Gram Dhal 2kg', sku: 'ST-POL-DHAL', cat: 'Staples / Rice', price: 185, stock: 240, img: 'https://images.unsplash.com/photo-1596040033229-a9821ebd058d?auto=format&fit=crop&q=80&w=200' },
{ name: 'Pure Wood Pressed Gingelly Oil 1L', sku: 'ST-OIL-WOOD', cat: 'Groceries / Oils', price: 395, stock: 190, img: 'https://images.unsplash.com/photo-1474979266404-7eaacbcd87c5?auto=format&fit=crop&q=80&w=200' }
];
return (
<div className="space-y-lg animate-in fade-in duration-500">
{/* Dynamic Navigation Toolbar header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-md border-b border-[#e2e8f0] pb-md">
<div>
<h1 className="font-sans font-bold text-2xl tracking-tight text-[#0f172a] flex items-center gap-xs">
<Layers className="text-[#581c87]" size={24} />
Coimbatore Grocery Assortment & Catalogue Studio
</h1>
<p className="text-zinc-500 font-sans text-xs mt-1">
Build regional catalogues, update localized stock balances, parse batch imports, and style brand bag templates.
</p>
<div className="mt-1.5">
{stockQ.isLoading ? (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-zinc-400 uppercase tracking-wide">
<span className="w-1.5 h-1.5 rounded-full bg-zinc-300 animate-pulse" /> Loading live stock
</span>
) : stockQ.isError ? (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-rose-600 uppercase tracking-wide">
<span className="w-1.5 h-1.5 rounded-full bg-rose-500" /> Live data unavailable
</span>
) : (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-emerald-600 uppercase tracking-wide">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" /> Live · {locationName} · {products.length} SKUs
</span>
)}
</div>
</div>
<div className="flex items-center gap-sm">
<button
onClick={() => setActiveTab('catalog')}
className={`px-4 py-2 rounded-lg text-xs font-bold transition-all cursor-pointer ${
activeTab === 'catalog'
? 'bg-[#581c87] text-white shadow-sm'
: 'bg-white hover:bg-zinc-50 text-zinc-700 border border-[#e2e8f0]'
}`}
>
🌾 Catalog Grid & Ledger
</button>
<button
onClick={() => setActiveTab('import_branding')}
className={`px-4 py-2 rounded-lg text-xs font-bold transition-all cursor-pointer ${
activeTab === 'import_branding'
? 'bg-[#581c87] text-white shadow-sm'
: 'bg-white hover:bg-zinc-50 text-zinc-700 border border-[#e2e8f0]'
}`}
>
📥 Import & Brand Studio
</button>
</div>
</div>
{activeTab === 'catalog' ? (
<>
{/* Quick Category Tab Filter Row */}
<div className="flex flex-wrap gap-2 py-1 items-center justify-between">
<div className="flex flex-wrap gap-2">
{categories.map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-4 py-2 rounded-lg font-sans text-xs font-semibold tracking-wide transition-all border outline-none cursor-pointer ${
selectedCategory === cat
? 'bg-[#581c87] text-white border-[#581c87] shadow-sm'
: 'bg-white text-zinc-700 border-[#e2e8f0] hover:bg-zinc-50'
}`}
>
{cat === 'ALL' ? '🌾 All Catalog Items' : cat.replace('Groceries / ', '').replace('Staples / ', '').replace('Beverages / ', '').replace('Fresh Produce / ', '')}
</button>
))}
</div>
<button
onClick={() => 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"
>
<Plus size={14} />
Add Manual SKU
</button>
</div>
{/* Multi-Pane Layout: Left Catalog Grid, Right Stock balances */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-gutter text-xs font-sans">
{/* Left Grid: Grocery Catalogue Items Showcase */}
<div className="lg:col-span-2 space-y-md">
<div className="bg-[#f8fafc]/50 border border-[#e2e8f0] p-md rounded-xl">
<h3 className="font-sans font-bold text-sm text-[#0f172a] mb-xs">Active Assortment Items</h3>
<p className="text-zinc-500 font-normal mb-md leading-relaxed text-[11px]">Primary catalog schema synchronized on customer booking apps. Total: {filteredProducts.length} items</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-sm">
{filteredProducts.map((prod) => {
return (
<div key={prod.id} className="bg-white border border-[#e2e8f0] rounded-xl overflow-hidden p-md flex gap-md shadow-sm hover:shadow-md transition-shadow relative">
<div className="w-16 h-16 rounded-xl border border-zinc-100 shrink-0 overflow-hidden bg-zinc-50">
<img
src={prod.image}
alt={prod.name}
referrerPolicy="no-referrer"
className="w-full h-full object-cover"
/>
</div>
<div className="flex-1 space-y-1">
<div className="flex items-start justify-between">
<div>
<h4 className="font-bold text-zinc-900 leading-tight text-xs">{prod.name}</h4>
<span className="text-[10px] text-zinc-400 font-bold tracking-tight">{prod.sku}</span>
</div>
<span className="px-1.5 py-0.5 rounded text-[9px] font-bold uppercase bg-purple-50 text-purple-700">
{prod.category.split(' / ')[0]}
</span>
</div>
<div className="flex justify-between items-center pt-2">
<div>
<span className="text-[9px] text-zinc-400 block uppercase tracking-wider font-bold">Sold (Units)</span>
<span className="font-bold text-zinc-800 font-mono">{prod.unitsSold.toLocaleString()}</span>
</div>
<div className="text-right">
<span className="text-[9px] text-zinc-400 block uppercase tracking-wider font-bold">Total revenue</span>
<span className="font-bold text-emerald-600 font-mono">{prod.revenue.toLocaleString()}</span>
</div>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
{/* Right Pane: Stock level adjustment ledgers */}
<div className="space-y-md">
<div className="bg-white border border-[#e2e8f0] rounded-xl p-md shadow-sm space-y-md">
<div>
<h3 className="font-sans font-bold text-sm text-[#0f172a]">Hub Balances Ledger</h3>
<p className="text-zinc-500 font-normal leading-relaxed text-[11px] mt-0.5">Physical checkout balances across localized Coimbatore warehouse locations.</p>
</div>
<div className="divide-y divide-[#f1f5f9] select-none">
{inventory.map((item, idx) => {
const percentage = (item.stockLevel / item.maxCapacity) * 100;
return (
<div key={idx} className="py-md space-y-xs">
<div className="flex justify-between items-start">
<div>
<p className="font-bold text-[#0f172a]">{item.name}</p>
<p className="text-[10px] text-zinc-400 mt-1 font-medium">{item.warehouse}</p>
<div className="flex gap-px pt-1 items-center">
<span className="bg-[#f1f5f9] px-1 py-0.5 rounded text-[8px] font-bold text-zinc-500 font-mono tracking-tight mr-1">{item.region}</span>
<span className={`px-1.5 py-0.5 rounded text-[8px] font-bold tracking-wide uppercase ${
item.status === 'Critical' ? 'bg-rose-50 text-rose-600 border border-rose-100 animate-pulse' : item.status === 'Low Stock' ? 'bg-amber-50 text-amber-600' : 'bg-emerald-50 text-emerald-600'
}`}>
{item.status}
</span>
</div>
</div>
<div className="text-right space-y-1">
<span className="font-mono font-bold text-[#0f172a] block">{item.stockLevel.toLocaleString()} units</span>
<div className="flex gap-1 justify-end">
<button
className="bg-zinc-100 hover:bg-zinc-200 p-1 px-2 rounded font-bold cursor-pointer text-[10px]"
onClick={() => handleUpdateStock(item.sku, -5)}
title="Decrement 5 units"
>
-5
</button>
<button
className="bg-zinc-100 hover:bg-zinc-200 p-1 px-2 rounded font-bold cursor-pointer text-[10px]"
onClick={() => handleUpdateStock(item.sku, 5)}
title="Increment 5 units"
>
+5
</button>
</div>
</div>
</div>
{/* Gauge percentage */}
<div className="pt-1.5 space-y-1">
<div className="w-full bg-[#eceef0] h-1.5 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-300 ${
item.status === 'Critical' ? 'bg-rose-500' : item.status === 'Low Stock' ? 'bg-amber-500' : 'bg-[#581c87]'
}`}
style={{ width: `${Math.min(percentage, 100)}%` }}
/>
</div>
<div className="flex justify-between text-[9px] text-zinc-400 font-bold">
<span>Verification Level: {Math.round(percentage)}%</span>
{item.status !== 'Optimal' && (
<button
onClick={() => handleReplenishSku(item.sku)}
className="text-[#581c87] hover:underline flex items-center gap-px font-bold cursor-pointer"
>
<Zap size={11} className="text-amber-500 animate-bounce" />
Auto-Replenish
</button>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
</div>
</>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-gutter text-xs font-sans">
{/* Left Column: Catalogue Import & Batch Console */}
<div className="space-y-lg">
{/* Fast Imports presets Card */}
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl shadow-sm space-y-md">
<div className="flex items-center gap-xs text-[#0f172a] font-bold text-sm">
<Sparkles className="text-amber-500" size={18} />
<h3>Tamil Nadu Region Catalog Presets</h3>
</div>
<p className="text-zinc-500 leading-relaxed text-[11px]">
Instantly import bulk verified grocers, spices and diary products catalogs from local Coimbatore farms & cooperatives.
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-sm">
{/* Preset 1 */}
<div className="border border-[#e2e8f0] rounded-xl p-sm space-y-md hover:border-purple-300 transition-colors bg-[#f8fafc]/30">
<div>
<h4 className="font-bold text-zinc-900 leading-tight">Nilgiris Dairy Fresh Pack</h4>
<p className="text-[10px] text-zinc-400 mt-0.5">3 High-Margin Butter & Cheese SKU</p>
</div>
<div className="flex justify-between items-center">
<span className="text-[10px] font-mono font-bold text-[#581c87]">CBE-COOP-04</span>
<button
onClick={() => handleImportPreset('Nilgiris Dairy Coop', nilgirisDairy)}
className="px-2 py-1 bg-[#581c87] hover:bg-purple-800 text-white font-bold rounded text-[9px] uppercase cursor-pointer"
>
Import Batch
</button>
</div>
</div>
{/* Preset 2 */}
<div className="border border-[#e2e8f0] rounded-xl p-sm space-y-md hover:border-purple-300 transition-colors bg-[#f8fafc]/30">
<div>
<h4 className="font-bold text-zinc-900 leading-tight">Coimbatore Heritage Grains</h4>
<p className="text-[10px] text-zinc-400 mt-0.5">3 Premium Boiled Rice & Oils</p>
</div>
<div className="flex justify-between items-center">
<span className="text-[10px] font-mono font-bold text-emerald-600">TAMIL-AGRI-09</span>
<button
onClick={() => handleImportPreset('Coimbatore Heritage', cbeHeritage)}
className="px-2 py-1 bg-[#581c87] hover:bg-purple-800 text-white font-bold rounded text-[9px] uppercase cursor-pointer"
>
Import Batch
</button>
</div>
</div>
</div>
</div>
{/* Custom CSV Parsing Box */}
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl shadow-sm space-y-md">
<div className="flex items-center gap-xs text-[#0f172a] font-bold text-sm">
<FileSpreadsheet className="text-[#581c87]" size={18} />
<h3>Manual CSV Direct-Entry Console</h3>
</div>
<p className="text-zinc-500 text-[11px]">
Paste comma-separated rows here (Name, SKU, Category, Price, InitialStock) to bulk register catalog elements.
</p>
<div className="space-y-sm">
<textarea
value={csvText}
onChange={(e) => setCsvText(e.target.value)}
className="w-full h-28 p-sm font-mono text-[11px] border border-[#e2e8f0] rounded-lg bg-[#f8fafc] outline-none focus:bg-white focus:ring-1 focus:ring-[#581c87] leading-relaxed"
/>
<div className="flex justify-between items-center">
<span className="text-[10px] text-zinc-400 font-medium">Header line is skipped automatically.</span>
<button
onClick={handleCSVImport}
className="bg-[#581c87] text-white px-xl py-2 rounded-lg text-xs font-bold uppercase tracking-wider cursor-pointer hover:bg-purple-800 transition"
>
Parse CSV Data & Sync
</button>
</div>
</div>
</div>
{/* Realtime Import Logs list */}
<div className="bg-[#f8fafc]/50 border border-[#e2e8f0] p-md rounded-xl">
<h3 className="font-sans font-bold text-sm text-[#0f172a] mb-xs">Live Channel Import Logs & Audit</h3>
<p className="text-zinc-505 mb-md text-[11px]">Recent logistics synchronization log sequences executed by central Coimbatore ERP.</p>
<div className="space-y-sm">
{importLogs.map((log, idx) => (
<div key={idx} className="bg-white p-sm border border-[#e2e8f0] rounded-lg flex items-center justify-between text-xs font-sans">
<div className="space-y-0.5">
<div className="flex items-center gap-sm">
<span className="font-mono font-bold text-[#581c87]">{log.batchRef}</span>
<span className="text-zinc-400 text-[10px] font-medium">{log.timestamp}</span>
</div>
<p className="font-bold text-zinc-800">{log.type} <em className="text-zinc-400 font-normal">{log.source}</em></p>
</div>
<span className="px-1.5 py-0.5 bg-emerald-50 border border-emerald-100 text-emerald-600 font-bold uppercase text-[9px] rounded">
{log.status}
</span>
</div>
))}
</div>
</div>
</div>
{/* Right Column: Beautiful Interactive Brand Design Studio */}
<div className="space-y-lg">
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl shadow-sm space-y-md">
<div className="flex items-center gap-xs text-[#0f172a] font-bold text-sm">
<Palette className="text-[#581c87]" size={18} />
<h3>Operational Branding & Package Studio</h3>
</div>
<p className="text-zinc-500 leading-relaxed text-[11px]">
Grocery apps and parcel delivery bags use custom generated corporate brand designs. Style bag backgrounds, badges, and titles live.
</p>
<div className="space-y-md text-xs">
{/* Studio Control 1 */}
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">BRAND THEME CAPTION</label>
<input
type="text"
value={brandStyle.themeName}
onChange={(e) => setBrandStyle({ ...brandStyle, themeName: 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]"
/>
</div>
{/* Studio Control 2 */}
<div className="grid grid-cols-2 gap-sm">
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">PRIMARY BACKPLANE COLOR</label>
<div className="flex gap-sm items-center">
<input
type="color"
value={brandStyle.primaryColor}
onChange={(e) => setBrandStyle({ ...brandStyle, primaryColor: e.target.value })}
className="w-8 h-8 rounded border border-zinc-200 cursor-pointer"
/>
<span className="font-mono font-bold text-zinc-700">{brandStyle.primaryColor}</span>
</div>
</div>
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">ACCENT TEXT COLOR</label>
<div className="flex gap-sm items-center">
<input
type="color"
value={brandStyle.secondaryColor}
onChange={(e) => setBrandStyle({ ...brandStyle, secondaryColor: e.target.value })}
className="w-8 h-8 rounded border border-zinc-200 cursor-pointer"
/>
<span className="font-mono font-bold text-zinc-700">{brandStyle.secondaryColor}</span>
</div>
</div>
</div>
{/* Studio Control 3 */}
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">BAG PRINT FOOTER TAG</label>
<input
type="text"
value={brandStyle.bagLabel}
onChange={(e) => setBrandStyle({ ...brandStyle, bagLabel: 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]"
/>
</div>
{/* Studio Control 4 */}
<div className="flex items-center justify-between p-sm bg-[#f8fafc] border border-zinc-200/50 rounded-lg">
<div>
<h4 className="font-bold text-zinc-900 text-xs">Acknowledge Eco-Certified Badge</h4>
<p className="text-[10px] text-zinc-400 mt-0.5">Prints stamp acknowledging sustainable jute bag usage.</p>
</div>
<input
type="checkbox"
checked={brandStyle.isEcoVerified}
onChange={() => setBrandStyle({ ...brandStyle, isEcoVerified: !brandStyle.isEcoVerified })}
className="w-4 h-4 text-emerald-600 border-[#e2e8f0] rounded focus:ring-0 outline-none cursor-pointer"
/>
</div>
{/* Interactive Dynamic Checkout Jute Bag Preview Canvas */}
<div className="border border-[#e2e8f0] rounded-xl p-md bg-zinc-50 space-y-sm">
<span className="text-[9px] font-sans font-bold text-zinc-400 uppercase tracking-widest block text-center border-b border-zinc-200 pb-1">
Live Packaged Grocery Bag Design Preview
</span>
<div className="relative mx-auto w-48 h-64 bg-[#efe5d9] border-2 border-[#d2b48c] rounded-b-2xl rounded-t-lg shadow-inner flex flex-col justify-between p-sm">
{/* Hanging handle simulation */}
<div className="absolute -top-3 left-1/2 -translate-x-1/2 w-20 h-5 border-2 border-b-0 border-[#d2b48c] rounded-t-full" />
<div className="text-center pt-md space-y-1">
<span className="text-[10px] font-bold block tracking-tight uppercase" style={{ color: brandStyle.primaryColor }}>
{brandStyle.themeName || 'nearledaily Fresh'}
</span>
<div className="w-12 h-0.5 mx-auto bg-amber-500" style={{ backgroundColor: brandStyle.secondaryColor }} />
</div>
<div className="my-auto flex flex-col items-center text-center p-1 space-y-1">
<ShoppingBag className="w-10 h-10 stroke-1" style={{ color: brandStyle.primaryColor }} />
<span className="text-[9px] font-medium max-w-[130px] leading-tight block text-zinc-700">
{brandStyle.bagLabel || 'Grown with Pride'}
</span>
</div>
<div className="flex justify-between items-center text-[8px] border-t border-zinc-300 pt-1">
<span className="font-bold text-zinc-500">100% COMPOSTABLE</span>
{brandStyle.isEcoVerified && (
<span className="text-emerald-700 font-bold bg-emerald-100 px-1 py-0.5 rounded text-[7px]">
CBE-ECO
</span>
)}
</div>
</div>
<p className="text-center text-[10px] text-zinc-405 font-medium">Standard printed thermal stamps scale according to the preview.</p>
</div>
</div>
</div>
</div>
</div>
)}
{/* CREATE NEW PRODUCT MODAL PORTAL */}
{showAddProductModal && (
<div
className="fixed inset-0 bg-[#0f172a]/40 backdrop-blur-sm z-[200] flex items-center justify-center p-md"
onClick={(e) => { if (e.target === e.currentTarget) setShowAddProductModal(false); }}
>
<div className="bg-white border border-[#e2e8f0] rounded-xl w-full max-w-[28rem] max-h-[90vh] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200 text-xs font-sans cursor-default">
<div className="p-md border-b border-[#e2e8f0] bg-[#f8fafc] flex justify-between items-center shrink-0">
<h4 className="font-bold text-[#0f172a] flex items-center gap-xs">
<Package size={15} className="text-[#581c87]" />
Introduce New Grocery Catalog SKU
</h4>
<button
onClick={() => setShowAddProductModal(false)}
className="p-1 hover:bg-zinc-200 rounded-full text-zinc-400 cursor-pointer transition-colors"
>
<X size={16} />
</button>
</div>
<form onSubmit={handleAddNewProduct} className="flex-1 flex flex-col min-h-0 overflow-hidden">
<div className="p-md space-y-md overflow-y-auto flex-1">
<div className="space-y-sm">
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">PRODUCT BRAND NAME (*)</label>
<input
type="text"
placeholder="e.g. Aavin Pure Cow Ghee"
value={newProduct.name}
onChange={(e) => setNewProduct({ ...newProduct, name: 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]"
required
/>
</div>
<div className="grid grid-cols-2 gap-sm">
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">SKU CODE IDENTIFIER (*)</label>
<input
type="text"
placeholder="e.g. GHEE-AAV-500"
value={newProduct.sku}
onChange={(e) => setNewProduct({ ...newProduct, sku: 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] font-mono"
required
/>
</div>
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">CATEGORY SEGMENT</label>
<select
value={newProduct.category}
onChange={(e) => setNewProduct({ ...newProduct, category: e.target.value })}
className="w-full border border-[#e2e8f0] rounded-lg p-2 bg-[#f8fafc] focus:bg-white outline-none"
>
<option value="Staples / Rice">Staples / Rice</option>
<option value="Groceries / Oils">Groceries / Oils</option>
<option value="Beverages / Coffee">Beverages / Coffee</option>
<option value="Fresh Produce / Veg">Fresh Produce / Veg</option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-sm">
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">ESTIMATED price ()</label>
<input
type="number"
value={newProduct.price}
onChange={(e) => setNewProduct({ ...newProduct, price: Number(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]"
required
/>
</div>
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">INITIAL ALLOCATED BALANCES</label>
<input
type="number"
value={newProduct.initialStock}
onChange={(e) => setNewProduct({ ...newProduct, initialStock: Number(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]"
required
/>
</div>
</div>
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">PRODUCT IMAGE PATH OR LINK</label>
<input
type="text"
value={newProduct.image}
onChange={(e) => setNewProduct({ ...newProduct, image: 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]"
/>
</div>
</div>
</div>
<div className="p-md border-t border-[#f1f5f9] flex justify-end gap-sm bg-[#f8fafc] shrink-0">
<button
type="button"
onClick={() => setShowAddProductModal(false)}
className="px-4 py-2 border border-[#e2e8f0] rounded-lg font-semibold text-zinc-500 hover:bg-zinc-50 cursor-pointer"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-[#581c87] text-white rounded-lg font-bold hover:bg-purple-800 cursor-pointer shadow-sm"
>
Commit Product Design SKU
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,989 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useState, useEffect } from 'react';
import {
Layers,
AlertTriangle,
CheckCircle,
HelpCircle,
ArrowUpRight,
TrendingUp,
Sliders,
DollarSign,
PackageCheck,
PlusSquare,
ArrowRightLeft,
XCircle,
FolderSync,
UploadCloud,
FileCheck,
Download,
AlertOctagon,
X,
Calendar,
FileSpreadsheet
} from 'lucide-react';
import { initialImportLogs } from '../data';
import { InventoryItem, OrderItem } from '../types';
import {
useFiestaStockStatement,
useFiestaDeliveries,
useFiestaTenantLocations,
} from '../services/fiestaQueries';
import { FIESTA_TENANT_ID, FIESTA_PRIMARY_LOCATION_ID, num as fnum, str as fstr, ymd } from '../services/fiestaApi';
import { stockRowToProduct, stockRowToInventory, mapOrderStatus, shortTime } from '../services/fiestaMappers';
interface OperationsViewProps {
searchQuery: string;
isCoimbatoreView: boolean;
}
export default function OperationsView({ searchQuery, isCoimbatoreView }: OperationsViewProps) {
// Sub-tabs state
const [activeSubTab, setActiveSubTab] = useState<'inventory' | 'catalogue' | 'orders' | 'import'>('inventory');
// ── Live operations data (Fiesta) ─────────────────────────────────────────
const today = new Date();
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
// Date-range filter for the Orders sub-tab (drives the live deliveries query).
const [fromdate, setFromdate] = useState<string>(ymd(monthStart));
const [todate, setTodate] = useState<string>(ymd(today));
const dayOffset = (n: number) => {
const d = new Date();
d.setDate(d.getDate() - n);
return ymd(d);
};
const datePresets: Array<{ key: string; label: string; from: string; to: string }> = [
{ 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: '30d', label: 'Last 30 Days', from: dayOffset(29), to: ymd(today) },
{ key: 'month', label: 'This Month', from: ymd(monthStart), to: ymd(today) },
];
const activePreset = datePresets.find((p) => p.from === fromdate && p.to === todate)?.key ?? 'custom';
const locationsQ = useFiestaTenantLocations(FIESTA_TENANT_ID);
const primaryLocation =
(locationsQ.data ?? []).find((l) => Number(l.locationid) === FIESTA_PRIMARY_LOCATION_ID) ||
(locationsQ.data ?? [])[0];
const locationId = primaryLocation ? Number(primaryLocation.locationid) : FIESTA_PRIMARY_LOCATION_ID;
const locationName = fstr(primaryLocation?.locationname) || 'Primary Outlet';
const stockQ = useFiestaStockStatement({
tenantid: FIESTA_TENANT_ID,
locationid: locationId,
keyword: '',
pageno: 1,
pagesize: 100,
});
const deliveriesQ = useFiestaDeliveries({ tenantid: FIESTA_TENANT_ID, fromdate, todate });
// Total inventory value = Σ closing × unit cost across the live stock statement.
const inventoryValue = (stockQ.data ?? []).reduce(
(sum, r) => sum + fnum(r.closing) * fnum(r.productcost),
0,
);
// Dynamic state arrays for interaction (seeded from live data once it loads).
const [inventoryList, setInventoryList] = useState<InventoryItem[]>([]);
const [productList, setProductList] = useState<ReturnType<typeof stockRowToProduct>[]>([]);
const [orderList, setOrderList] = useState<OrderItem[]>([]);
const [importLogs, setImportLogs] = useState(initialImportLogs);
useEffect(() => {
if (stockQ.data) {
setProductList(stockQ.data.map(stockRowToProduct));
setInventoryList(stockQ.data.map((r) => stockRowToInventory(r, locationName)));
}
}, [stockQ.data, locationName]);
useEffect(() => {
if (deliveriesQ.data) {
setOrderList(
deliveriesQ.data.map((r): OrderItem => {
const cust = mapOrderStatus(fstr(r.orderstatus));
return {
id: fstr(r.orderid) || `DLV-${fstr(r.deliveryid)}`,
store: fstr(r.pickupcustomer) || fstr(r.pickuplocation) || `Location ${fstr(r.locationid)}`,
amount: fnum(r.deliveryamt) || fnum(r.orderamount),
time: shortTime(r.assigntime || r.deliverydate),
status: cust === 'DELIVERED' ? 'SHIPPED' : fstr(r.orderstatus).toLowerCase() === 'cancelled' ? 'FLAGGED' : 'PROCESSING',
};
}),
);
}
}, [deliveriesQ.data]);
// Modal open states
const [showAddSkuModal, setShowAddSkuModal] = useState(false);
const [showTransferModal, setShowTransferModal] = useState(false);
// Form states
const [newSku, setNewSku] = useState({
sku: '',
name: '',
warehouse: '',
stockLevel: 0,
maxCapacity: 1000,
status: 'Optimal' as 'Optimal' | 'Low Stock' | 'Critical',
region: 'CBE-NORTH' as 'CBE-NORTH' | 'CBE-SOUTH' | 'CBE-EAST' | 'CBE-WEST' | 'TIRUPPUR'
});
const [transferData, setTransferData] = useState({
sku: '',
origin: '',
destination: '',
quantity: 100
});
// Filter lists based on global Search query
const filteredInventory = inventoryList.filter(item =>
item.sku.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.warehouse.toLowerCase().includes(searchQuery.toLowerCase())
);
const filteredCatalogue = productList.filter(prod =>
prod.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
prod.sku.toLowerCase().includes(searchQuery.toLowerCase()) ||
prod.category.toLowerCase().includes(searchQuery.toLowerCase())
);
const filteredOrders = orderList.filter(ord =>
ord.id.toLowerCase().includes(searchQuery.toLowerCase()) ||
ord.store.toLowerCase().includes(searchQuery.toLowerCase())
);
// Form submit handles
const handleAddSku = (e: React.FormEvent) => {
e.preventDefault();
if (!newSku.sku || !newSku.name || !newSku.warehouse) {
alert('Please fill out all metadata fields before committing.');
return;
}
const itemToAdd: InventoryItem = {
sku: newSku.sku,
name: newSku.name,
warehouse: newSku.warehouse,
stockLevel: Number(newSku.stockLevel),
maxCapacity: Number(newSku.maxCapacity),
status: newSku.stockLevel < 20 ? 'Critical' : newSku.stockLevel < 100 ? 'Low Stock' : 'Optimal',
region: newSku.region
};
setInventoryList([itemToAdd, ...inventoryList]);
setShowAddSkuModal(false);
// reset form
setNewSku({
sku: '',
name: '',
warehouse: '',
stockLevel: 0,
maxCapacity: 1000,
status: 'Optimal',
region: 'CBE-NORTH'
});
};
const handleExecuteTransfer = (e: React.FormEvent) => {
e.preventDefault();
if (!transferData.sku || !transferData.origin || !transferData.destination) {
alert('Kindly configure transfer coordinates and SKU.');
return;
}
// Attempt to update origin stock levels
setInventoryList(prev =>
prev.map(item => {
if (item.sku === transferData.sku) {
const newLevel = Math.max(0, item.stockLevel - transferData.quantity);
return {
...item,
stockLevel: newLevel,
status: newLevel < 20 ? 'Critical' : newLevel < 100 ? 'Low Stock' : 'Optimal'
};
}
return item;
})
);
alert(`Transfer of ${transferData.quantity} units committed successfully. Tracking ID: TRF-${Math.floor(Math.random() * 900000 + 100000)}`);
setShowTransferModal(false);
};
const handleToggleProductExposure = (id: string) => {
setProductList(prev =>
prev.map(p => p.id === id ? { ...p, verified: !p.verified } : p)
);
};
return (
<div className="space-y-lg animate-in fade-in duration-500">
{/* Tab Navigation header */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-end border-b border-[#e2e8f0] pb-px gap-2">
<div>
<h1 className="font-sans font-bold text-2xl tracking-tight text-[#0f172a]">
Operations Hub{isCoimbatoreView && ': Coimbatore'}
</h1>
<p className="text-zinc-500 font-sans text-xs mt-1">
Global product assortment, inventory levels, warehouse tracking, and data sync tools.
</p>
</div>
{/* Dynamic Nav Sub-Tabs */}
<nav className="flex gap-lg">
{(['inventory', 'catalogue', 'orders', 'import'] as const).map((tab) => (
<button
key={tab}
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-zinc-400 hover:text-zinc-600'
}`}
>
{tab}
{activeSubTab === tab && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-[#581c87] animate-in slide-in-from-left-2 duration-100" />
)}
</button>
))}
</nav>
</div>
{/* Dynamic Display Area based on tabs selection */}
{activeSubTab === 'inventory' && (
<div className="space-y-lg">
{/* Top Key Operational Indicators Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-gutter">
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl flex justify-between items-start shadow-sm">
<div>
<p className="text-[11px] font-sans font-bold text-zinc-500 uppercase tracking-widest">
Total Inventory Value
</p>
<h3 className="font-sans font-bold text-[#0f172a] text-xl mt-xs">
{inventoryValue.toLocaleString('en-IN', { maximumFractionDigits: 0 })}
</h3>
<p className="text-[11px] text-emerald-600 font-semibold mt-sm flex items-center gap-1.5">
<TrendingUp size={12} />
{productList.length} SKUs · {locationName}
</p>
</div>
<div className="p-2.5 rounded-lg bg-purple-50 text-[#581c87]">
<DollarSign size={18} />
</div>
</div>
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl flex justify-between items-start shadow-sm">
<div>
<p className="text-[11px] font-sans font-bold text-zinc-500 uppercase tracking-widest">
Low Stock Alerts
</p>
<h3 className="font-sans font-bold text-rose-500 text-xl mt-xs">
{inventoryList.filter(item => item.status !== 'Optimal').length} SKUs
</h3>
<p className="text-[11px] text-zinc-400 mt-sm">Across local regional warehouses</p>
</div>
<div className="p-2.5 rounded-lg bg-rose-50 text-rose-500">
<AlertTriangle size={18} />
</div>
</div>
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl flex justify-between items-start shadow-sm">
<div>
<p className="text-[11px] font-sans font-bold text-zinc-500 uppercase tracking-widest">
Fulfillment Health
</p>
<h3 className="font-sans font-bold text-[#0f172a] text-xl mt-xs">98.4%</h3>
<div className="w-40 bg-[#eceef0] h-1.5 rounded-full overflow-hidden mt-sm">
<div className="bg-[#581c87] h-full rounded-full" style={{ width: '98.4%' }}></div>
</div>
</div>
<div className="p-2.5 rounded-lg bg-emerald-50 text-emerald-600 animate-pulse">
<PackageCheck size={18} />
</div>
</div>
</div>
{/* Sub Grid splits: Product state table (3 cols) and Action sidebar panels (1 col) */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-gutter">
{/* Left table container */}
<div className="lg:col-span-3 bg-white border border-[#e2e8f0] rounded-xl overflow-hidden flex flex-col justify-between shadow-sm">
<div>
<div className="p-md border-b border-[#e2e8f0] flex justify-between items-center bg-[#f8fafc]">
<h4 className="font-sans font-bold text-sm text-[#0f172a]">
Product Inventory Levels
</h4>
<div className="flex gap-2 text-xs">
<span className="text-zinc-500 font-medium">Auto-synced</span>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left font-sans text-xs">
<thead className="bg-[#f8fafc] border-b border-[#e2e8f0] text-zinc-500 text-[10px] uppercase tracking-wider font-semibold">
<tr>
<th className="p-md">Product SKU</th>
<th className="p-md">Warehouse</th>
<th className="p-md">Stock Level</th>
<th className="p-md">Status</th>
<th className="p-md text-right">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-[#f1f5f9]">
{filteredInventory.length === 0 ? (
<tr>
<td colSpan={5} className="text-center py-10 text-zinc-400">
No matching items identified. Try another query or reload.
</td>
</tr>
) : (
filteredInventory.map((item, idx) => {
const percentage = (item.stockLevel / item.maxCapacity) * 100;
return (
<tr key={idx} className="hover:bg-[#f2f4f6]/50 transition-colors">
<td className="p-md">
<p className="font-bold text-[#0f172a]">{item.sku}</p>
<p className="text-[10px] text-zinc-400 font-medium">{item.name}</p>
</td>
<td className="p-md text-zinc-600 font-medium">
<span className="bg-[#f2f4f6] px-1.5 py-0.5 rounded text-[10px] font-mono mr-1">
{item.region}
</span>
{item.warehouse}
</td>
<td className="p-md">
<p className="font-mono font-bold text-zinc-700">{item.stockLevel.toLocaleString()} units</p>
<div className="w-24 bg-[#eceef0] h-1 mt-1 rounded-full overflow-hidden">
<div
className={`h-full rounded-full ${
item.status === 'Critical' ? 'bg-rose-500' : item.status === 'Low Stock' ? 'bg-amber-500' : 'bg-[#581c87]'
}`}
style={{ width: `${Math.min(percentage, 100)}%` }}
/>
</div>
</td>
<td className="p-md">
<span className={`px-2 py-0.5 rounded-full text-[10px] uppercase font-bold tracking-tight inline-block ${
item.status === 'Critical'
? 'bg-rose-100 text-rose-750'
: item.status === 'Low Stock'
? 'bg-amber-100 text-amber-750'
: 'bg-emerald-100 text-emerald-750'
}`}>
{item.status}
</span>
</td>
<td className="p-md text-right">
<div className="flex justify-end gap-1">
<button
onClick={() => {
setTransferData({
sku: item.sku,
origin: item.warehouse,
destination: '',
quantity: 50
});
setShowTransferModal(true);
}}
className="text-xs font-semibold text-[#581c87] hover:underline cursor-pointer"
>
Transfer
</button>
</div>
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
<div className="p-md border-t border-[#f1f5f9] bg-[#f8fafc] flex justify-between items-center text-[10px] text-zinc-500 font-medium font-sans">
<span>Displaying 1-{filteredInventory.length} of {inventoryList.length} global items</span>
<span>Active Ledger Nodes Online</span>
</div>
</div>
{/* Right sidebar quick tasks */}
<div className="space-y-gutter">
{/* Forecast module visual details */}
<div className="bg-[#0f172a] text-white p-6 rounded-xl relative overflow-hidden flex flex-col justify-between shadow-md h-48">
<div>
<span className="text-[10px] tracking-wider font-bold opacity-60 uppercase">
Forecast Efficiency
</span>
<p className="font-sans font-bold text-3xl mt-sm">92%</p>
<p className="text-zinc-300 text-xs mt-sm leading-relaxed">
AI-Driven automated replenishment is saving an estimated 1.9L/week in system overstock costs.
</p>
</div>
{/* Embedded SVG graphic visual */}
<div className="absolute right-3 bottom-3 opacity-15">
<PackageCheck size={64} className="text-purple-300" />
</div>
</div>
{/* Quick Actions buttons block */}
<div className="bg-white border border-[#e2e8f0] rounded-xl p-md shadow-sm">
<span className="text-[10px] font-sans font-bold text-zinc-400 uppercase tracking-widest block pb-xs mb-md border-b border-[#f1f5f9]">
Quick Actions Ledger
</span>
<div className="grid grid-cols-2 gap-sm">
<button
onClick={() => 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"
>
<PlusSquare size={16} className="text-zinc-400 group-hover:text-[#581c87]" />
<span className="text-[10px] font-sans font-bold uppercase">Add SKU</span>
</button>
<button
onClick={() => {
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"
>
<ArrowRightLeft size={16} className="text-zinc-400 group-hover:text-[#581c87]" />
<span className="text-[10px] font-sans font-bold uppercase">Transfer</span>
</button>
<button
onClick={() => {
const amount = prompt('Enter return item SKU code:');
if (amount) {
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"
>
<XCircle size={16} className="text-zinc-400 group-hover:text-[#581c87]" />
<span className="text-[10px] font-sans font-bold uppercase">Returns</span>
</button>
<button
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"
>
<FolderSync size={16} className="text-zinc-400 group-hover:text-[#581c87]" />
<span className="text-[10px] font-sans font-bold uppercase">Audit CSV</span>
</button>
</div>
</div>
</div>
</div>
</div>
)}
{activeSubTab === 'catalogue' && (
<div className="space-y-md animate-in slide-in-from-right-5">
<div className="flex justify-between items-end bg-[#f8fafc] border border-[#e2e8f0] p-md rounded-xl shadow-sm">
<div>
<h3 className="font-sans font-bold text-sm text-[#0f172a]">
Master Assortment Catalogue
</h3>
<p className="text-zinc-500 text-xs font-sans mt-0.5">
Global inventory master list and exposure levels across 4,200 nodes.
</p>
</div>
<button
onClick={() => {
const title = prompt('Enter product brand title:');
const sku = prompt('Enter SKU catalog code:');
const category = prompt('Enter SKU Category:');
if (title && sku && category) {
setProductList(prev => [
...prev,
{
id: String(prev.length + 1),
name: title,
sku: sku,
unitsSold: 0,
revenue: 0,
stockStatus: 'Healthy',
trend: 'flat',
image: 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&w=150&q=80',
category: category,
exposure: '0/120 Stores',
verified: false
}
]);
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"
>
Add Brand Product
</button>
</div>
<div className="bg-white border border-[#e2e8f0] rounded-xl overflow-hidden shadow-sm">
<table className="w-full text-left font-sans text-xs">
<thead className="bg-[#f8fafc] border-b border-[#e2e8f0] text-zinc-500 text-[10px] uppercase font-bold tracking-wider">
<tr>
<th className="p-md">Product Details</th>
<th className="p-md">Category segment</th>
<th className="p-md">Verification status</th>
<th className="p-md">Store exposure</th>
<th className="p-md text-right">Exposure toggle</th>
</tr>
</thead>
<tbody className="divide-y divide-[#f1f5f9]">
{filteredCatalogue.map((prod) => (
<tr key={prod.id} className="hover:bg-[#f2f4f6]/50 transition-colors">
<td className="p-md flex items-center gap-md">
<div className="w-10 h-10 rounded-lg border border-[#e2e8f0] overflow-hidden shrink-0 bg-zinc-50">
<img
src={prod.image}
alt={prod.name}
referrerPolicy="no-referrer"
className="w-full h-full object-cover"
/>
</div>
<div>
<p className="font-bold text-[#0f172a]">{prod.name}</p>
<p className="text-[10px] text-zinc-400 font-mono">SKU: {prod.sku}</p>
</div>
</td>
<td className="p-md text-zinc-600 font-medium">{prod.category}</td>
<td className="p-md">
{prod.verified ? (
<span className="flex items-center gap-1.5 text-emerald-600 font-semibold tracking-tight text-[11px]">
<span className="w-2 h-2 rounded-full bg-emerald-500"></span> Verified Portfolio
</span>
) : (
<span className="flex items-center gap-1.5 text-zinc-400 font-medium tracking-tight text-[11px]">
<span className="w-2 h-2 rounded-full bg-zinc-300"></span> Under Inspection
</span>
)}
</td>
<td className="p-md text-zinc-500 font-medium">{prod.exposure}</td>
<td className="p-md text-right">
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={prod.verified}
onChange={() => handleToggleProductExposure(prod.id)}
className="sr-only peer"
/>
<div className="w-9 h-5 bg-zinc-200 rounded-full peer peer-focus:ring-0 peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-zinc-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-[#581c87]"></div>
</label>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{activeSubTab === 'orders' && (
<div className="space-y-md animate-in slide-in-from-right-5">
{/* Day-wise date filter — drives the live deliveries/orders query */}
<div className="bg-white border border-[#e2e8f0] rounded-xl p-md shadow-sm flex flex-col lg:flex-row lg:items-center justify-between gap-md">
<div className="flex items-center gap-sm flex-wrap">
<span className="flex items-center gap-1.5 text-[10px] font-bold text-zinc-400 uppercase tracking-widest pr-1">
<Calendar size={13} className="text-[#581c87]" /> View
</span>
{datePresets.map((p) => (
<button
key={p.key}
onClick={() => { setFromdate(p.from); setTodate(p.to); }}
className={`px-3 py-1.5 rounded-lg text-[11px] font-bold transition-all border cursor-pointer ${
activePreset === p.key
? 'bg-[#581c87] text-white border-[#581c87] shadow-sm'
: 'bg-white text-zinc-600 border-[#e2e8f0] hover:bg-zinc-50'
}`}
>
{p.label}
</button>
))}
</div>
<div className="flex items-center gap-sm text-xs">
<div className="flex items-center gap-1.5">
<label className="text-[10px] font-bold text-zinc-400 uppercase tracking-wider">From</label>
<input
type="date"
value={fromdate}
max={todate}
onChange={(e) => setFromdate(e.target.value)}
className="border border-[#e2e8f0] rounded-lg px-2 py-1.5 text-zinc-700 font-medium outline-none focus:ring-1 focus:ring-[#581c87] cursor-pointer"
/>
</div>
<span className="text-zinc-300"></span>
<div className="flex items-center gap-1.5">
<label className="text-[10px] font-bold text-zinc-400 uppercase tracking-wider">To</label>
<input
type="date"
value={todate}
min={fromdate}
max={ymd(today)}
onChange={(e) => setTodate(e.target.value)}
className="border border-[#e2e8f0] rounded-lg px-2 py-1.5 text-zinc-700 font-medium outline-none focus:ring-1 focus:ring-[#581c87] cursor-pointer"
/>
</div>
</div>
</div>
<div className="bg-white border border-[#e2e8f0] rounded-xl overflow-hidden shadow-sm">
<div className="p-md border-b border-[#e2e8f0] bg-[#f8fafc] flex justify-between items-center">
<h4 className="font-sans font-bold text-sm text-[#0f172a]">
Orders ({filteredOrders.length})
</h4>
<span className="text-[10px] text-zinc-400 font-medium uppercase tracking-wider">
{fromdate === todate ? fromdate : `${fromdate}${todate}`}
</span>
</div>
<table className="w-full text-left font-sans text-xs">
<thead className="bg-[#f8fafc] border-b border-[#e2e8f0] text-zinc-500 text-[10px] uppercase font-bold tracking-wider">
<tr>
<th className="p-md">Order ID</th>
<th className="p-md">Origin Store Terminal</th>
<th className="p-md">Invoice Amount</th>
<th className="p-md">Committed Time (IST)</th>
<th className="p-md">System state status</th>
</tr>
</thead>
<tbody className="divide-y divide-[#f1f5f9]">
{filteredOrders.length === 0 ? (
<tr>
<td colSpan={5} className="text-center py-10 text-zinc-400">
{deliveriesQ.isLoading ? 'Loading live orders…' : 'No orders in this date range.'}
</td>
</tr>
) : (
filteredOrders.map((ord) => (
<tr key={ord.id} className="hover:bg-[#f2f4f6]/50 transition-colors">
<td className="p-md font-mono font-bold text-[#581c87]">{ord.id}</td>
<td className="p-md text-[#0f172a] font-medium">{ord.store}</td>
<td className="p-md font-mono font-bold text-zinc-700">{ord.amount.toLocaleString()}</td>
<td className="p-md text-zinc-500 font-medium">{ord.time}</td>
<td className="p-md">
<span className={`px-2 py-0.5 rounded text-[9px] font-bold tracking-wider ${
ord.status === 'SHIPPED'
? 'bg-purple-100 text-[#581c87] border border-purple-200'
: ord.status === 'FLAGGED'
? 'bg-rose-100 text-rose-700 border border-rose-200'
: 'bg-zinc-100 text-zinc-650 border border-zinc-200'
}`}>
{ord.status}
</span>
</td>
</tr>
)))}
</tbody>
</table>
</div>
</div>
)}
{activeSubTab === 'import' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-gutter animate-in slide-in-from-right-5">
{/* Upload panel zone */}
<div
onClick={() => {
const fileRef = prompt('Enter CSV filename representation path:');
if (fileRef) {
const logsToAdd = {
timestamp: 'Just now',
batchRef: `#IMP_0922_${String.fromCharCode(65 + Math.floor(Math.random() * 26))}`,
type: 'Inventory Sync',
source: fileRef,
result: `SUCCESS (98 Rows verified)`,
status: 'SUCCESS' as const
};
setImportLogs([logsToAdd, ...importLogs]);
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"
>
<div className="h-14 w-14 bg-purple-50 text-[#581c87] rounded-full flex items-center justify-center mb-md shadow-sm">
<UploadCloud size={24} />
</div>
<h4 className="font-sans font-bold text-base text-[#0f172a]">
Upload Inventory CSV
</h4>
<p className="text-zinc-500 text-xs mt-2 max-w-[20rem] leading-relaxed">
Drag and drop your .csv, .xlsx or .xml sheets here to automatically update global stock balances or master portfolios schemas.
</p>
<div className="mt-lg flex gap-md">
<button className="bg-[#0f172a] text-white text-xs font-semibold px-4 py-2 rounded-lg cursor-pointer hover:bg-zinc-800 transition-colors shadow-sm">
Browse Files
</button>
<button
onClick={(e) => {
e.stopPropagation();
alert('Template documentation initiated.');
}}
className="bg-white border border-[#e2e8f0] text-zinc-600 text-xs px-4 py-2 rounded-lg cursor-pointer hover:bg-zinc-50 transition-colors"
>
Template CSV
</button>
</div>
</div>
{/* Validation indicators checker logs split */}
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl flex flex-col justify-between shadow-sm">
<div>
<span className="text-[10px] font-sans font-bold text-zinc-400 uppercase tracking-widest block pb-xs mb-md border-b border-[#f1f5f9]">
Interactive Schema Validator
</span>
<div className="space-y-sm">
<div className="p-sm bg-emerald-50/50 border border-emerald-100 rounded-xl flex gap-sm items-start text-xs">
<FileCheck size={16} className="text-emerald-500 shrink-0 mt-0.5" />
<div>
<h5 className="font-bold text-[#0f172a]">Verification Rule Passed</h5>
<p className="text-zinc-600 mt-0.5">Primary header nodes align perfectly with Master specification v2.8.</p>
</div>
</div>
<div className="p-sm bg-rose-50/50 border border-rose-100 rounded-xl flex gap-sm items-start text-xs">
<AlertOctagon size={16} className="text-rose-500 shrink-0 mt-bar" />
<div>
<h5 className="font-bold text-[#0f172a]">14 Duplicate SKUs Detected</h5>
<p className="text-zinc-600 mt-0.5">Duplicate item overlaps flagged inside columns 45, 82. Verify manual index configurations before finalizing commit.</p>
<button
onClick={() => alert('Downloading conflicts summary report...')}
className="text-rose-600 font-bold hover:underline mt-sm block"
>
DOWNLOAD RESOLUTION LOG
</button>
</div>
</div>
</div>
</div>
{/* Logs table list */}
<div className="mt-md pt-md border-t border-[#f1f5f9]">
<span className="text-[10px] font-sans font-bold text-zinc-400 uppercase tracking-wider block mb-sm">Recent Import Logs</span>
<div className="space-y-1 max-h-36 overflow-y-auto text-xs">
{importLogs.map((log, i) => (
<div key={i} className="flex justify-between items-center p-2 bg-[#f8fafc] border border-[#e2e8f0]/40 rounded-lg hover:bg-[#faf5ff]/20 transition-colors">
<div>
<p className="font-mono text-[10px] font-bold text-[#581c87]">{log.batchRef}</p>
<p className="text-[9px] text-zinc-400 font-medium">{log.timestamp} {log.source}</p>
</div>
<span className={`text-[9px] font-bold px-1.5 py-0.5 rounded ${
log.status === 'SUCCESS' ? 'text-emerald-700 bg-emerald-100' : 'text-rose-700 bg-rose-100'
}`}>
{log.result}
</span>
</div>
))}
</div>
</div>
</div>
</div>
)}
{/* MODAL 1: ADD SKU MODAL */}
{showAddSkuModal && (
<div className="fixed inset-0 bg-[#0f172a]/40 backdrop-blur-xs z-[150] flex items-center justify-center p-md animate-in fade-in duration-150">
<div className="bg-white border border-[#e2e8f0] rounded-xl w-full max-w-[28rem] shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200">
<div className="p-md border-b border-[#e2e8f0] bg-[#f8fafc] flex justify-between items-center">
<h4 className="font-sans font-bold text-sm text-[#0f172a]">
Add Item SKU to Balance Ledger
</h4>
<button
onClick={() => setShowAddSkuModal(false)}
className="p-1.5 hover:bg-zinc-200 rounded-full text-zinc-400 cursor-pointer text-left"
>
<X size={16} />
</button>
</div>
<form onSubmit={handleAddSku} className="p-md space-y-md text-xs">
<div className="grid grid-cols-2 gap-sm">
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest block text-[9px]">SKU Code (*)</label>
<input
type="text"
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]"
required
/>
</div>
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest block text-[9px]">Product Name</label>
<input
type="text"
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]"
required
/>
</div>
</div>
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest block text-[9px]">Target Warehouse</label>
<input
type="text"
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]"
required
/>
</div>
<div className="grid grid-cols-2 gap-sm">
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest block text-[9px]">Initial Balance</label>
<input
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]"
required
/>
</div>
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest block text-[9px]">Warehouse Region</label>
<select
value={newSku.region}
onChange={(e) => 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]"
>
<option value="CBE-NORTH">Coimbatore North (CBE-NORTH)</option>
<option value="CBE-SOUTH">Coimbatore South (CBE-SOUTH)</option>
<option value="CBE-EAST">Coimbatore East (CBE-EAST)</option>
<option value="CBE-WEST">Coimbatore West (CBE-WEST)</option>
<option value="TIRUPPUR">Tiruppur Regional Hub (TIRUPPUR)</option>
</select>
</div>
</div>
<div className="pt-sm border-t border-[#f1f5f9] flex justify-end gap-sm">
<button
type="button"
onClick={() => setShowAddSkuModal(false)}
className="px-4 py-2 border border-[#e2e8f0] rounded-lg font-semibold text-zinc-500 hover:bg-zinc-50 cursor-pointer"
>
Cancel
</button>
<button
type="submit"
className="px-4 py-2 bg-[#581c87] text-white rounded-lg font-semibold hover:bg-purple-800 cursor-pointer shadow-sm"
>
Commit Ledger SKU
</button>
</div>
</form>
</div>
</div>
)}
{/* MODAL 2: TRANSFER STOCK MODAL */}
{showTransferModal && (
<div className="fixed inset-0 bg-[#0f172a]/40 backdrop-blur-xs z-[150] flex items-center justify-center p-md animate-in fade-in duration-150">
<div className="bg-white border border-[#e2e8f0] rounded-xl w-full max-w-[24rem] shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200">
<div className="p-md border-b border-[#e2e8f0] bg-[#f8fafc] flex justify-between items-center">
<h4 className="font-sans font-bold text-sm text-[#0f172a]">
Request Node Stock Transfer
</h4>
<button
onClick={() => setShowTransferModal(false)}
className="p-1 hover:bg-zinc-200 rounded-full text-zinc-400 cursor-pointer"
>
<X size={16} />
</button>
</div>
<form onSubmit={handleExecuteTransfer} className="p-md space-y-md text-xs">
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest block text-[9px]">TRANSFERRING SKU</label>
<input
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]"
required
/>
</div>
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest block text-[9px]">ORIGIN WAREHOUSE</label>
<input
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]"
required
/>
</div>
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest block text-[9px]">TARGET DESTINATION WAREHOUSE</label>
<input
type="text"
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]"
required
/>
</div>
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest block text-[9px]">TRANSFER QUANTITY</label>
<input
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]"
required
/>
</div>
<div className="pt-sm border-t border-[#f1f5f9] flex justify-end gap-sm">
<button
type="button"
onClick={() => setShowTransferModal(false)}
className="px-4 py-2 border border-[#e2e8f0] rounded-lg font-semibold text-zinc-500 hover:bg-zinc-50 cursor-pointer"
>
Close
</button>
<button
type="submit"
className="px-4 py-2 bg-[#581c87] text-white rounded-lg font-semibold hover:bg-purple-800 cursor-pointer shadow-sm"
>
Approve Routing
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,749 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useState, useEffect } from 'react';
import {
ShoppingBag,
Truck,
CheckCircle2,
Clock,
UserCheck,
MapPin,
TrendingUp,
Plus,
ChevronRight,
Package,
ArrowRight,
AlertCircle,
Clock4,
Search,
Check,
Calendar,
X
} from 'lucide-react';
import { CustomerOrder } from '../types';
import {
useFiestaDeliveries,
useFiestaDeliverySummary,
useFiestaRiders,
} from '../services/fiestaQueries';
import { FIESTA_TENANT_ID, num as fnum, str as fstr, ymd } from '../services/fiestaApi';
import { deliveryRowToOrder } from '../services/fiestaMappers';
interface OrdersDeliveriesViewProps {
searchQuery?: string;
isCoimbatoreView?: boolean;
locationid?: number;
}
interface DeliveryExecutive {
id: string;
name: string;
phone: string;
status: 'Active Duty' | 'Idle' | 'Offline';
rating: number;
completedToday: number;
currentZone: string;
avatar: string;
}
const RIDER_AVATARS = [
'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=150&q=80',
'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=150&q=80',
'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=150&q=80',
];
function riderRowToExecutive(row: Record<string, unknown>, idx: number): DeliveryExecutive {
return {
id: `DE-${fstr(row.userid) || idx}`,
name: fstr(row.fullname) || `${fstr(row.firstname)} ${fstr(row.lastname)}`.trim() || 'Rider',
phone: fstr(row.contactno) || '—',
status: fstr(row.starttime) ? 'Active Duty' : 'Idle',
rating: 4.7,
completedToday: fnum(row.completed) || fnum(row.deliverycount),
currentZone: fstr(row.city) || fstr(row.vehiclename) || fstr(row.vehicleno) || 'Coimbatore',
avatar: RIDER_AVATARS[idx % RIDER_AVATARS.length],
};
}
export default function OrdersDeliveriesView({ searchQuery = '', isCoimbatoreView = false, locationid }: OrdersDeliveriesViewProps) {
// ── Live deliveries / fleet (Fiesta) ──────────────────────────────────────
// Order feed + dispatch controls run off the live deliveries board; the KPI
// strip uses the delivery summary; the fleet panel uses the active riders.
// A date-range filter lets the user view orders/deliveries day-wise.
const today = new Date();
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
const [fromdate, setFromdate] = useState<string>(ymd(monthStart));
const [todate, setTodate] = useState<string>(ymd(today));
// Quick-range presets (computed off the current day; no Date.now in render path).
const dayOffset = (n: number) => {
const d = new Date();
d.setDate(d.getDate() - n);
return ymd(d);
};
const presets: Array<{ key: string; label: string; from: string; to: string }> = [
{ 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: '30d', label: 'Last 30 Days', from: dayOffset(29), to: ymd(today) },
{ key: 'month', label: 'This Month', from: ymd(monthStart), to: ymd(today) },
];
const activePreset = presets.find((p) => p.from === fromdate && p.to === todate)?.key ?? 'custom';
const deliveriesQ = useFiestaDeliveries({ tenantid: FIESTA_TENANT_ID, fromdate, todate });
const summaryQ = useFiestaDeliverySummary({ tenantid: FIESTA_TENANT_ID, fromdate, todate });
const ridersQ = useFiestaRiders({ tenantid: FIESTA_TENANT_ID });
const [orders, setOrders] = useState<CustomerOrder[]>([]);
const [executives, setExecutives] = useState<DeliveryExecutive[]>([]);
const [selectedOrder, setSelectedOrder] = useState<CustomerOrder | null>(null);
const [filterStatus, setFilterStatus] = useState<string>('ALL');
const [localSearch, setLocalSearch] = useState('');
// Seed local state once live data arrives so existing dispatch/create handlers
// continue to mutate in-session.
useEffect(() => {
if (deliveriesQ.data) {
const mapped = deliveriesQ.data.map(deliveryRowToOrder);
setOrders(mapped);
// Keep the current selection only if it's still in the new range; otherwise
// fall back to the first order so the detail panel stays in sync.
setSelectedOrder((prev) =>
(prev && mapped.some((o) => o.id === prev.id)) ? prev : mapped[0] ?? null,
);
}
}, [deliveriesQ.data]);
useEffect(() => {
if (ridersQ.data) setExecutives(ridersQ.data.map(riderRowToExecutive));
}, [ridersQ.data]);
const summary = summaryQ.data;
// Local filtered list of orders
const storeOrders = locationid ? orders.filter(o => o.locationid === locationid) : orders;
const filteredOrdersList = storeOrders.filter(o => {
const term = (localSearch || searchQuery).toLowerCase();
const matchesSearch = o.id.toLowerCase().includes(term) ||
o.customerName.toLowerCase().includes(term) ||
o.address.toLowerCase().includes(term);
const matchesFilter = filterStatus === 'ALL' || o.status === filterStatus;
return matchesSearch && matchesFilter;
});
// Calculate dynamic stats for metrics cards based on filtered storeOrders
const totalDeliveriesCount = storeOrders.length;
const pendingFulfillmentCount = storeOrders.filter(o => o.status === 'PROCESSING' || o.status === 'CONFIRMED').length;
const activeDispatchCount = storeOrders.filter(o => o.status === 'OUT_FOR_DELIVERY').length;
const completedDeliveriesCount = storeOrders.filter(o => o.status === 'DELIVERED').length;
const MOCK_NAMES = ['Aravind Swamy', 'Karthik Raja', 'Priya Mani', 'Meera Jasmine', 'Sanjay Dutt', 'Divya Spandana', 'Vijay Sethupathi', 'Nayan Thara'];
const MOCK_STREETS = ['Avarampalayam Rd', 'DB Road', 'Cross Cut Road', 'Avinashi Road', 'Trichy Road', 'NSR Road', 'Sathy Road', 'Marudhamalai Road'];
const MOCK_ITEMS = [
{ name: 'Tata Salt Premium Iodized 1kg', price: 28 },
{ name: 'Gold Winner Sunflower Oil 1L', price: 145 },
{ name: 'Britannia Marie Gold Biscuit 250g', price: 35 },
{ name: 'MTR Sambar Powder 200g', price: 85 },
{ name: 'Aavin Salted Butter 500g', price: 260 },
{ name: 'Ponni Boiled Rice 5kg', price: 380 },
{ name: 'Fresh Ooty Carrots 500g', price: 45 },
{ name: 'Nescafe Classic Coffee 100g', price: 185 },
];
const handleCreateMockOrder = () => {
const randomName = MOCK_NAMES[Math.floor(Math.random() * MOCK_NAMES.length)];
const randomStreet = MOCK_STREETS[Math.floor(Math.random() * MOCK_STREETS.length)];
const numItems = Math.floor(Math.random() * 3) + 1; // 1 to 3 items
const selectedItems = [];
let amount = 0;
for (let k = 0; k < numItems; k++) {
const it = MOCK_ITEMS[Math.floor(Math.random() * MOCK_ITEMS.length)];
const qty = Math.floor(Math.random() * 2) + 1;
selectedItems.push({ name: it.name, quantity: qty, price: it.price });
amount += it.price * qty;
}
const newId = `ORD-${Math.floor(100000 + Math.random() * 900000)}`;
const newOrder: CustomerOrder = {
id: newId,
customerName: randomName,
phone: `9${Math.floor(100000000 + Math.random() * 900000000)}`,
address: `${Math.floor(10 + Math.random() * 190)}, ${randomStreet}, Coimbatore`,
items: selectedItems,
amount,
time: new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }),
status: 'PROCESSING',
assignedRider: 'Pending Assignment',
hub: locationid ? `Outlet Node #${locationid}` : 'Coimbatore Hub',
locationid: locationid ?? 1097,
};
setOrders(prev => [newOrder, ...prev]);
setSelectedOrder(newOrder);
};
const handleUpdateStatus = (newStatus: CustomerOrder['status']) => {
if (!selectedOrder) return;
setOrders(prev => prev.map(o => {
if (o.id === selectedOrder.id) {
const updated = { ...o, status: newStatus };
setSelectedOrder(updated);
return updated;
}
return o;
}));
};
const handleAssignRider = (riderName: string) => {
if (!selectedOrder) return;
setOrders(prev => prev.map(o => {
if (o.id === selectedOrder.id) {
const updated = {
...o,
assignedRider: riderName,
status: o.status === 'PROCESSING' ? 'CONFIRMED' : o.status
};
setSelectedOrder(updated);
return updated;
}
return o;
}));
};
return (
<div className="space-y-lg animate-in fade-in duration-500">
{/* View Header with Statistics Overview */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-md border-b border-[#e2e8f0] pb-xl">
<div>
<h1 className="font-sans font-bold text-2xl tracking-tight text-[#0f172a]">
Orders & Delivery Operations
</h1>
<p className="text-zinc-500 font-sans text-xs mt-1">
Real-time tracking of app orders, dispatch queues, and active delivery partners across Coimbatore regional sub-hubs.
</p>
<div className="mt-1.5">
{deliveriesQ.isLoading ? (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-zinc-400 uppercase tracking-wide">
<span className="w-1.5 h-1.5 rounded-full bg-zinc-300 animate-pulse" /> Loading live deliveries
</span>
) : deliveriesQ.isError ? (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-rose-600 uppercase tracking-wide">
<span className="w-1.5 h-1.5 rounded-full bg-rose-500" /> Live data unavailable
</span>
) : (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-emerald-600 uppercase tracking-wide">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" /> Live · {orders.length} deliveries · {executives.length} riders
</span>
)}
</div>
</div>
</div>
{/* Top Level Delivery Performance Indicators */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-gutter font-sans">
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl flex items-center gap-md shadow-sm">
<div className="p-2 bg-purple-50 text-[#581c87] rounded-lg">
<ShoppingBag size={20} />
</div>
<div>
<p className="text-[10px] text-zinc-400 uppercase tracking-wider font-bold">Deliveries in Range</p>
<p className="font-sans font-bold text-lg text-zinc-800">{totalDeliveriesCount.toLocaleString('en-IN')} total</p>
<p className="text-[10px] text-emerald-600 font-semibold mt-0.5">{fromdate === todate ? fromdate : `${fromdate}${todate}`}</p>
</div>
</div>
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl flex items-center gap-md shadow-sm">
<div className="p-2 bg-amber-50 text-amber-600 rounded-lg">
<Clock size={20} />
</div>
<div>
<p className="text-[10px] text-zinc-400 uppercase tracking-wider font-bold">Pending Fulfilment</p>
<p className="font-sans font-bold text-lg text-zinc-800">
{pendingFulfillmentCount + activeDispatchCount} active
</p>
<p className="text-[10px] text-amber-600 font-semibold mt-0.5">Awaiting dispatch / in transit</p>
</div>
</div>
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl flex items-center gap-md shadow-sm">
<div className="p-2 bg-emerald-50 text-emerald-600 rounded-lg">
<Truck size={20} />
</div>
<div>
<p className="text-[10px] text-zinc-400 uppercase tracking-wider font-bold">Successful Deliveries</p>
<p className="font-sans font-bold text-lg text-zinc-800">
{completedDeliveriesCount} done
</p>
<p className="text-[10px] text-[#581c87] font-semibold mt-0.5">{locationid ? 'At this location' : 'Across all locations'}</p>
</div>
</div>
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl flex items-center gap-md shadow-sm">
<div className="p-2 bg-purple-50 text-purple-600 rounded-lg">
<UserCheck size={20} />
</div>
<div>
<p className="text-[10px] text-zinc-400 uppercase tracking-wider font-bold">Active Delivery Fleet</p>
<p className="font-sans font-bold text-lg text-zinc-800">
{executives.filter(e => e.status !== 'Offline').length} partners
</p>
<p className="text-[10px] text-purple-600 font-semibold mt-0.5">{executives.length} riders registered</p>
</div>
</div>
</div>
{/* Day-wise date filter — drives the live deliveries + summary queries */}
<div className="bg-white border border-[#e2e8f0] rounded-xl p-md shadow-sm flex flex-col lg:flex-row lg:items-center justify-between gap-md">
<div className="flex items-center gap-sm flex-wrap">
<span className="flex items-center gap-1.5 text-[10px] font-bold text-zinc-400 uppercase tracking-widest pr-1">
<Calendar size={13} className="text-[#581c87]" /> View
</span>
{presets.map((p) => (
<button
key={p.key}
onClick={() => { setFromdate(p.from); setTodate(p.to); }}
className={`px-3 py-1.5 rounded-lg text-[11px] font-bold transition-all border cursor-pointer ${
activePreset === p.key
? 'bg-[#581c87] text-white border-[#581c87] shadow-sm'
: 'bg-white text-zinc-600 border-[#e2e8f0] hover:bg-zinc-50'
}`}
>
{p.label}
</button>
))}
</div>
<div className="flex items-center gap-sm text-xs">
<div className="flex items-center gap-1.5">
<label className="text-[10px] font-bold text-zinc-400 uppercase tracking-wider">From</label>
<input
type="date"
value={fromdate}
max={todate}
onChange={(e) => setFromdate(e.target.value)}
className="border border-[#e2e8f0] rounded-lg px-2 py-1.5 text-zinc-700 font-medium outline-none focus:ring-1 focus:ring-[#581c87] cursor-pointer"
/>
</div>
<span className="text-zinc-300"></span>
<div className="flex items-center gap-1.5">
<label className="text-[10px] font-bold text-zinc-400 uppercase tracking-wider">To</label>
<input
type="date"
value={todate}
min={fromdate}
max={ymd(today)}
onChange={(e) => setTodate(e.target.value)}
className="border border-[#e2e8f0] rounded-lg px-2 py-1.5 text-zinc-700 font-medium outline-none focus:ring-1 focus:ring-[#581c87] cursor-pointer"
/>
</div>
</div>
</div>
{/* Main interactive segment splits */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-gutter text-xs font-sans">
{/* Left List of Customer App Orders */}
<div className="lg:col-span-2 space-y-md">
<div className="bg-white border border-[#e2e8f0] rounded-xl shadow-sm overflow-hidden flex flex-col justify-between">
<div>
<div className="p-md border-b border-[#e2e8f0] bg-[#f8fafc] flex flex-col gap-md">
<div className="flex flex-col sm:flex-row justify-between sm:items-center gap-sm">
<div>
<h4 className="font-sans font-bold text-sm text-[#0f172a]">
Customer Orders Feed ({filteredOrdersList.length})
</h4>
<p className="text-[10px] text-zinc-400 font-medium mt-0.5">Interactive list of customer purchases made via client app</p>
</div>
<button
onClick={handleCreateMockOrder}
className="bg-[#581c87] text-white px-3 py-1.5 rounded-lg text-[10px] font-bold uppercase tracking-wider flex items-center justify-center gap-1 cursor-pointer hover:bg-purple-800 transition shadow-sm"
>
<Plus size={12} />
Create Simulated Order
</button>
</div>
<div className="flex flex-col sm:flex-row items-center gap-sm w-full">
{/* Local Search Input */}
<div className="relative w-full sm:max-w-xs">
<Search size={13} className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" />
<input
type="text"
placeholder="Search orders by customer, street, ID..."
value={localSearch}
onChange={(e) => setLocalSearch(e.target.value)}
className="w-full pl-8 pr-4 py-1.5 border border-[#e2e8f0] rounded-lg text-[11px] outline-none bg-white focus:ring-1 focus:ring-[#581c87] transition-all"
/>
</div>
{/* Filter Status buttons */}
<div className="flex gap-1 overflow-x-auto w-full sm:w-auto">
{['ALL', 'PROCESSING', 'CONFIRMED', 'OUT_FOR_DELIVERY', 'DELIVERED'].map((st) => (
<button
key={st}
onClick={() => setFilterStatus(st)}
className={`px-2 py-1.5 rounded text-[9px] font-bold uppercase transition-all border outline-none cursor-pointer whitespace-nowrap ${
filterStatus === st
? 'bg-[#581c87] text-white border-[#581c87] shadow-sm'
: 'bg-white text-zinc-500 border-[#e2e8f0] hover:bg-zinc-50'
}`}
>
{st.replace(/_/g, ' ')}
</button>
))}
</div>
</div>
</div>
{/* Order item rows */}
<div className="divide-y divide-[#f1f5f9] max-h-[480px] overflow-y-auto">
{filteredOrdersList.length === 0 ? (
<div className="p-xl text-center text-zinc-400 font-medium">
No orders matching status filter found. Try another query or place a mock delivery item.
</div>
) : (
filteredOrdersList.map(order => (
<div
key={order.id}
onClick={() => setSelectedOrder(order)}
className={`p-md flex items-center justify-between hover:bg-zinc-50 border-l-4 transition-all cursor-pointer ${
selectedOrder?.id === order.id ? 'bg-[#faf5ff]/50 border-[#581c87]' : 'border-transparent'
}`}
>
<div className="space-y-1">
<div className="flex items-center gap-sm">
<span className="font-bold text-zinc-700">{order.customerName}</span>
<span className="text-[10px] text-zinc-400"> {order.time}</span>
</div>
<p className="text-zinc-500 truncate max-w-[24rem]">{order.address}</p>
<div className="flex gap-sm py-1 items-center">
<span className="bg-[#f1f5f9] px-1.5 py-0.5 rounded text-[9px] font-bold text-zinc-500 uppercase">{order.hub}</span>
<span className="text-[9px] text-[#581c87] font-bold">{order.itemCount ?? order.items.length} Items</span>
</div>
</div>
<div className="text-right space-y-1">
<p className="font-bold font-mono text-sm text-[#0f172a]">{order.amount.toLocaleString()}</p>
<span className={`px-2 py-0.5 rounded text-[9px] font-bold tracking-wider inline-block uppercase ${
order.status === 'DELIVERED'
? 'bg-emerald-50 text-emerald-600 border border-emerald-100'
: order.status === 'OUT_FOR_DELIVERY'
? 'bg-purple-50 text-purple-700 border border-purple-100'
: order.status === 'CONFIRMED'
? 'bg-amber-50 text-amber-600 border border-amber-100 animate-pulse'
: 'bg-zinc-100 text-zinc-650 border border-zinc-200'
}`}>
{order.status.replace(/_/g, ' ')}
</span>
</div>
</div>
))
)}
</div>
</div>
</div>
{/* Delivery Executives Fleet Section */}
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl shadow-sm">
<span className="text-[10px] font-sans font-bold text-zinc-400 uppercase tracking-widest block pb-xs mb-md border-b border-[#f1f5f9]">
Coimbatore Delivery Executive Fleet status
</span>
<div className="grid grid-cols-1 md:grid-cols-2 gap-sm">
{executives.map((ex) => (
<div key={ex.id} className="p-sm border border-[#e2e8f0]/80 rounded-xl bg-[#f8fafc]/40 flex justify-between items-center">
<div className="flex items-center gap-sm">
<img
src={ex.avatar}
alt={ex.name}
referrerPolicy="no-referrer"
className="w-10 h-10 rounded-full object-cover border border-zinc-200 shrink-0"
/>
<div>
<p className="font-semibold text-zinc-800">{ex.name}</p>
<p className="text-[10px] text-zinc-400 font-medium">Zone: <strong>{ex.currentZone}</strong> Rated {ex.rating}</p>
</div>
</div>
<div className="text-right">
<span className={`px-1.5 py-0.5 rounded text-[9px] font-bold uppercase inline-block ${
ex.status === 'Active Duty' ? 'bg-sky-50 text-sky-600 border border-sky-100' : ex.status === 'Idle' ? 'bg-emerald-50 text-emerald-600 border border-emerald-100' : 'bg-zinc-100 text-zinc-400'
}`}>
{ex.status}
</span>
<p className="text-[10px] text-zinc-500 font-semibold mt-1">Completed: {ex.completedToday}</p>
</div>
</div>
))}
</div>
</div>
{selectedOrder ? (
<div className="bg-white border border-[#e2e8f0] rounded-xl p-md shadow-sm space-y-md animate-in zoom-in-95 duration-150">
<span className="text-[10px] font-sans font-bold text-zinc-400 uppercase tracking-widest block pb-xs border-b border-[#f1f5f9]">
Order Details: {selectedOrder.id}
</span>
{/* Customer summary */}
<div className="p-sm bg-[#f8fafc] rounded-lg border border-[#e2e8f0]/50 space-y-xs">
<div className="flex justify-between font-semibold">
<span>Customer Name</span>
<span className="text-zinc-700">{selectedOrder.customerName}</span>
</div>
<div className="flex justify-between font-semibold">
<span>Contact info</span>
<span className="text-zinc-600 font-mono">{selectedOrder.phone}</span>
</div>
<div>
<span className="text-[10px] text-zinc-400 font-bold uppercase block mt-1">Delivery Address</span>
<p className="text-zinc-700 mt-0.5 leading-relaxed font-medium">{selectedOrder.address}</p>
</div>
</div>
{/* Category items description list */}
<div>
<span className="text-[10px] text-zinc-400 font-bold uppercase tracking-wide block mb-sm">Ordered Grocery basket Items:</span>
<div className="divide-y divide-[#f1f5f9] bg-zinc-50/50 p-2.5 rounded-lg border border-[#e2e8f0]/40">
{selectedOrder.items.length === 0 && (
<div className="py-2 flex justify-between items-center text-xs text-zinc-500">
<span className="font-medium">{selectedOrder.itemCount ?? 0} line item(s)</span>
<span className="text-[10px] text-zinc-400">Detail lines not loaded on board view</span>
</div>
)}
{selectedOrder.items.map((item, idx) => (
<div key={idx} className="py-2 flex justify-between items-center text-xs">
<div>
<p className="font-bold text-[#0f172a]">{item.name}</p>
<p className="text-[10px] text-zinc-400">Qty: {item.quantity} x {item.price}</p>
</div>
<span className="font-bold font-mono text-zinc-700">{(item.price * Number(item.quantity))}</span>
</div>
))}
<div className="pt-2 flex justify-between items-center font-bold text-sm text-[#581c87] border-t border-dashed border-[#e2e8f0]">
<span>Grand Total Invoice</span>
<span className="font-mono">{selectedOrder.amount.toLocaleString()}</span>
</div>
</div>
</div>
{/* Interactive Status advancement controls */}
<div className="pt-xs space-y-sm">
<span className="text-[9px] font-sans font-bold text-zinc-400 uppercase tracking-widest block pb-xs border-b border-[#f1f5f9]">OPERATIONAL CONTROL</span>
{selectedOrder.status === 'PROCESSING' && (
<button
onClick={() => handleUpdateStatus('CONFIRMED')}
className="w-full bg-amber-500 hover:bg-amber-600 text-white font-bold text-[11px] py-2 rounded-xl transition duration-150 shadow-sm flex items-center justify-center gap-1 cursor-pointer border-none"
>
<Check size={14} /> Pack & Bag Order
</button>
)}
{selectedOrder.status === 'CONFIRMED' && (
<button
onClick={() => {
if (selectedOrder.assignedRider === 'Pending Assignment') {
alert('Please assign a delivery partner from the fleet roster first.');
return;
}
handleUpdateStatus('OUT_FOR_DELIVERY');
}}
className="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold text-[11px] py-2 rounded-xl transition duration-150 shadow-sm flex items-center justify-center gap-1 cursor-pointer border-none"
>
<Truck size={14} /> Dispatch Rider
</button>
)}
{selectedOrder.status === 'OUT_FOR_DELIVERY' && (
<button
onClick={() => handleUpdateStatus('DELIVERED')}
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white font-bold text-[11px] py-2 rounded-xl transition duration-150 shadow-sm flex items-center justify-center gap-1 cursor-pointer border-none"
>
<CheckCircle2 size={14} /> Verify Delivery Handover
</button>
)}
{selectedOrder.status === 'DELIVERED' && (
<div className="bg-emerald-50 border border-emerald-250 text-emerald-800 font-bold text-[10px] py-2.5 rounded-xl text-center flex items-center justify-center gap-1 select-none">
<CheckCircle2 size={13} className="text-emerald-600" /> Order Completed Successfully
</div>
)}
</div>
{/* Active Rider Assignment (only if not delivered) */}
{selectedOrder.status !== 'DELIVERED' && (
<div className="space-y-sm pt-xs">
<div className="flex justify-between items-center border-b border-[#f1f5f9] pb-xs">
<span className="text-[9px] font-sans font-bold text-zinc-400 uppercase tracking-widest">ASSIGN DELIVERY EXECUTIVE</span>
<span className="text-[9px] text-[#581c87] font-bold">Fleet Roster</span>
</div>
<div className="space-y-1.5 max-h-[140px] overflow-y-auto pr-1">
{executives.length === 0 ? (
<p className="text-[10px] text-zinc-405">No riders currently available.</p>
) : (
executives.map(ex => {
const isAssigned = selectedOrder.assignedRider === ex.name;
return (
<button
key={ex.id}
type="button"
onClick={() => handleAssignRider(ex.name)}
className={`w-full p-2 border rounded-xl flex items-center justify-between text-left transition-all cursor-pointer ${
isAssigned
? 'bg-purple-50 border-[#581c87] text-[#581c87] font-semibold'
: 'bg-[#f8fafc]/50 hover:bg-zinc-55 border-[#e2e8f0] text-zinc-700'
}`}
>
<div className="flex items-center gap-2">
<img src={ex.avatar} alt={ex.name} referrerPolicy="no-referrer" className="w-6 h-6 rounded-full object-cover border border-zinc-200" />
<div>
<p className="text-[10px] font-bold leading-tight">{ex.name}</p>
<p className="text-[9px] text-zinc-450 leading-none">{ex.currentZone} {ex.rating}</p>
</div>
</div>
<span className={`text-[8px] font-bold uppercase px-1.5 py-0.5 rounded ${
isAssigned
? 'bg-[#581c87] text-white'
: 'bg-zinc-200 text-zinc-650'
}`}>
{isAssigned ? 'Assigned' : 'Assign'}
</span>
</button>
);
})
)}
</div>
</div>
)}
{/* Simulated GPS map tracking path */}
{selectedOrder.status === 'OUT_FOR_DELIVERY' && (
<div className="space-y-xs pt-xs">
<span className="text-[9px] font-sans font-bold text-zinc-400 uppercase tracking-widest block">
LIVE GPS ROUTE TRACKER
</span>
<div className="relative overflow-hidden rounded-xl border border-zinc-200 bg-zinc-950 p-4 h-40 text-white flex flex-col justify-between font-sans shadow-inner select-none">
{/* Grid background lines */}
<div className="absolute inset-0 opacity-10 bg-[linear-gradient(to_right,#808080_1px,transparent_1px),linear-gradient(to_bottom,#808080_1px,transparent_1px)] bg-[size:12px_18px]" />
<svg className="absolute inset-0 w-full h-full" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="route-grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#c084fc" />
<stop offset="100%" stopColor="#818cf8" />
</linearGradient>
</defs>
{/* Route path line */}
<path
d="M 30 110 C 60 70, 110 110, 160 40"
fill="none"
stroke="#1e293b"
strokeWidth="4"
strokeLinecap="round"
/>
<path
d="M 30 110 C 60 70, 110 110, 160 40"
fill="none"
stroke="url(#route-grad)"
strokeWidth="4"
strokeLinecap="round"
strokeDasharray="200"
strokeDashoffset="200"
style={{
animation: 'dash 6s linear infinite'
}}
/>
{/* Hub Marker */}
<circle cx="30" cy="110" r="5" fill="#c084fc" className="animate-pulse" />
<circle cx="30" cy="110" r="3" fill="#a855f7" />
{/* Destination Marker */}
<circle cx="160" cy="40" r="5" fill="#f43f5e" className="animate-ping" />
<circle cx="160" cy="40" r="3" fill="#e11d48" />
</svg>
<style dangerouslySetInnerHTML={{__html: `
@keyframes dash {
to {
stroke-dashoffset: 0;
}
}
`}} />
{/* Map overlays */}
<div className="z-10 flex justify-between items-start">
<div className="bg-zinc-900/90 backdrop-blur-md px-2 py-0.5 rounded border border-zinc-800 text-[8px] font-bold text-zinc-300">
GPS ACTIVE: IN TRANSIT
</div>
<div className="bg-zinc-900/90 backdrop-blur-md px-2 py-0.5 rounded border border-zinc-800 text-[8px] font-bold text-[#c084fc] flex items-center gap-1">
<span className="w-1 h-1 rounded-full bg-purple-500 animate-ping" />
ETA 9 MINS
</div>
</div>
<div className="z-10 bg-zinc-900/95 backdrop-blur-md p-2 rounded-lg border border-zinc-800 flex items-center justify-between">
<div>
<p className="text-[8px] text-zinc-400 font-bold uppercase tracking-wider">Executive</p>
<p className="text-[10px] font-bold text-white leading-tight">{selectedOrder.assignedRider}</p>
</div>
<div className="text-right">
<p className="text-[8px] text-zinc-400 font-bold uppercase tracking-wider">Distance</p>
<p className="text-[10px] font-bold text-[#c084fc] font-mono leading-tight">1.2 km left</p>
</div>
</div>
</div>
</div>
)}
{/* Delivery tracking visual roadmap layout */}
<div className="bg-zinc-50 border border-[#e2e8f0]/60 rounded-xl p-md">
<span className="text-[9px] font-sans font-bold text-zinc-400 uppercase tracking-widest block pb-xs mb-sm border-b border-[#f1f5f9]">
Live Dispatch Timeline Tracker
</span>
<div className="space-y-xs pt-1 relative text-[11px]">
<div className="flex gap-md items-start relative group">
<span className="text-emerald-500 mt-0.5"><CheckCircle2 size={12} /></span>
<div>
<h5 className="font-semibold text-zinc-800">Order Received ({selectedOrder.time})</h5>
<p className="text-[10px] text-zinc-400">Placed via customer app cart checkout successfully.</p>
</div>
</div>
<div className="flex gap-md items-start pt-3">
<span className={['CONFIRMED', 'OUT_FOR_DELIVERY', 'DELIVERED'].includes(selectedOrder.status) ? 'text-emerald-500 mt-0.5' : 'text-zinc-300 mt-0.5'}><CheckCircle2 size={12} /></span>
<div>
<h5 className="font-semibold text-zinc-800">Assortment Packaged & Bagged</h5>
<p className="text-[10px] text-zinc-400">Verified fresh produce items in-stock levels.</p>
</div>
</div>
<div className="flex gap-md items-start pt-3">
<span className={['OUT_FOR_DELIVERY', 'DELIVERED'].includes(selectedOrder.status) ? 'text-emerald-500 mt-0.5' : 'text-zinc-300 mt-0.5'}><CheckCircle2 size={12} /></span>
<div>
<h5 className="font-semibold text-zinc-800">Out for Delivery</h5>
<p className="text-[10px] text-zinc-400">Dispatched with executive partner on bike route.</p>
</div>
</div>
<div className="flex gap-md items-start pt-3">
<span className={selectedOrder.status === 'DELIVERED' ? 'text-emerald-500 mt-0.5' : 'text-zinc-300 mt-0.5'}><CheckCircle2 size={12} /></span>
<div>
<h5 className="font-semibold text-zinc-800">Handover Verified</h5>
<p className="text-[10px] text-zinc-400">Delivered directly to door step location.</p>
</div>
</div>
</div>
</div>
</div>
) : (
<div className="p-xl bg-white border border-[#e2e8f0] rounded-xl text-center text-zinc-400 font-medium">
Select any customer order from the feed to view its details.
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,478 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useState } from 'react';
import {
TrendingUp,
TrendingDown,
Download,
Filter,
ArrowUpRight,
ChevronDown,
TrendingUp as TrendUp,
TrendingDown as TrendDown,
Equal as TrendFlat,
ChevronLeft,
ChevronRight,
Info
} from 'lucide-react';
import { LeaderboardNode } from '../types';
import {
useFiestaOrderSummary,
useFiestaLocationSummary,
useFiestaOrderInsight,
useFiestaStockStatement,
} from '../services/fiestaQueries';
import { FIESTA_TENANT_ID, FIESTA_PRIMARY_LOCATION_ID, num as fnum, str as fstr, ymd } from '../services/fiestaApi';
import { stockRowToProduct } from '../services/fiestaMappers';
interface ReportsViewProps {
searchQuery: string;
isCoimbatoreView: boolean;
}
const MONTH_KEYS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dece'];
const MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
export default function ReportsView({ searchQuery, isCoimbatoreView }: ReportsViewProps) {
const [selectedTimeframe, setSelectedTimeframe] = useState('Month to Date');
const [stockFilter, setStockFilter] = useState<'All' | 'Healthy' | 'Low Stock' | 'Critical'>('All');
const [selectedCell, setSelectedCell] = useState<{ day: string; hour: string; val: number } | null>(null);
const [currentPage, setCurrentPage] = useState(1);
// ── Live analytics (Fiesta) ───────────────────────────────────────────────
const today = new Date();
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
const yearStart = new Date(today.getFullYear(), 0, 1);
const fromdate = ymd(monthStart);
const todate = ymd(today);
const summaryQ = useFiestaOrderSummary(FIESTA_TENANT_ID, ymd(yearStart), todate);
const locSummaryQ = useFiestaLocationSummary(FIESTA_TENANT_ID);
const insightQ = useFiestaOrderInsight(FIESTA_TENANT_ID);
const stockQ = useFiestaStockStatement({
tenantid: FIESTA_TENANT_ID,
locationid: FIESTA_PRIMARY_LOCATION_ID,
keyword: '',
pageno: 1,
pagesize: 100,
});
const s = summaryQ.data;
const activeSkus = (stockQ.data ?? []).length;
// KPI row — all live.
const reportsKPIs = [
{
title: 'Total Orders (YTD)',
value: (s?.total ?? 0).toLocaleString('en-IN'),
subtext: `${ymd(yearStart)}${todate}`,
trend: `${s?.delivered ?? 0} delivered`,
isPositive: true,
},
{
title: 'Delivered',
value: (s?.delivered ?? 0).toLocaleString('en-IN'),
subtext: `${s ? Math.round(((s.delivered) / Math.max(s.total, 1)) * 100) : 0}% of all orders`,
trend: `${s?.pending ?? 0} pending`,
isPositive: true,
},
{
title: 'Cancelled',
value: (s?.cancelled ?? 0).toLocaleString('en-IN'),
subtext: `${s ? Math.round(((s.cancelled) / Math.max(s.total, 1)) * 100) : 0}% of all orders`,
trend: `${s?.created ?? 0} created`,
isPositive: false,
},
{
title: 'Active SKUs',
value: activeSkus.toLocaleString('en-IN'),
subtext: 'Live stock statement entries',
trend: 'In catalogue',
isPositive: true,
},
];
// Leaderboard — outlets ranked by total live orders.
const leaderboard: LeaderboardNode[] = (() => {
const rows = [...(locSummaryQ.data ?? [])].sort((a, b) => b.total - a.total).slice(0, 4);
const max = rows.length ? rows[0].total : 0;
return rows.map((r, i) => ({
rank: String(i + 1).padStart(2, '0'),
name: r.locationname || `Location ${r.locationid}`,
percentage: max > 0 ? Math.round((r.total / max) * 100) : 0,
revenue: `${r.total.toLocaleString('en-IN')} ord`,
}));
})();
const currentLeaderboard = leaderboard;
// Monthly order distribution per outlet (replaces the static hourly heatmap).
const insightRows = (insightQ.data ?? []).map((r) => ({
name: fstr(r.locationname) || `Location ${fstr(r.locationid)}`,
months: (r.ordermonths ?? {}) as Record<string, unknown>,
}));
const heatmapMax = Math.max(
1,
...insightRows.flatMap((row) => MONTH_KEYS.map((k) => fnum(row.months[k]))),
);
// Live product performance matrix.
const liveProducts = (stockQ.data ?? []).map(stockRowToProduct);
const filteredProducts = liveProducts.filter((prod) => {
const matchesSearch =
prod.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
prod.sku.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStock = stockFilter === 'All' ? true : prod.stockStatus === stockFilter;
return matchesSearch && matchesStock;
});
// Heatmap cell colour, scaled relative to the busiest month/outlet.
const getHeatmapColorClass = (val: number) => {
const ratio = val / heatmapMax;
if (ratio < 0.15) return 'bg-[#581c87]/10 text-[#0f172a] hover:bg-[#581c87]/20';
if (ratio <= 0.5) return 'bg-[#a78bfa]/40 text-[#581c87] hover:bg-[#a78bfa]/50';
return 'bg-[#581c87] text-white hover:bg-purple-800';
};
// Export alerts
const triggerExport = (format: 'PDF' | 'CSV') => {
alert(`BI Engine initiating automated ${format} bundle export. Generating compiled schema reports...`);
};
return (
<div className="space-y-lg animate-in fade-in duration-500">
{/* Context filter header row */}
<div className="flex flex-col md:flex-row md:items-end justify-between gap-md bg-[#f8fafc] border border-[#e2e8f0] p-md rounded-xl shadow-sm">
<div>
<h1 className="font-sans font-bold text-2xl tracking-tight text-[#0f172a]">
Business Intelligence Center
</h1>
<p className="text-zinc-500 font-sans text-xs mt-1">
Real-time analytical metrics engine surfacing regional performance deltas and potential logistic constraints.
</p>
</div>
{/* Action picker filters */}
<div className="flex items-center gap-sm flex-wrap text-xs">
<div className="bg-white border border-[#e2e8f0] rounded-lg px-sm py-1.5 flex items-center gap-sm shadow-sm select-none">
<span className="text-zinc-400 font-medium">Timeframe:</span>
<select
value={selectedTimeframe}
onChange={(e) => setSelectedTimeframe(e.target.value)}
className="bg-transparent border-none focus:ring-0 font-sans font-semibold text-zinc-700 cursor-pointer outline-none"
>
<option>Month to Date</option>
<option>Year to Date</option>
<option>Last 12 Months</option>
</select>
</div>
<div className="bg-white border border-[#e2e8f0] rounded-lg px-sm py-1.5 flex items-center gap-sm shadow-sm font-semibold text-zinc-700">
<Filter size={14} className="text-zinc-400 font-medium" />
<span>{isCoimbatoreView ? 'Coimbatore Zones (5)' : 'All Regions (12)'}</span>
</div>
<button
onClick={() => triggerExport('PDF')}
className="bg-[#581c87] text-white font-sans font-semibold px-4 py-1.5 rounded-lg flex items-center gap-sm cursor-pointer transition-colors hover:bg-purple-800 active:bg-purple-900 shadow-sm"
>
<Download size={13} />
Export PDF
</button>
</div>
</div>
{/* Primary KPI Row - 4 Key cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-gutter mb-xl text-xs">
{reportsKPIs.map((kpi, idx) => (
<div key={idx} className="bg-white border border-[#e2e8f0] rounded-xl p-md shadow-sm">
<div className="flex justify-between items-start mb-sm">
<span className="text-[11px] font-sans font-bold text-zinc-400 uppercase tracking-widest block font-sans">
{kpi.title}
</span>
<span className={`text-[10px] font-bold px-1.5 py-0.5 rounded flex items-center gap-px ${
kpi.isPositive ? 'bg-emerald-50 text-emerald-600' : 'bg-rose-50 text-rose-600'
}`}>
{kpi.isPositive ? <TrendingUp size={10} /> : <TrendingDown size={10} />}
{kpi.trend}
</span>
</div>
<div className="font-sans font-bold text-[#0f172a] text-xl tracking-tight">
{kpi.value}
</div>
<div className="text-[10px] text-zinc-400 font-medium tracking-wide mt-sm uppercase font-sans">
{kpi.subtext}
</div>
</div>
))}
</div>
{/* Bento split maps */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-gutter">
{/* Revenue Heatmap table - 8 Cols */}
<div className="lg:col-span-8 bg-white border border-[#e2e8f0] rounded-xl overflow-hidden flex flex-col justify-between shadow-sm">
<div className="bg-[#f8fafc] border-b border-[#e2e8f0] px-md py-sm flex justify-between items-center">
<span className="text-[11px] font-sans font-bold text-[#0f172a] uppercase tracking-widest block">
Monthly Order Distribution by Outlet
</span>
<div className="flex items-center gap-2 text-[10px] font-bold text-zinc-400 uppercase tracking-tight">
<span className="w-2 h-2 rounded-full bg-[#581c87] animate-pulse"></span>
<span>Busiest Month</span>
</div>
</div>
<div className="p-md flex-1 overflow-x-auto">
{selectedCell && (
<div className="mb-sm p-sm bg-purple-50 border border-purple-100 rounded-lg text-xs flex justify-between items-center animate-in fade-in">
<span className="font-sans text-blue-900 font-medium">
<strong className="font-bold">{selectedCell.day}</strong> registered <strong className="font-bold font-mono">{selectedCell.val}</strong> order(s) in <strong className="font-bold">{selectedCell.hour}</strong>.
</span>
<button
onClick={() => setSelectedCell(null)}
className="text-xs font-bold text-[#581c87] hover:underline cursor-pointer"
>
Dismiss
</button>
</div>
)}
{insightRows.length === 0 ? (
<div className="py-xl text-center text-zinc-400 text-xs">
{insightQ.isLoading ? 'Loading monthly order distribution…' : 'No order insight available for this tenant.'}
</div>
) : (
<table className="w-full text-center border-collapse text-xs font-sans">
<thead>
<tr>
<th className="p-xs text-[10px] font-bold text-zinc-400 w-32 uppercase italic">Outlet</th>
{MONTH_LABELS.map((m) => (
<th key={m} className="p-sm text-[10px] font-bold text-[#0f172a] border-b border-[#f1f5f9]">
{m}
</th>
))}
</tr>
</thead>
<tbody>
{insightRows.map((row, idx) => (
<tr key={idx}>
<td className="p-sm text-left font-bold text-[#0f172a] tracking-wide truncate max-w-[8rem] border-r border-[#f1f5f9]">
{row.name}
</td>
{MONTH_KEYS.map((key, mIdx) => {
const val = fnum(row.months[key]);
return (
<td key={key} className="p-1 border border-white">
<button
onClick={() => setSelectedCell({ day: row.name, hour: MONTH_LABELS[mIdx], val })}
className={`w-full py-2.5 rounded-lg font-semibold transition-all duration-100 cursor-pointer ${getHeatmapColorClass(val)}`}
>
{val}
</button>
</td>
);
})}
</tr>
))}
</tbody>
</table>
)}
</div>
<div className="px-md py-sm bg-[#f8fafc] border-t border-[#e2e8f0] flex justify-between text-[11px] text-zinc-400 font-sans font-medium">
<span>Colour intensity scales with monthly order volume per outlet</span>
<span>Click cells to inspect metrics</span>
</div>
</div>
{/* Leaderboard nodes bar list - 4 Cols */}
<div className="lg:col-span-4 bg-white border border-[#e2e8f0] rounded-xl flex flex-col shadow-sm mt-0">
<div className="bg-[#f8fafc] border-b border-[#e2e8f0] px-md py-sm">
<span className="text-[11px] font-sans font-bold text-[#0f172a] uppercase tracking-widest block">
Top Performing Nodes
</span>
</div>
<div className="p-md flex-1 space-y-md flex flex-col justify-center">
{currentLeaderboard.map((node) => (
<div key={node.rank} className="flex items-center gap-md text-xs">
<span className="text-xs font-bold text-zinc-400 font-mono w-4">
{node.rank}
</span>
<div className="flex-1">
<div className="flex justify-between items-center font-medium">
<span className="text-[#0f172a] font-semibold">{node.name}</span>
<span className="text-[#581c87] font-mono font-bold">{node.revenue}</span>
</div>
<div className="w-full bg-[#eceef0] h-1.5 rounded-full mt-1 overflow-hidden">
<div
className="bg-[#581c87] h-full rounded-full transition-all duration-300"
style={{ width: `${node.percentage}%` }}
/>
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Detailed Performance Matrix table */}
<div className="bg-white border border-[#e2e8f0] rounded-xl overflow-hidden shadow-sm">
{/* Table header with filters control */}
<div className="bg-[#f8fafc] border-b border-[#e2e8f0] p-md flex flex-col sm:flex-row justify-between items-start sm:items-center gap-sm">
<div>
<h3 className="font-sans font-bold text-sm text-[#0f172a]">
Product Performance Matrix
</h3>
<p className="text-zinc-500 text-xs font-sans mt-0.5">
Assortment unit sales and physical balance trend indices.
</p>
</div>
{/* Quick interactive filter pills */}
<div className="flex gap-2 text-xs font-semibold">
{(['All', 'Healthy', 'Low Stock', 'Critical'] as const).map((filter) => (
<button
key={filter}
onClick={() => setStockFilter(filter)}
className={`px-3 py-1 rounded-lg cursor-pointer transition-colors ${
stockFilter === filter
? 'bg-[#581c87] text-white shadow-sm'
: 'bg-white border border-[#e2e8f0] text-zinc-600 hover:bg-zinc-50'
}`}
>
{filter}
</button>
))}
<button
onClick={() => triggerExport('CSV')}
className="px-3 py-1 bg-[#0f172a] text-white rounded-lg cursor-pointer hover:bg-zinc-800 transition-colors shadow-sm"
>
Export CSV
</button>
</div>
</div>
{/* Matrix Data table */}
<div className="overflow-x-auto text-xs font-sans">
<table className="w-full text-left">
<thead className="bg-[#f8fafc] border-b border-[#e2e8f0] text-zinc-500 text-[10px] uppercase font-bold tracking-wider">
<tr>
<th className="px-md py-sm">Product Name</th>
<th className="px-md py-sm">SKU ID</th>
<th className="px-md py-sm text-right">Units Sold</th>
<th className="px-md py-sm text-right">Revenue</th>
<th className="px-md py-sm text-right">Stock Status</th>
<th className="px-md py-sm text-center">Trend index</th>
</tr>
</thead>
<tbody className="divide-y divide-[#f1f5f9]">
{filteredProducts.length === 0 ? (
<tr>
<td colSpan={6} className="text-center py-8 text-zinc-400">
No matching items matching stock filter criteria.
</td>
</tr>
) : (
filteredProducts.map((prod) => (
<tr key={prod.id} className="hover:bg-[#f2f4f6]/40 transition-all">
<td className="px-md py-md flex items-center gap-sm">
<div className="w-8 h-8 rounded-lg shrink-0 border border-[#e2e8f0] overflow-hidden bg-zinc-50">
<img
src={prod.image}
alt={prod.name}
referrerPolicy="no-referrer"
className="w-full h-full object-cover"
/>
</div>
<span className="font-semibold text-[#0f172a]">{prod.name}</span>
</td>
<td className="px-md py-md font-mono text-zinc-500 font-medium">
{prod.sku}
</td>
<td className="px-md py-md text-right font-mono text-zinc-600 font-semibold">
{prod.unitsSold.toLocaleString()}
</td>
<td className="px-md py-md text-right font-mono text-zinc-700 font-bold">
{prod.revenue.toLocaleString()}
</td>
<td className="px-md py-md text-right">
<span className={`px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-tight ${
prod.stockStatus === 'Healthy'
? 'bg-emerald-100 text-emerald-750 font-bold font-sans'
: prod.stockStatus === 'Low Stock'
? 'bg-amber-100 text-amber-750 font-bold font-sans'
: 'bg-rose-100 text-rose-750 font-bold font-sans'
}`}>
{prod.stockStatus}
</span>
</td>
<td className="px-md py-md text-center">
<span className="inline-block p-1 bg-[#f8fafc] border border-zinc-100 rounded-full">
{prod.trend === 'up' ? (
<TrendUp size={14} className="text-emerald-500" />
) : prod.trend === 'down' ? (
<TrendDown size={14} className="text-rose-500" />
) : (
<TrendFlat size={14} className="text-zinc-400" />
)}
</span>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Matrix table pagination */}
<div className="p-md bg-[#f8fafc] border-t border-[#e2e8f0] flex justify-between items-center text-[10px] text-zinc-500 font-semibold font-sans">
<span>Showing 1-{filteredProducts.length} of {liveProducts.length} live products</span>
<div className="flex gap-xs">
<button
onClick={() => 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"
>
<ChevronLeft size={12} />
</button>
<button className={`w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center font-bold text-[10px] bg-[#0f172a] text-white`}>
1
</button>
<button
onClick={() => 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-500 font-bold font-mono text-[10px] cursor-pointer"
>
2
</button>
<button
onClick={() => 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-500 font-bold font-mono text-[10px] cursor-pointer"
>
3
</button>
<button
onClick={() => 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"
>
<ChevronRight size={12} />
</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,511 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useEffect, useMemo, useRef, useState } from 'react';
import {
Building2,
Store,
Truck,
CreditCard,
SlidersHorizontal,
MapPin,
Phone,
Mail,
Check,
RotateCcw,
CheckCircle2,
} from 'lucide-react';
import { useFiestaAllTenants, useFiestaTenantLocations } from '../services/fiestaQueries';
import { FIESTA_TENANT_ID, str as fstr, num as fnum, roleName } from '../services/fiestaApi';
interface SettingsViewProps {
tenantId?: number;
}
type TabKey = 'profile' | 'outlets' | 'delivery' | 'payment' | 'preferences';
/** Locally-persisted merchant preferences (survive reload via localStorage). */
interface MerchantSettings {
// Business profile (seeded from live tenant data, then locally editable)
contactEmail: string;
contactPhone: string;
minOrderValue: number;
// Delivery
deliveryCharge: number;
prepMins: number;
deliveryWindowMins: number;
cancelWindowSecs: number;
autoAssignRider: boolean;
// Payment & tax
defaultTaxPercent: number;
codEnabled: boolean;
onlinePaymentEnabled: boolean;
// Preferences
defaultRegion: string;
defaultNewUserRole: number;
orderNotifications: boolean;
lowStockAlerts: boolean;
dailySummaryEmail: boolean;
syncInterval: number;
sandboxMode: boolean;
}
const STORAGE_KEY = 'merchant-settings-v1';
const DEFAULTS: MerchantSettings = {
contactEmail: '',
contactPhone: '',
minOrderValue: 0,
deliveryCharge: 30,
prepMins: 15,
deliveryWindowMins: 45,
cancelWindowSecs: 60,
autoAssignRider: true,
defaultTaxPercent: 5,
codEnabled: true,
onlinePaymentEnabled: true,
defaultRegion: 'Coimbatore',
defaultNewUserRole: 4,
orderNotifications: true,
lowStockAlerts: true,
dailySummaryEmail: false,
syncInterval: 5,
sandboxMode: false,
};
function loadSettings(): { settings: MerchantSettings; hadSaved: boolean } {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) return { settings: { ...DEFAULTS, ...JSON.parse(raw) }, hadSaved: true };
} catch {
/* ignore corrupt storage */
}
return { settings: { ...DEFAULTS }, hadSaved: false };
}
// ── Small presentational helpers ────────────────────────────────────────────
function Toggle({ checked, onChange }: { checked: boolean; onChange: () => void }) {
return (
<label className="relative inline-flex items-center cursor-pointer shrink-0">
<input type="checkbox" checked={checked} onChange={onChange} className="sr-only peer" />
<div className="w-9 h-5 bg-zinc-200 rounded-full peer peer-focus:ring-0 peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-zinc-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-[#581c87]" />
</label>
);
}
function Row({
title,
desc,
children,
}: {
title: string;
desc?: string;
children: React.ReactNode;
}) {
return (
<div className="flex justify-between items-center gap-md p-sm bg-zinc-50 rounded-lg border border-[#e2e8f0]/45">
<div className="min-w-0">
<h4 className="font-sans font-bold text-xs text-[#0f172a]">{title}</h4>
{desc && <p className="text-zinc-400 text-[10px] mt-xs">{desc}</p>}
</div>
<div className="shrink-0">{children}</div>
</div>
);
}
const numberInputCls =
'w-24 border border-[#e2e8f0] rounded-lg p-1.5 text-right font-semibold text-zinc-700 bg-white outline-none focus:ring-1 focus:ring-[#581c87]';
const textInputCls =
'w-full border border-[#e2e8f0] rounded-lg p-sm bg-white outline-none focus:ring-1 focus:ring-[#581c87] text-zinc-700 font-medium';
const selectCls =
'border border-[#e2e8f0] bg-white rounded-lg p-1.5 font-semibold text-zinc-700 outline-none cursor-pointer';
export default function SettingsView({ tenantId = FIESTA_TENANT_ID }: SettingsViewProps) {
const [activeTab, setActiveTab] = useState<TabKey>('profile');
// Live tenant profile + outlets.
const tenantsQ = useFiestaAllTenants({ pagesize: 50 });
const tenant = (tenantsQ.data ?? []).find((t) => Number(t.tenantid) === tenantId) || null;
const locationsQ = useFiestaTenantLocations(tenantId);
const outlets = locationsQ.data ?? [];
// Persisted preferences.
const initial = useRef(loadSettings());
const [form, setForm] = useState<MerchantSettings>(initial.current.settings);
const [saved, setSaved] = useState<MerchantSettings>(initial.current.settings);
const [toast, setToast] = useState<string | null>(null);
// First-run seeding: if nothing was saved yet, fill contact/min-order/region
// from the live tenant once it arrives.
const seededRef = useRef(initial.current.hadSaved);
useEffect(() => {
if (seededRef.current || !tenant) return;
seededRef.current = true;
const seed = (prev: MerchantSettings): MerchantSettings => ({
...prev,
contactEmail: prev.contactEmail || fstr(tenant.primaryemail),
contactPhone: prev.contactPhone || fstr(tenant.primarycontact),
minOrderValue: prev.minOrderValue || fnum(tenant.minorder),
defaultRegion: prev.defaultRegion || fstr(tenant.city) || 'Coimbatore',
});
setForm(seed);
setSaved(seed);
}, [tenant]);
const dirty = useMemo(() => JSON.stringify(form) !== JSON.stringify(saved), [form, saved]);
const set = <K extends keyof MerchantSettings>(key: K, value: MerchantSettings[K]) =>
setForm((f) => ({ ...f, [key]: value }));
const handleSave = () => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(form));
} catch {
/* ignore quota errors */
}
setSaved(form);
setToast('Settings saved');
window.setTimeout(() => setToast(null), 2200);
};
const handleReset = () => setForm(saved);
const tabs: Array<{ key: TabKey; label: string; icon: typeof Building2 }> = [
{ key: 'profile', label: 'Business Profile', icon: Building2 },
{ key: 'outlets', label: 'Outlets', icon: Store },
{ key: 'delivery', label: 'Delivery', icon: Truck },
{ key: 'payment', label: 'Payment & Tax', icon: CreditCard },
{ key: 'preferences', label: 'Preferences', icon: SlidersHorizontal },
];
const roleOptions = [1, 2, 3, 4, 6];
return (
<div className="space-y-lg animate-in fade-in duration-300 relative">
{/* Header */}
<div>
<h1 className="font-sans font-bold text-2xl tracking-tight text-[#0f172a]">Settings</h1>
<p className="text-zinc-500 font-sans text-xs mt-1">
Manage your store profile, outlets, delivery, payments, and workspace preferences.
</p>
<div className="mt-1.5">
{tenantsQ.isLoading ? (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-zinc-400 uppercase tracking-wide">
<span className="w-1.5 h-1.5 rounded-full bg-zinc-300 animate-pulse" /> Loading live profile
</span>
) : tenant ? (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-emerald-600 uppercase tracking-wide">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" /> Live · {fstr(tenant.tenantname)} · Tenant {tenantId}
</span>
) : (
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-rose-600 uppercase tracking-wide">
<span className="w-1.5 h-1.5 rounded-full bg-rose-500" /> Tenant profile unavailable
</span>
)}
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-gutter items-start">
{/* Tab rail */}
<nav className="lg:col-span-1 bg-white border border-[#e2e8f0] rounded-xl p-2 shadow-sm flex lg:flex-col gap-1 overflow-x-auto">
{tabs.map((t) => {
const Icon = t.icon;
const active = activeTab === t.key;
return (
<button
key={t.key}
onClick={() => setActiveTab(t.key)}
className={`flex items-center gap-sm px-sm py-2 rounded-lg text-xs font-semibold transition-colors whitespace-nowrap cursor-pointer ${
active ? 'bg-[#faf5ff] text-[#581c87]' : 'text-zinc-600 hover:bg-zinc-50'
}`}
>
<Icon size={15} className={active ? 'text-[#581c87]' : 'text-zinc-400'} />
{t.label}
</button>
);
})}
</nav>
{/* Panel */}
<div className="lg:col-span-3 space-y-gutter text-xs font-sans">
{activeTab === 'profile' && (
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl shadow-sm space-y-md">
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest block pb-xs border-b border-[#f1f5f9]">
Business Profile
</span>
{/* Live identity (read-only) */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-sm">
<div className="p-sm bg-zinc-50 rounded-lg border border-[#e2e8f0]/45">
<span className="text-[9px] text-zinc-400 uppercase font-bold tracking-widest">Store Name</span>
<p className="font-bold text-[#0f172a] mt-0.5">{fstr(tenant?.tenantname) || '—'}</p>
</div>
<div className="p-sm bg-zinc-50 rounded-lg border border-[#e2e8f0]/45">
<span className="text-[9px] text-zinc-400 uppercase font-bold tracking-widest">Legal / Company</span>
<p className="font-bold text-[#0f172a] mt-0.5">{fstr(tenant?.companyname) || '—'}</p>
</div>
<div className="p-sm bg-zinc-50 rounded-lg border border-[#e2e8f0]/45">
<span className="text-[9px] text-zinc-400 uppercase font-bold tracking-widest">Category</span>
<p className="font-bold text-[#0f172a] mt-0.5">{fstr(tenant?.subcategoryname) || `Category ${fnum(tenant?.categoryid)}`}</p>
</div>
<div className="p-sm bg-zinc-50 rounded-lg border border-[#e2e8f0]/45 flex items-center justify-between">
<div>
<span className="text-[9px] text-zinc-400 uppercase font-bold tracking-widest">Account Status</span>
<p className="font-bold text-[#0f172a] mt-0.5">{fstr(tenant?.status) || '—'}</p>
</div>
<span className={`px-1.5 py-0.5 rounded text-[9px] font-bold uppercase ${
fstr(tenant?.status).toLowerCase() === 'active'
? 'text-emerald-700 bg-emerald-100'
: 'text-zinc-500 bg-zinc-200'
}`}>
{fnum(tenant?.approved) === 1 ? 'Approved' : 'Pending'}
</span>
</div>
</div>
<div className="p-sm bg-zinc-50 rounded-lg border border-[#e2e8f0]/45 flex items-start gap-sm">
<MapPin size={13} className="text-zinc-400 shrink-0 mt-0.5" />
<div>
<span className="text-[9px] text-zinc-400 uppercase font-bold tracking-widest">Registered Address</span>
<p className="text-zinc-700 font-medium mt-0.5 leading-relaxed">
{fstr(tenant?.address) || '—'}
{tenant?.city ? ` · ${fstr(tenant.city)}, ${fstr(tenant.state)} ${fstr(tenant.postcode)}` : ''}
</p>
</div>
</div>
{/* Editable contact (persisted locally) */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-sm pt-xs">
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px] flex items-center gap-1">
<Mail size={11} /> Contact Email
</label>
<input
type="email"
value={form.contactEmail}
onChange={(e) => set('contactEmail', e.target.value)}
className={textInputCls}
placeholder="store@example.com"
/>
</div>
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px] flex items-center gap-1">
<Phone size={11} /> Contact Phone
</label>
<input
type="text"
value={form.contactPhone}
onChange={(e) => set('contactPhone', e.target.value)}
className={textInputCls}
placeholder="9876543210"
/>
</div>
</div>
<p className="text-[10px] text-zinc-400">
Identity fields above are read live from your tenant record. Contact details are saved to this workspace.
</p>
</div>
)}
{activeTab === 'outlets' && (
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl shadow-sm space-y-md">
<div className="flex justify-between items-center pb-xs border-b border-[#f1f5f9]">
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest">Outlet Locations</span>
<span className="text-[10px] text-[#581c87] font-bold bg-purple-50 px-2 py-0.5 rounded border border-purple-100">
{locationsQ.isLoading ? 'Loading…' : `${outlets.length} outlet${outlets.length === 1 ? '' : 's'}`}
</span>
</div>
{locationsQ.isLoading ? (
<div className="text-center py-lg text-zinc-400">Loading live outlets</div>
) : outlets.length === 0 ? (
<div className="text-center py-lg text-zinc-400">No outlets found for this tenant.</div>
) : (
<div className="space-y-sm max-h-[28rem] overflow-y-auto">
{outlets.map((loc, i) => (
<div key={fstr(loc.locationid) || i} className="p-sm border border-[#e2e8f0] rounded-lg bg-[#f8fafc]/40">
<div className="flex justify-between items-start gap-md">
<div className="min-w-0">
<p className="font-bold text-[#0f172a] truncate">{fstr(loc.locationname)}</p>
<p className="text-[10px] text-zinc-500 mt-0.5 flex items-center gap-1">
<MapPin size={10} className="shrink-0 text-zinc-400" />
<span className="truncate">{fstr(loc.suburb)}, {fstr(loc.city)} {fstr(loc.postcode)}</span>
</p>
</div>
<span className={`shrink-0 px-1.5 py-0.5 rounded text-[9px] font-bold uppercase ${
fstr(loc.status).toLowerCase() === 'active'
? 'text-emerald-600 bg-emerald-50 border border-emerald-100'
: 'text-zinc-500 bg-zinc-100'
}`}>
{fstr(loc.status) || '—'}
</span>
</div>
<div className="grid grid-cols-3 gap-2 mt-sm text-center">
<div className="bg-white border border-[#e2e8f0]/60 rounded p-1">
<p className="text-[9px] text-zinc-400 uppercase font-bold">Hours</p>
<p className="font-mono font-semibold text-zinc-700 text-[10px] mt-0.5">
{fstr(loc.opentime).slice(11, 16) || '—'}{fstr(loc.closetime).slice(11, 16) || '—'}
</p>
</div>
<div className="bg-white border border-[#e2e8f0]/60 rounded p-1">
<p className="text-[9px] text-zinc-400 uppercase font-bold">Radius</p>
<p className="font-mono font-semibold text-zinc-700 text-[10px] mt-0.5">{fnum(loc.deliveryradius)} m</p>
</div>
<div className="bg-white border border-[#e2e8f0]/60 rounded p-1">
<p className="text-[9px] text-zinc-400 uppercase font-bold">ETA</p>
<p className="font-mono font-semibold text-zinc-700 text-[10px] mt-0.5">{fnum(loc.deliverymins)} min</p>
</div>
</div>
</div>
))}
</div>
)}
<p className="text-[10px] text-zinc-400">Outlets are read live from your tenant. Add or edit them in the Stores section.</p>
</div>
)}
{activeTab === 'delivery' && (
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl shadow-sm space-y-md">
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest block pb-xs border-b border-[#f1f5f9]">
Delivery Settings
</span>
<div className="space-y-sm">
<Row title="Default Delivery Charge" desc="Flat fee added to each delivery order.">
<div className="flex items-center gap-1">
<span className="text-zinc-400 font-bold"></span>
<input type="number" min={0} value={form.deliveryCharge}
onChange={(e) => set('deliveryCharge', Number(e.target.value))} className={numberInputCls} />
</div>
</Row>
<Row title="Preparation Time" desc="Minutes a store needs before pickup.">
<input type="number" min={0} value={form.prepMins}
onChange={(e) => set('prepMins', Number(e.target.value))} className={numberInputCls} />
</Row>
<Row title="Delivery Window" desc="Target minutes from dispatch to doorstep.">
<input type="number" min={0} value={form.deliveryWindowMins}
onChange={(e) => set('deliveryWindowMins', Number(e.target.value))} className={numberInputCls} />
</Row>
<Row title="Cancellation Window" desc="Seconds a customer can cancel for free.">
<input type="number" min={0} value={form.cancelWindowSecs}
onChange={(e) => set('cancelWindowSecs', Number(e.target.value))} className={numberInputCls} />
</Row>
<Row title="Auto-assign Rider" desc="Automatically dispatch the nearest available rider.">
<Toggle checked={form.autoAssignRider} onChange={() => set('autoAssignRider', !form.autoAssignRider)} />
</Row>
</div>
</div>
)}
{activeTab === 'payment' && (
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl shadow-sm space-y-md">
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest block pb-xs border-b border-[#f1f5f9]">
Payment & Tax
</span>
<div className="space-y-sm">
<Row title="Default Tax Rate" desc="Applied to taxable catalogue items.">
<div className="flex items-center gap-1">
<input type="number" min={0} max={100} value={form.defaultTaxPercent}
onChange={(e) => set('defaultTaxPercent', Number(e.target.value))} className={numberInputCls} />
<span className="text-zinc-400 font-bold">%</span>
</div>
</Row>
<Row title="Minimum Order Value" desc="Smallest order a customer can place.">
<div className="flex items-center gap-1">
<span className="text-zinc-400 font-bold"></span>
<input type="number" min={0} value={form.minOrderValue}
onChange={(e) => set('minOrderValue', Number(e.target.value))} className={numberInputCls} />
</div>
</Row>
<Row title="Cash on Delivery" desc="Allow customers to pay on delivery.">
<Toggle checked={form.codEnabled} onChange={() => set('codEnabled', !form.codEnabled)} />
</Row>
<Row title="Online Payments" desc="Accept UPI / card / wallet at checkout.">
<Toggle checked={form.onlinePaymentEnabled} onChange={() => set('onlinePaymentEnabled', !form.onlinePaymentEnabled)} />
</Row>
<div className="p-sm bg-purple-50 border border-purple-100 rounded-lg text-[#581c87] text-[11px] font-medium">
Live tenant payment configuration code: <strong>{fnum(tenant?.paymenttype) || '—'}</strong>
</div>
</div>
</div>
)}
{activeTab === 'preferences' && (
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl shadow-sm space-y-md">
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest block pb-xs border-b border-[#f1f5f9]">
Workspace Preferences
</span>
<div className="space-y-sm">
<Row title="Default Region" desc="Region applied to new outlets and reports.">
<input type="text" value={form.defaultRegion}
onChange={(e) => set('defaultRegion', e.target.value)} className={`${numberInputCls} w-40 text-left`} />
</Row>
<Row title="Default Role for New Users" desc="Pre-selected role in the Add User dialog.">
<select value={form.defaultNewUserRole}
onChange={(e) => set('defaultNewUserRole', Number(e.target.value))} className={selectCls}>
{roleOptions.map((r) => (
<option key={r} value={r}>{roleName(r)}</option>
))}
</select>
</Row>
<Row title="Data Sync Interval" desc="How often live data refreshes from the API.">
<select value={form.syncInterval}
onChange={(e) => set('syncInterval', Number(e.target.value))} className={selectCls}>
<option value={1}>Every 1 min</option>
<option value={5}>Every 5 mins</option>
<option value={15}>Every 15 mins</option>
<option value={30}>Every 30 mins</option>
</select>
</Row>
<Row title="Order Notifications" desc="Alert on every new incoming order.">
<Toggle checked={form.orderNotifications} onChange={() => set('orderNotifications', !form.orderNotifications)} />
</Row>
<Row title="Low-stock Alerts" desc="Notify when an SKU drops below threshold.">
<Toggle checked={form.lowStockAlerts} onChange={() => set('lowStockAlerts', !form.lowStockAlerts)} />
</Row>
<Row title="Daily Summary Email" desc="Email a closing-hours performance digest.">
<Toggle checked={form.dailySummaryEmail} onChange={() => set('dailySummaryEmail', !form.dailySummaryEmail)} />
</Row>
<Row title="Sandbox Mode" desc="Simulate warning states without affecting live ops.">
<Toggle checked={form.sandboxMode} onChange={() => set('sandboxMode', !form.sandboxMode)} />
</Row>
</div>
</div>
)}
{/* Save / Reset — lives with the settings card, not pinned to the screen */}
<div className="bg-white border border-[#e2e8f0] rounded-xl p-md shadow-sm flex flex-col sm:flex-row sm:items-center justify-between gap-sm">
<span className={`text-xs font-medium ${dirty ? 'text-amber-600' : 'text-zinc-400'}`}>
{dirty ? '● You have unsaved changes' : 'All changes saved'}
</span>
<div className="flex gap-sm">
<button
onClick={handleReset}
disabled={!dirty}
className="px-4 py-2 border border-[#e2e8f0] rounded-lg text-xs font-semibold text-zinc-600 hover:bg-zinc-50 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
>
<RotateCcw size={13} /> Reset
</button>
<button
onClick={handleSave}
disabled={!dirty}
className="px-4 py-2 bg-[#581c87] text-white rounded-lg text-xs font-bold hover:bg-purple-800 cursor-pointer shadow-sm disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
>
<Check size={13} /> Save Changes
</button>
</div>
</div>
</div>
</div>
{/* Toast */}
{toast && (
<div className="fixed bottom-md right-md z-[130] bg-[#0f172a] text-white px-4 py-2.5 rounded-lg shadow-2xl flex items-center gap-2 text-xs font-semibold animate-in slide-in-from-bottom-2 fade-in duration-200">
<CheckCircle2 size={15} className="text-emerald-400" />
{toast}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,73 @@
/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import React from 'react';
import {
LayoutDashboard,
Store,
Layers,
ShoppingBag,
Users,
Settings
} from 'lucide-react';
import { MainSection } from '../types';
interface SidebarProps {
currentSection: MainSection;
setCurrentSection: (section: MainSection) => void;
isCoimbatoreView: boolean;
setIsCoimbatoreView: (val: boolean) => void;
isOpen: boolean;
}
export default function Sidebar({
currentSection,
setCurrentSection,
isCoimbatoreView,
setIsCoimbatoreView,
isOpen
}: SidebarProps) {
// Navigation elements
const navItems = [
{ id: 'dashboard' as MainSection, label: 'Dashboard', icon: LayoutDashboard },
{ id: 'stores' as MainSection, label: 'Stores', icon: Store },
{ id: 'inventory' as MainSection, label: 'Inventory Catalog', icon: Layers },
{ id: 'users' as MainSection, label: 'Users', icon: Users },
{ id: 'settings' as MainSection, label: 'Settings', icon: Settings }
];
return (
<aside
className={`fixed left-0 top-0 h-screen bg-[#581c87] border-r border-[#4c1d95] text-white flex-col py-xl pt-24 z-40 hidden md:flex transition-all duration-300 ${
isOpen ? 'w-64' : 'w-20'
}`}
>
{/* Main Navigation Sidebar Links */}
<nav className="flex-1 space-y-1 overflow-y-auto px-xs">
{navItems.map((item) => {
const IconComponent = item.icon;
const isActive = currentSection === item.id;
return (
<button
key={item.id}
onClick={() => setCurrentSection(item.id)}
title={item.label}
className={`w-full flex items-center py-3 rounded-lg text-left transition-all duration-200 cursor-pointer ${
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'
}`}
>
<IconComponent size={18} className={isActive ? 'text-white' : 'text-purple-300'} />
{isOpen && <span className="font-sans text-sm font-medium">{item.label}</span>}
</button>
);
})}
</nav>
</aside>
);
}

File diff suppressed because it is too large Load Diff