Files
daily_merchant_web/src/App.tsx
2026-07-20 18:54:09 +05:30

825 lines
38 KiB
TypeScript

/**
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useState, useEffect } from 'react';
import { Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom';
import {
Network,
Truck,
Sliders,
Calendar,
FileCheck,
Building,
CheckCircle2,
Clock,
ShieldCheck,
Send,
HelpCircle,
Database,
ArrowRight,
X,
Search,
Plus,
MapPin,
Phone,
Activity,
TrendingUp,
Award,
Layers,
Store,
Settings,
LayoutDashboard,
Users,
Box
} from 'lucide-react';
import { MainSection } from './types';
import {
useFiestaTenantLocations,
useFiestaLocationSummary,
useFiestaUpdateLocation,
useFiestaCreateLocation,
useFiestaOrderSummary,
} from './services/fiestaQueries';
import { FIESTA_TENANT_ID, FIESTA_PRIMARY_LOCATION_ID, str as fstr, num as fnum } from './services/fiestaApi';
import Sidebar from './components/Sidebar';
import Header from './components/Header';
import DashboardView from './components/DashboardView';
import ReportsView from './components/ReportsView';
import InventoryView from './components/InventoryView';
import SettingsView from './components/SettingsView';
import StoreDetailView from './components/StoreDetailView';
import DispatchHubView from './components/DispatchHubView';
import LoginView from './components/LoginView';
import UserStorePage from './components/UserStorePage';
import SuperAdminPage from './components/SuperAdminPage';
import AwaitingApi from './components/AwaitingApi';
import ComparisonModal from './components/ComparisonModal';
import CatalogueBrowser from './components/CatalogueBrowser';
import type { AuthUser } from './services/auth';
import ragulStoreCover from './assets/images/store_front_view_1780299351800.png';
const AUTH_STORAGE_KEY = 'nearledaily.auth';
/** Rehydrate the signed-in user from localStorage (keeps the session across refreshes). */
function loadStoredUser(): AuthUser | null {
try {
const raw = localStorage.getItem(AUTH_STORAGE_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw) as AuthUser;
return parsed && typeof parsed === 'object' && (parsed.role === 'admin' || parsed.role === 'user')
? parsed
: null;
} catch {
return null;
}
}
export default function App() {
// Auth gate — null = signed out (show login). Admin → full dashboard, user →
// their single store console. The verified user is persisted to localStorage so
// a refresh keeps the session (rehydrated via the lazy initializer); authRole
// is derived from it so the two can't drift out of sync.
const [authUser, setAuthUser] = useState<AuthUser | null>(() => loadStoredUser());
const authRole = authUser?.role ?? null;
// Persist (or clear) the session whenever the signed-in user changes.
useEffect(() => {
try {
if (authUser) localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(authUser));
else localStorage.removeItem(AUTH_STORAGE_KEY);
} catch {
/* storage unavailable (private mode / quota) — session just won't persist */
}
}, [authUser]);
const handleLogin = (user: AuthUser) => setAuthUser(user);
const currentUser = {
name: authUser?.name || 'Account',
role: authRole === 'admin' ? 'Operations Admin' : 'Team Member',
email: authUser?.email || '—',
};
// Navigation indicators states
const location = useLocation();
const navigate = useNavigate();
// Derive current section from URL path, default to 'dashboard'
const pathParts = location.pathname.split('/');
const currentSection = (pathParts.includes('admin') ? pathParts[pathParts.length - 1] : 'dashboard') as MainSection;
const [selectedStore, setSelectedStore] = useState<{ locationid?: number; name: string; zone: string; deliveries: number; sales: string; orders?: number; staff: string; color: string; status: string } | null>(null);
const [isCoimbatoreView, setIsCoimbatoreView] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [sidebarOpen, setSidebarOpen] = useState(true);
// Scope every Fiesta query to the signed-in merchant. The login record carries
// the user's tenantid; fall back to the shared constant only when it's absent
// (e.g. a legacy session before tenantid was captured) so the page still loads.
const tenantId = authUser?.tenantid || FIESTA_TENANT_ID;
// ── Live data for the secondary sections (Fiesta) ─────────────────────────
// Stores ← tenant locations + per-location order summary (seeded into local
// state so the "Add Store" handler keeps working). Users management now lives
// under Settings → Users & Access (see UsersPanel).
const locationsQ = useFiestaTenantLocations(tenantId);
const locSummaryQ = useFiestaLocationSummary(tenantId);
const updateLocation = useFiestaUpdateLocation();
const today = new Date();
const monthStart = new Date(today);
monthStart.setDate(today.getDate() - 30);
const ymd = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
const fromdate = ymd(monthStart);
const todate = ymd(today);
const summaryQ = useFiestaOrderSummary(tenantId, fromdate, todate);
const STORE_COVERS = [
'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&w=600&q=80',
'https://images.unsplash.com/photo-1578916171728-46686eac8d58?auto=format&fit=crop&w=600&q=80',
'https://images.unsplash.com/photo-1604719312566-8912e9227c6a?auto=format&fit=crop&w=600&q=80',
'https://images.unsplash.com/photo-1534723452862-4c874018d66d?auto=format&fit=crop&w=600&q=80',
'https://images.unsplash.com/photo-1582408929130-98a2c2640b8a?auto=format&fit=crop&w=600&q=80',
'https://images.unsplash.com/photo-1516594798947-e65505dbb29d?auto=format&fit=crop&w=600&q=80',
'https://images.unsplash.com/photo-1601599561263-60a4e4e083cd?auto=format&fit=crop&w=600&q=80',
'https://images.unsplash.com/photo-1441986300917-64674bd600d8?auto=format&fit=crop&w=600&q=80',
'https://images.unsplash.com/photo-1528698827591-e19ccd7bc23d?auto=format&fit=crop&w=600&q=80',
'https://images.unsplash.com/photo-1536697246787-1f7ae568d89a?auto=format&fit=crop&w=600&q=80',
'https://images.unsplash.com/photo-1506617498306-bd97b3663b65?auto=format&fit=crop&w=600&q=80',
'https://images.unsplash.com/photo-1579621970563-ebec7560ff3e?auto=format&fit=crop&w=600&q=80',
];
const getStoreCover = (name: string) => {
if (name.toLowerCase().includes('ragul')) return ragulStoreCover;
let hash = 0;
for (let j = 0; j < name.length; j++) {
hash = name.charCodeAt(j) + ((hash << 5) - hash);
}
const idx = Math.abs(hash) % STORE_COVERS.length;
return STORE_COVERS[idx];
};
// Dynamic Secondary Modules list states (seeded from live data once it loads).
const [storesList, setStoresList] = useState<Array<{ locationid?: number; name: string; zone: string; deliveries: number; sales: string; orders: number; staff: string; color: string; status: string }>>([]);
const [storesSearch, setStoresSearch] = useState('');
const [storesFilter, setStoresFilter] = useState<'ALL' | 'ACTIVE' | 'INACTIVE'>('ALL');
const filteredStoresList = storesList.filter((st) => {
const q = storesSearch.trim().toLowerCase();
// Match across every field shown on the card — name, zone, manager/contact,
// and the outlet id — coercing each to a string so a missing/numeric value
// never throws and silently breaks the whole filter.
const haystack = [st.name, st.zone, st.staff, st.locationid]
.map((v) => String(v ?? '').toLowerCase())
.join(' ');
const matchesSearch = !q || haystack.includes(q);
if (storesFilter === 'ACTIVE') {
return matchesSearch && st.status.toLowerCase() === 'active';
}
if (storesFilter === 'INACTIVE') {
return (
matchesSearch &&
(st.status.toLowerCase() !== 'active')
);
}
return matchesSearch;
});
const activeCount = storesList.filter((st) => st.status.toLowerCase() === 'active').length;
const totalCount = storesList.length;
const totalDeliveries = storesList.reduce((acc, st) => acc + st.deliveries, 0);
useEffect(() => {
const locations = locationsQ.data ?? [];
const summaries = locSummaryQ.data ?? [];
if (locations.length) {
setStoresList(
locations.map((loc) => {
const sum = summaries.find((s) => s.locationid === Number(loc.locationid));
return {
locationid: Number(loc.locationid),
name: fstr(loc.locationname) || `Location ${fstr(loc.locationid)}`,
zone: [fstr(loc.suburb), fstr(loc.city)].filter(Boolean).join(', ') || 'Coimbatore',
deliveries: sum?.delivered ?? 0,
sales: `${(sum?.total ?? 0).toLocaleString('en-IN')} orders`,
orders: Math.max(sum?.delivered ?? 0, sum?.total ?? 0),
staff: fstr(loc.contactno) || fstr(loc.email) || '—',
color: fstr(loc.status).toLowerCase() === 'active' ? 'emerald' : 'amber',
status: fstr(loc.status) || 'Active',
};
}),
);
}
}, [locationsQ.data, locSummaryQ.data]);
// Secondary sub-sections modals triggers
const [showAddStoreModal, setShowAddStoreModal] = useState(false);
// New forms states
const [newStore, setNewStore] = useState({ name: '', zone: '', lead: '', sales: '₹1,50,000' });
// Form submission handles for secondary sections
const handleCreateStore = (e: React.FormEvent) => {
e.preventDefault();
if (!newStore.name || !newStore.zone || !newStore.lead) {
alert('Kindly fill store metadata completely.');
return;
}
setStoresList([...storesList, {
locationid: 10000 + Math.floor(Math.random() * 9000),
name: newStore.name,
zone: newStore.zone,
deliveries: 0,
sales: '0 orders',
orders: 0,
staff: newStore.lead,
color: 'emerald',
status: 'Active'
}]);
setShowAddStoreModal(false);
setNewStore({ name: '', zone: '', lead: '', sales: '₹1,50,000' });
alert(`Node outlet "${newStore.name}" commissioned to live operations feed successfully.`);
};
// Calendar Event Modal state
const [showCalendarModal, setShowCalendarModal] = useState(false);
// Callback action triggers
const handleHelp = () => {
alert('nearledaily User Manual & Documentation Center linked successfully. Contact Coimbatore regional IT hub desk for urgent escalations.');
};
const handleLogout = () => setAuthUser(null);
// Define secondary sections (Stores, Logistics, Staffing, Settings) within main body
const renderStoresSection = () => {
const isSoleStore = !selectedStore && storesList.length === 1;
const activeStore = selectedStore ?? (isSoleStore ? storesList[0] : null);
if (activeStore) {
return (
<div className="space-y-md animate-in fade-in duration-300">
{isSoleStore && (
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 pb-4 border-b border-zinc-205">
<div>
<h1 className="font-sans font-bold text-2xl tracking-tight text-[#0f172a]">
Store Console
</h1>
<p className="text-zinc-500 font-sans text-xs mt-1">
This merchant operates a single store. Add a branch to manage multiple outlets.
</p>
</div>
</div>
)}
<StoreDetailView
store={activeStore}
onBack={selectedStore ? () => setSelectedStore(null) : undefined}
tenantId={tenantId}
/>
</div>
);
}
return (
<div className="space-y-lg animate-in fade-in duration-300">
{/* ── Search & Filter Toolbar ── */}
<div className="flex flex-col lg:flex-row items-center gap-3 bg-white p-1.5 rounded-2xl border border-slate-200/70 shadow-sm mb-8 w-full">
{/* Search Input */}
<div className="relative flex-1 w-full group">
<div className="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-slate-400 group-focus-within:text-purple-600 transition-colors">
<Search className="w-5 h-5" />
</div>
<input
type="text"
placeholder="Search store by name, zone, or manager..."
value={storesSearch}
onChange={(e) => setStoresSearch(e.target.value)}
className="w-full pl-11 pr-12 py-3 bg-transparent border-none text-sm font-semibold text-slate-800 placeholder-slate-400 focus:outline-none focus:ring-0 transition-all duration-300"
/>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center">
{storesSearch && (
<button
onClick={() => setStoresSearch('')}
className="p-1.5 rounded-lg text-slate-400 hover:text-slate-600 hover:bg-slate-100 transition-colors"
>
<X className="w-4 h-4" />
</button>
)}
</div>
</div>
{/* Vertical Divider (Hidden on Mobile) */}
<div className="hidden lg:block w-px h-8 bg-slate-200" />
{/* Filter Segmented Control */}
<div className="flex items-center p-1 bg-slate-50/80 rounded-xl border border-slate-200/50 w-full lg:w-auto shrink-0">
<button
onClick={() => setStoresFilter('ALL')}
className={`flex-1 lg:flex-none px-5 py-2 rounded-lg text-xs font-bold transition-all duration-300 flex items-center justify-center gap-2 ${
storesFilter === 'ALL'
? 'bg-white text-slate-800 shadow-[0_1px_3px_rgba(0,0,0,0.1)] ring-1 ring-slate-900/5'
: 'text-slate-500 hover:text-slate-700 hover:bg-white/50'
}`}
>
All Nodes
<span className={`text-[10px] px-2 py-0.5 rounded-full font-bold ${storesFilter === 'ALL' ? 'bg-slate-100 text-slate-700' : 'bg-slate-200/50 text-slate-400'}`}>
{storesList.length}
</span>
</button>
<button
onClick={() => setStoresFilter('ACTIVE')}
className={`flex-1 lg:flex-none px-5 py-2 rounded-lg text-xs font-bold transition-all duration-300 flex items-center justify-center gap-2 ${
storesFilter === 'ACTIVE'
? 'bg-white text-emerald-600 shadow-[0_1px_3px_rgba(0,0,0,0.1)] ring-1 ring-emerald-500/20'
: 'text-slate-500 hover:text-emerald-600 hover:bg-emerald-50/50'
}`}
>
Active
<span className={`text-[10px] px-2 py-0.5 rounded-full font-bold ${storesFilter === 'ACTIVE' ? 'bg-emerald-50 text-emerald-600' : 'bg-slate-200/50 text-slate-400'}`}>
{storesList.filter(s => s.status.toLowerCase() === 'active').length}
</span>
</button>
<button
onClick={() => setStoresFilter('INACTIVE')}
className={`flex-1 lg:flex-none px-5 py-2 rounded-lg text-xs font-bold transition-all duration-300 flex items-center justify-center gap-2 ${
storesFilter === 'INACTIVE'
? 'bg-white text-slate-700 shadow-[0_1px_3px_rgba(0,0,0,0.1)] ring-1 ring-slate-900/5'
: 'text-slate-500 hover:text-slate-700 hover:bg-slate-200/50'
}`}
>
Inactive
<span className={`text-[10px] px-2 py-0.5 rounded-full font-bold ${storesFilter === 'INACTIVE' ? 'bg-slate-100 text-slate-700' : 'bg-slate-200/50 text-slate-400'}`}>
{storesList.filter(s => s.status.toLowerCase() !== 'active').length}
</span>
</button>
</div>
</div>
{/* Empty States */}
{filteredStoresList.length === 0 && (
<div className="text-center py-12 text-zinc-400 text-xs border border-dashed border-[#e2e8f0] rounded-xl bg-white p-8">
{locationsQ.isLoading ? (
<div className="flex flex-col items-center justify-center gap-2">
<div className="w-6 h-6 border-2 border-[#662582] border-t-transparent rounded-full animate-spin" />
<span>Loading live store locations</span>
</div>
) : (
<span>No store locations found matching your filter criteria.</span>
)}
</div>
)}
{/* Immersive Background Blur Blobs */}
<div className="relative">
<div className="absolute top-10 left-10 w-72 h-72 bg-purple-400/10 rounded-full blur-[100px] pointer-events-none -z-10 animate-pulse" />
<div className="absolute bottom-10 right-10 w-80 h-80 bg-indigo-400/10 rounded-full blur-[100px] pointer-events-none -z-10 animate-pulse" style={{ animationDuration: '6s' }} />
{/* Store Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-gutter relative z-10">
{filteredStoresList.map((st, i) => {
const totalOrders = st.orders ?? st.deliveries ?? 0;
const fulfillmentRate = totalOrders > 0 ? Math.min(100, Math.round((st.deliveries / totalOrders) * 100)) : 100;
return (
<div
key={st.locationid || i}
onClick={() => setSelectedStore(st)}
className={`group relative overflow-hidden bg-white/70 backdrop-blur-md border border-zinc-200/80 rounded-2xl shadow-sm hover:shadow-[0_20px_40px_rgba(88,28,135,0.12)] transition-all duration-500 cursor-pointer flex flex-col ${
st.status.toLowerCase() === 'active'
? st.deliveries > 40
? 'hover:border-rose-300'
: 'hover:border-emerald-300'
: 'hover:border-amber-300'
}`}
>
{/* Card Cover Image with Zoom effect */}
<div className="relative h-32 w-full overflow-hidden flex-shrink-0">
<img
src={getStoreCover(st.name)}
alt={st.name}
className="w-full h-full object-cover group-hover:scale-108 transition-transform duration-700 ease-out"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-900/90 via-slate-900/20 to-transparent" />
{/* Status Badge */}
<div className="absolute top-3 right-3">
<span className={`px-2.5 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider flex items-center gap-1 border backdrop-blur-md ${
st.status.toLowerCase() === 'active'
? st.deliveries > 40
? 'text-rose-200 bg-rose-950/60 border-rose-500/30'
: 'text-emerald-200 bg-emerald-950/60 border-emerald-500/30'
: 'text-amber-200 bg-amber-950/60 border-amber-500/30'
}`}>
<span className="w-1.5 h-1.5 rounded-full bg-current animate-pulse" />
{st.status.toLowerCase() === 'active' && st.deliveries > 40 ? 'High Load' : st.status}
</span>
</div>
{/* Zone & Title */}
<div className="absolute bottom-3 left-3 right-3 text-white">
<p className="text-[9px] text-purple-200 font-bold uppercase tracking-widest leading-none mb-1">{st.zone}</p>
<h3 className="font-sans font-bold text-sm leading-tight text-white group-hover:text-purple-200 transition-colors">{st.name}</h3>
</div>
</div>
{/* Card Content Area */}
<div className="p-5 flex-1 flex flex-col justify-between">
{/* Metrics Row & Progress Circle */}
<div className="flex items-center justify-between gap-4">
<div className="grid grid-cols-2 gap-4 flex-1">
<div>
<span className="text-[9px] text-zinc-400 uppercase tracking-widest font-bold block">Deliveries</span>
<p className="font-extrabold text-base text-slate-900 mt-0.5 font-mono">{st.deliveries.toLocaleString()}</p>
<span className="text-[8px] text-emerald-600 font-semibold block mt-0.5">Dispatched Today</span>
</div>
<div>
<span className="text-[9px] text-zinc-400 uppercase tracking-widest font-bold block">Total Orders</span>
<p className="font-extrabold text-base text-[#662582] mt-0.5 font-mono">{totalOrders.toLocaleString()}</p>
<span className="text-[8px] text-purple-600 font-semibold block mt-0.5">Incoming Volume</span>
</div>
</div>
{/* Circular Progress Ring */}
<div className="relative flex items-center justify-center flex-shrink-0" title={`Fulfillment Rate: ${fulfillmentRate}%`}>
<svg className="w-12 h-12 transform -rotate-90">
<circle
cx="24"
cy="24"
r="18"
className="stroke-zinc-100"
strokeWidth="3.5"
fill="transparent"
/>
<circle
cx="24"
cy="24"
r="18"
className={`transition-all duration-500 ${
st.status.toLowerCase() === 'active'
? st.deliveries > 40
? 'stroke-rose-500'
: 'stroke-emerald-500'
: 'stroke-amber-500'
}`}
strokeWidth="3.5"
fill="transparent"
strokeDasharray="113"
strokeDashoffset={113 - (113 * fulfillmentRate) / 100}
strokeLinecap="round"
/>
</svg>
<span className="absolute text-[9px] font-extrabold text-zinc-700 font-mono">
{fulfillmentRate}%
</span>
</div>
</div>
{/* Lead Manager Profile block */}
<div className="flex justify-between items-center text-xs text-zinc-650 bg-zinc-50/80 rounded-xl p-2.5 border border-zinc-100/80 mt-4">
<div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-full bg-gradient-to-tr from-purple-600 to-indigo-600 text-white flex items-center justify-center font-bold text-[10px] border border-white shadow-sm flex-shrink-0">
{st.staff.slice(0, 2).toUpperCase()}
</div>
<div className="min-w-0">
<span className="text-[8px] text-zinc-400 block font-semibold leading-none mb-0.5">Node Lead</span>
<span className="font-bold text-[#0f172a] text-xs truncate max-w-[100px] block leading-none">{st.staff}</span>
</div>
</div>
<button
onClick={(e) => {
e.stopPropagation();
alert(`Routing communications channel directly to manager ${st.staff}...`);
}}
className="p-1.5 rounded-lg bg-white border border-zinc-200 hover:border-[#662582] hover:text-[#662582] text-zinc-500 transition-colors shadow-sm"
title="Communicate with Node Lead"
>
<Phone className="w-3.5 h-3.5" />
</button>
</div>
{/* Card footer - enter console */}
<div className="flex items-center justify-between text-[10px] font-bold text-[#662582] mt-4 pt-3 border-t border-zinc-100/80">
<button
onClick={(e) => {
e.stopPropagation();
const newStatus = st.status.toLowerCase() === 'active' ? 'Inactive' : 'Active';
setStoresList(stores => stores.map(s => s.locationid === st.locationid ? { ...s, status: newStatus } : s));
if (st.locationid) {
updateLocation.mutate({ locationid: st.locationid, status: newStatus.toLowerCase() });
}
}}
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none ${st.status.toLowerCase() === 'active' ? 'bg-emerald-500' : 'bg-zinc-300'}`}
title={`Toggle store to ${st.status.toLowerCase() === 'active' ? 'Inactive' : 'Active'}`}
>
<span className={`inline-block h-3 w-3 transform rounded-full bg-white transition-transform ${st.status.toLowerCase() === 'active' ? 'translate-x-5' : 'translate-x-1'}`} />
</button>
<div className="flex items-center gap-1 uppercase tracking-wider">
<span>Enter Terminal Console</span>
<ArrowRight className="w-3.5 h-3.5 transform group-hover:translate-x-1.5 transition-transform duration-350" />
</div>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
);
};
const renderSecondarySection = () => {
switch (currentSection) {
case 'settings':
return <SettingsView tenantId={tenantId} user={authUser ?? undefined} />;
default:
return null;
}
};
// ── Auth gate ──────────────────────────────────────────────────────────────
if (authRole === null) {
return (
<Routes>
<Route path="/login" element={<LoginView onLogin={handleLogin} />} />
<Route path="*" element={<Navigate to="/login" replace />} />
</Routes>
);
}
if (authRole === 'user' && authUser) {
return (
<Routes>
<Route path="/store/*" element={<UserStorePage onLogout={handleLogout} user={authUser} />} />
<Route path="*" element={<Navigate to="/store/console" replace />} />
</Routes>
);
}
// Platform operator, not scoped to any tenant — never sees the merchant
// console (dashboard/inventory/catalogue/etc), only tenant onboarding.
if (authRole === 'super_admin' && authUser) {
return (
<Routes>
<Route path="/superadmin/*" element={<SuperAdminPage onLogout={handleLogout} user={authUser} />} />
<Route path="*" element={<Navigate to="/superadmin/onboarding" replace />} />
</Routes>
);
}
const AdminConsole = (
<div className="min-h-screen bg-[#f8fafc] text-[#0f172a] font-sans antialiased overflow-x-hidden">
{/* Navbar segment */}
<Header
isCoimbatoreView={isCoimbatoreView}
onToggleSidebar={() => setSidebarOpen((prev) => !prev)}
isSidebarOpen={sidebarOpen}
onHelpClick={handleHelp}
onLogoutClick={handleLogout}
profile={currentUser}
storeContext={{
storeName: currentSection === 'dashboard'
? (summaryQ.data?.tenantname ? `${summaryQ.data.tenantname} Admin` : 'Admin Console')
: currentSection === 'inventory'
? 'Products'
: currentSection === 'dispatch'
? 'Console'
: currentSection.charAt(0).toUpperCase() + currentSection.slice(1),
icon: currentSection === 'dashboard'
? LayoutDashboard
: currentSection === 'inventory'
? Layers
: currentSection === 'stores'
? Store
: currentSection === 'reports'
? TrendingUp
: currentSection === 'settings'
? Settings
: currentSection === 'dispatch'
? Truck
: undefined
}}
/>
{/* Main Container workspace layout splits */}
<div className="flex pt-16 w-full max-w-[100vw]">
{/* Interactive Left Rail */}
<Sidebar
isCoimbatoreView={isCoimbatoreView}
setIsCoimbatoreView={setIsCoimbatoreView}
isOpen={sidebarOpen}
isAdmin={authRole === 'admin'}
onClose={() => setSidebarOpen(false)}
/>
{/* Main core pages payload area */}
<main className={`flex-1 min-w-0 transition-all duration-300 ${sidebarOpen ? 'md:pl-64' : 'md:pl-16'} ${currentSection === 'inventory' || currentSection === 'dispatch' ? 'h-[calc(100vh-64px)] overflow-hidden' : 'min-h-[calc(100vh-64px)]'}`}>
<div className={`w-full transition-all duration-300 ${currentSection === 'dispatch' ? 'h-full overflow-hidden' : currentSection === 'inventory' ? 'h-full px-4 md:px-6 pt-2 pb-4 overflow-hidden' : currentSection === 'settings' ? 'p-4 md:p-6 space-y-lg' : 'p-container-margin md:p-xl space-y-lg'}`}>
<Routes>
<Route index element={<Navigate to="dashboard" replace />} />
<Route path="dashboard" element={
selectedStore ? (
renderStoresSection()
) : (
<div className="space-y-6">
<DashboardView searchQuery={searchQuery} isCoimbatoreView={isCoimbatoreView} tenantId={tenantId} />
<div>
{renderStoresSection()}
</div>
</div>
)
} />
<Route path="inventory" element={
<InventoryView
searchQuery={searchQuery}
isCoimbatoreView={isCoimbatoreView}
tenantId={tenantId}
isSidebarOpen={sidebarOpen}
/>
} />
<Route path="catalogue" element={<Navigate to="/admin/inventory" replace />} />
<Route path="reports" element={
<ReportsView
searchQuery={searchQuery}
isCoimbatoreView={isCoimbatoreView}
setIsCoimbatoreView={setIsCoimbatoreView}
tenantId={tenantId}
/>
} />
<Route path="settings" element={
<SettingsView tenantId={tenantId} user={authUser ?? undefined} />
} />
<Route path="dispatch" element={
<DispatchHubView tenantId={tenantId} />
} />
</Routes>
</div>
</main>
</div>
</div>
);
return (
<>
<Routes>
<Route path="/admin/*" element={AdminConsole} />
<Route path="*" element={<Navigate to="/admin/dashboard" replace />} />
</Routes>
{/* CALENDAR SCHEDULER DIALOG MODAL */}
{showCalendarModal && (
<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) setShowCalendarModal(false); }}
>
<div className="bg-white border border-[#e2e8f0] rounded-xl w-full max-w-[24rem] 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">
<Calendar size={15} className="text-[#662582]" />
Scheduled Reports Calendar
</h4>
<button
onClick={() => setShowCalendarModal(false)}
className="p-1 hover:bg-zinc-200 rounded-full text-zinc-400 cursor-pointer transition-colors"
>
<X size={16} />
</button>
</div>
<div className="p-md space-y-md overflow-y-auto flex-1">
<p className="text-zinc-500 leading-relaxed font-semibold">
Automated compliance summaries will appear here once scheduled-report exports are available.
</p>
<AwaitingApi label="Scheduled reports" api="[R13]" />
</div>
<div className="p-sm bg-[#f8fafc] border-t border-[#e2e8f0] flex justify-end shrink-0">
<button
onClick={() => setShowCalendarModal(false)}
className="px-4 py-2 bg-[#0f172a] text-white rounded-lg font-semibold hover:bg-zinc-800 cursor-pointer shadow-sm transition-colors"
>
Close Calendar
</button>
</div>
</div>
</div>
)}
{/* CREATE NEW STORE MODAL */}
{showAddStoreModal && (
<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) setShowAddStoreModal(false); }}
>
<div className="bg-white border border-[#e2e8f0] rounded-xl w-full max-w-[24rem] 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">
<Building size={15} className="text-[#662582]" />
Commission New Regional Store Node
</h4>
<button
onClick={() => setShowAddStoreModal(false)}
className="p-1 hover:bg-zinc-200 rounded-full text-zinc-400 cursor-pointer transition-colors"
>
<X size={16} />
</button>
</div>
<form onSubmit={handleCreateStore} 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]">STORE OUTLET NAME (*)</label>
<input
type="text"
placeholder="e.g. RS Puram Super Hub"
value={newStore.name}
onChange={(e) => setNewStore({ ...newStore, 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-[#662582]"
required
/>
</div>
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">LOCAL ZONE AREA (*)</label>
<input
type="text"
placeholder="e.g. Coimbatore North"
value={newStore.zone}
onChange={(e) => setNewStore({ ...newStore, zone: e.target.value })}
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
required
/>
</div>
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">OUTLET TEAM MANAGER (*)</label>
<input
type="text"
placeholder="e.g. Sridhar Sundaram"
value={newStore.lead}
onChange={(e) => setNewStore({ ...newStore, lead: e.target.value })}
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
required
/>
</div>
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">ESTIMATED INITIAL REVENUE</label>
<input
type="text"
placeholder="₹1,50,000"
value={newStore.sales}
onChange={(e) => setNewStore({ ...newStore, sales: e.target.value })}
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
/>
</div>
</div>
</div>
<div className="p-md border-t border-[#f1f5f9] flex justify-end gap-sm bg-[#f8fafc] shrink-0">
<button
type="button"
onClick={() => setShowAddStoreModal(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-[#662582] text-white rounded-lg font-bold hover:bg-purple-800 cursor-pointer shadow-sm"
>
Create Outlet Node
</button>
</div>
</form>
</div>
</div>
)}
<ComparisonModal />
</>
);
}