udpates on the ui changes on theprodut catalogs

This commit is contained in:
2026-06-16 20:12:45 +05:30
parent a0586dc11c
commit 2e9ffb15bb
22 changed files with 921 additions and 598 deletions

View File

@@ -25,13 +25,19 @@ import {
Phone,
Activity,
TrendingUp,
Award
Award,
Layers,
Store,
Settings,
LayoutDashboard
} from 'lucide-react';
import { MainSection } from './types';
import {
useFiestaTenantLocations,
useFiestaLocationSummary,
useFiestaUpdateLocation,
useFiestaOrderSummary,
} from './services/fiestaQueries';
import { FIESTA_TENANT_ID, str as fstr } from './services/fiestaApi';
import Sidebar from './components/Sidebar';
@@ -113,6 +119,15 @@ export default function App() {
// under Settings → Users & Access (see UsersPanel).
const locationsQ = useFiestaTenantLocations(tenantId);
const locSummaryQ = useFiestaLocationSummary(tenantId);
const updateLocation = useFiestaUpdateLocation();
const today = new Date();
const monthStart = new Date(today);
monthStart.setDate(today.getDate() - 30);
const ymd = (d: Date) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
const fromdate = ymd(monthStart);
const todate = ymd(today);
const summaryQ = useFiestaOrderSummary(tenantId, fromdate, todate);
const STORE_COVERS = [
'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&w=600&q=80',
@@ -143,7 +158,7 @@ export default function App() {
const [storesList, setStoresList] = useState<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' | 'CRITICAL'>('ALL');
const [storesFilter, setStoresFilter] = useState<'ALL' | 'ACTIVE' | 'INACTIVE'>('ALL');
const filteredStoresList = storesList.filter((st) => {
const q = storesSearch.trim().toLowerCase();
@@ -158,10 +173,10 @@ export default function App() {
if (storesFilter === 'ACTIVE') {
return matchesSearch && st.status.toLowerCase() === 'active';
}
if (storesFilter === 'CRITICAL') {
if (storesFilter === 'INACTIVE') {
return (
matchesSearch &&
(st.status.toLowerCase() !== 'active' || st.deliveries > 40)
(st.status.toLowerCase() !== 'active')
);
}
return matchesSearch;
@@ -257,13 +272,6 @@ export default function App() {
This merchant operates a single store. Add a branch to manage multiple outlets.
</p>
</div>
<button
onClick={() => setShowAddStoreModal(true)}
className="bg-[#581c87] text-white px-5 py-2.5 rounded-lg text-xs font-bold uppercase tracking-wider flex items-center justify-center gap-2 cursor-pointer hover:bg-purple-800 transition shadow-sm self-start md:self-auto"
>
<Plus className="w-4 h-4" />
Add Retail Outlet Node
</button>
</div>
)}
<StoreDetailView
@@ -287,14 +295,6 @@ export default function App() {
Local nodes registry, active manager assignments, and live dispatch and grocery delivery fulfillment statistics.
</p>
</div>
<button
onClick={() => setShowAddStoreModal(true)}
className="bg-[#581c87] text-white px-5 py-2.5 rounded-lg text-xs font-bold uppercase tracking-wider flex items-center justify-center gap-2 cursor-pointer hover:bg-purple-800 transition shadow-sm"
>
<Plus className="w-4 h-4" />
Add Retail Outlet Node
</button>
</div>
{/* Filter control bar */}
@@ -309,7 +309,7 @@ export default function App() {
placeholder="Search stores by name, zone, or manager..."
value={storesSearch}
onChange={(e) => setStoresSearch(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-zinc-50 border border-zinc-200 rounded-lg text-xs font-medium text-zinc-800 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-[#581c87]/20 focus:border-[#581c87] transition-all"
className="w-full pl-10 pr-4 py-2 bg-zinc-50 border border-zinc-200 rounded-lg text-xs font-medium text-zinc-800 placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-[#662582]/20 focus:border-[#662582] transition-all"
/>
{storesSearch && (
<button
@@ -327,7 +327,7 @@ export default function App() {
onClick={() => setStoresFilter('ALL')}
className={`flex-1 md:flex-none px-4 py-1.5 rounded-md text-xs font-bold transition-all ${
storesFilter === 'ALL'
? 'bg-white text-[#581c87] shadow-sm'
? 'bg-white text-[#662582] shadow-sm'
: 'text-zinc-500 hover:text-zinc-800 hover:bg-white/40'
}`}
>
@@ -344,14 +344,14 @@ export default function App() {
Active ({storesList.filter(s => s.status.toLowerCase() === 'active').length})
</button>
<button
onClick={() => setStoresFilter('CRITICAL')}
onClick={() => setStoresFilter('INACTIVE')}
className={`flex-1 md:flex-none px-4 py-1.5 rounded-md text-xs font-bold transition-all ${
storesFilter === 'CRITICAL'
storesFilter === 'INACTIVE'
? 'bg-white text-rose-600 shadow-sm'
: 'text-zinc-500 hover:text-rose-650 hover:bg-white/40'
}`}
>
Critical Alerts ({storesList.filter(s => s.status.toLowerCase() !== 'active' || s.deliveries > 40 || s.deliveries === 0).length})
Inactive ({storesList.filter(s => s.status.toLowerCase() !== 'active').length})
</button>
</div>
</div>
@@ -361,7 +361,7 @@ export default function App() {
<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-[#581c87] border-t-transparent rounded-full animate-spin" />
<div className="w-6 h-6 border-2 border-[#662582] border-t-transparent rounded-full animate-spin" />
<span>Loading live store locations</span>
</div>
) : (
@@ -435,7 +435,7 @@ export default function App() {
</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-[#581c87] mt-0.5 font-mono">{totalOrders.toLocaleString()}</p>
<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>
@@ -479,7 +479,7 @@ export default function App() {
<div className="mt-4 pt-3.5 border-t border-zinc-100">
<div className="flex justify-between items-center text-[8px] text-zinc-400 font-bold uppercase tracking-widest mb-1.5">
<span>Speed Index (Live Feed)</span>
<span className="text-[#581c87] flex items-center gap-0.5 text-[8px] font-bold">
<span className="text-[#662582] flex items-center gap-0.5 text-[8px] font-bold">
<Activity className="w-2.5 h-2.5 animate-pulse" /> Live
</span>
</div>
@@ -492,7 +492,7 @@ export default function App() {
st.status.toLowerCase() === 'active'
? st.deliveries > 40
? 'bg-rose-500/80 group-hover/bar:bg-rose-500'
: 'bg-[#581c87]/70 group-hover/bar:bg-[#581c87]'
: 'bg-[#662582]/70 group-hover/bar:bg-[#662582]'
: 'bg-amber-500/70 group-hover/bar:bg-amber-500'
}`}
/>
@@ -518,7 +518,7 @@ export default function App() {
e.stopPropagation();
alert(`Routing communications channel directly to manager ${st.staff}...`);
}}
className="p-1.5 rounded-lg bg-white border border-zinc-200 hover:border-[#581c87] hover:text-[#581c87] text-zinc-500 transition-colors shadow-sm"
className="p-1.5 rounded-lg bg-white border border-zinc-200 hover:border-[#662582] hover:text-[#662582] text-zinc-500 transition-colors shadow-sm"
title="Communicate with Node Lead"
>
<Phone className="w-3.5 h-3.5" />
@@ -526,9 +526,26 @@ export default function App() {
</div>
{/* Card footer - enter console */}
<div className="flex items-center justify-between text-[10px] font-bold text-[#581c87] mt-4 pt-3 border-t border-zinc-100/80">
<span className="uppercase tracking-wider">Enter Terminal Console</span>
<ArrowRight className="w-3.5 h-3.5 transform group-hover:translate-x-1.5 transition-transform duration-350 text-[#581c87]" />
<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>
@@ -571,10 +588,28 @@ export default function App() {
onHelpClick={handleHelp}
onLogoutClick={handleLogout}
profile={currentUser}
storeContext={{
storeName: currentSection === 'dashboard'
? (summaryQ.data?.tenantname ? `${summaryQ.data.tenantname} Admin` : 'Admin Console')
: currentSection === 'inventory'
? 'Products'
: currentSection.charAt(0).toUpperCase() + currentSection.slice(1),
icon: currentSection === 'dashboard'
? LayoutDashboard
: currentSection === 'inventory'
? Layers
: currentSection === 'stores'
? Store
: currentSection === 'reports'
? TrendingUp
: currentSection === 'settings'
? Settings
: undefined
}}
/>
{/* Main Container workspace layout splits */}
<div className="flex pt-20">
<div className="flex pt-16">
{/* Interactive Left Rail */}
<Sidebar
@@ -587,7 +622,7 @@ export default function App() {
/>
{/* Main core pages payload area */}
<main className={`flex-1 min-w-0 min-h-[calc(100vh-80px)] transition-all duration-300 ${sidebarOpen ? 'md:pl-64' : 'md:pl-20'}`}>
<main className={`flex-1 min-w-0 min-h-[calc(100vh-64px)] transition-all duration-300 ${sidebarOpen ? 'md:pl-64' : 'md:pl-20'}`}>
<div className="w-full p-container-margin md:p-xl space-y-lg transition-all duration-300">
{/* Nav content routing */}
{currentSection === 'dashboard' && (
@@ -629,7 +664,7 @@ export default function App() {
<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-[#581c87]" />
<Calendar size={15} className="text-[#662582]" />
Scheduled Reports Calendar
</h4>
<button
@@ -669,7 +704,7 @@ export default function App() {
<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-[#581c87]" />
<Building size={15} className="text-[#662582]" />
Commission New Regional Store Node
</h4>
<button
@@ -690,7 +725,7 @@ export default function App() {
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-[#581c87]"
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
required
/>
</div>
@@ -702,7 +737,7 @@ export default function App() {
placeholder="e.g. Coimbatore North"
value={newStore.zone}
onChange={(e) => setNewStore({ ...newStore, zone: e.target.value })}
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87]"
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
required
/>
</div>
@@ -714,7 +749,7 @@ export default function App() {
placeholder="e.g. Sridhar Sundaram"
value={newStore.lead}
onChange={(e) => setNewStore({ ...newStore, lead: e.target.value })}
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87]"
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
required
/>
</div>
@@ -726,7 +761,7 @@ export default function App() {
placeholder="₹1,50,000"
value={newStore.sales}
onChange={(e) => setNewStore({ ...newStore, sales: e.target.value })}
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87]"
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
/>
</div>
</div>
@@ -742,7 +777,7 @@ export default function App() {
</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"
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>

View File

@@ -378,7 +378,7 @@ VALUES (${newUserId}, 1, 'Active', NOW());
<div className="flex justify-between items-start border-b border-slate-100 pb-5">
<div>
<h3 className="text-lg font-bold text-slate-900 flex items-center gap-2">
<Store className="text-[#581c87]" size={20} />
<Store className="text-[#662582]" size={20} />
Add Store Outlet Location
</h3>
<p className="text-slate-550 text-xs mt-1">
@@ -635,7 +635,7 @@ VALUES (${newUserId}, 1, 'Active', NOW());
<button
type="submit"
disabled={createLocationMut.isPending}
className="bg-gradient-to-r from-[#581c87] to-indigo-650 hover:from-purple-755 hover:to-indigo-700 text-white px-6 py-3 rounded-xl text-xs font-bold uppercase tracking-wider flex items-center gap-2 cursor-pointer shadow-md active:scale-95 transition-all border-none"
className="bg-gradient-to-r from-[#662582] to-indigo-650 hover:from-purple-755 hover:to-indigo-700 text-white px-6 py-3 rounded-xl text-xs font-bold uppercase tracking-wider flex items-center gap-2 cursor-pointer shadow-md active:scale-95 transition-all border-none"
>
{createLocationMut.isPending ? (
<>
@@ -914,11 +914,11 @@ VALUES (${newUserId}, 1, 'Active', NOW());
onClick={() => { setActiveTab('tenant'); setTenantSuccess(null); }}
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all duration-200 whitespace-nowrap cursor-pointer border-none w-full ${
activeTab === 'tenant'
? 'bg-purple-50 text-[#581c87] shadow-sm border-l-2 border-purple-650'
? 'bg-purple-50 text-[#662582] shadow-sm border-l-2 border-purple-650'
: 'text-slate-600 hover:text-slate-900 hover:bg-white bg-transparent'
}`}
>
<Building2 size={16} className={activeTab === 'tenant' ? 'text-[#581c87]' : 'text-slate-450'} />
<Building2 size={16} className={activeTab === 'tenant' ? 'text-[#662582]' : 'text-slate-450'} />
<span>Tenant Onboarding</span>
</button>
@@ -926,11 +926,11 @@ VALUES (${newUserId}, 1, 'Active', NOW());
onClick={() => { setActiveTab('store'); setStoreSuccess(null); }}
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all duration-200 whitespace-nowrap cursor-pointer border-none w-full ${
activeTab === 'store'
? 'bg-purple-50 text-[#581c87] shadow-sm border-l-2 border-purple-650'
? 'bg-purple-50 text-[#662582] shadow-sm border-l-2 border-purple-650'
: 'text-slate-600 hover:text-slate-900 hover:bg-white bg-transparent'
}`}
>
<Store size={16} className={activeTab === 'store' ? 'text-[#581c87]' : 'text-slate-450'} />
<Store size={16} className={activeTab === 'store' ? 'text-[#662582]' : 'text-slate-450'} />
<span>Store Branch Onboarding</span>
</button>
@@ -938,11 +938,11 @@ VALUES (${newUserId}, 1, 'Active', NOW());
onClick={() => { setActiveTab('rider'); setRiderSuccess(null); }}
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all duration-200 whitespace-nowrap cursor-pointer border-none w-full ${
activeTab === 'rider'
? 'bg-purple-50 text-[#581c87] shadow-sm border-l-2 border-purple-650'
? 'bg-purple-50 text-[#662582] shadow-sm border-l-2 border-purple-650'
: 'text-slate-600 hover:text-slate-900 hover:bg-white bg-transparent'
}`}
>
<Bike size={16} className={activeTab === 'rider' ? 'text-[#581c87]' : 'text-slate-450'} />
<Bike size={16} className={activeTab === 'rider' ? 'text-[#662582]' : 'text-slate-450'} />
<span>Rider Onboarding</span>
</button>
</nav>

View File

@@ -15,9 +15,7 @@ import {
Clock,
ArrowUpRight,
} from 'lucide-react';
import { useOrderSummary, useTenantInfo, useInvoiceInsight } from '../services/queries';
import { DEFAULT_CONFIG_ID } from '../services/api';
import { useFiestaLocationSummary, useFiestaTenantLocations } from '../services/fiestaQueries';
import { useFiestaLocationSummary, useFiestaTenantLocations, useFiestaRevenueSummary, useFiestaOrderSummary } from '../services/fiestaQueries';
import { FIESTA_TENANT_ID } from '../services/fiestaApi';
interface DashboardViewProps {
@@ -43,19 +41,18 @@ export default function DashboardView({ searchQuery, tenantId = FIESTA_TENANT_ID
// All scoped to the signed-in merchant's tenant. Store locations come from the
// Fiesta source (the single source of truth used across the app) — it's already
// deduped and stripped of test rows, unlike the raw Hasura tenant-locations feed.
const summaryQ = useOrderSummary(tenantId, fromdate, todate, DEFAULT_CONFIG_ID);
const tenantQ = useTenantInfo(tenantId);
const summaryQ = useFiestaOrderSummary(tenantId, fromdate, todate);
const locationsQ = useFiestaTenantLocations(tenantId);
const insightQ = useInvoiceInsight(tenantId);
const revenueQ = useFiestaRevenueSummary({ tenantid: tenantId, fromdate, todate });
const s = summaryQ.data;
const tenantName = str(tenantQ.data?.tenantname) || s?.tenantname || `Tenant ${tenantId}`;
const tenantName = s?.tenantname || `Tenant ${tenantId}`;
// Revenue + profit come from the live invoice/financial insight. The endpoint
// returns two distinct figures (revenue and profit); we surface both rather than
// repeating one. When the tenant has no invoice records we show "—" instead of a
// misleading ₹0.
const insight = insightQ.data as any;
const insight = revenueQ.data as any;
const money = (v: number | null) => (v == null ? '—' : `${Math.round(v).toLocaleString('en-IN')}`);
const monthlyRevenue = insight ? Number(insight.grossrevenue || insight.overallrevenue || insight.revenue || 0) : null;
const monthlyProfit = insight ? Number(insight.profit || insight.netrevenue || insight.margin || 0) : null;
@@ -112,7 +109,7 @@ export default function DashboardView({ searchQuery, tenantId = FIESTA_TENANT_ID
icon: Wallet,
bar: 'from-sky-500 to-cyan-500',
chip: 'bg-sky-50 text-sky-600 ring-sky-100',
loading: insightQ.isLoading,
loading: revenueQ.isLoading,
},
{
title: 'MONTHLY PROFIT',
@@ -121,7 +118,7 @@ export default function DashboardView({ searchQuery, tenantId = FIESTA_TENANT_ID
icon: TrendingUp,
bar: 'from-emerald-500 to-teal-500',
chip: 'bg-emerald-50 text-emerald-600 ring-emerald-100',
loading: insightQ.isLoading,
loading: revenueQ.isLoading,
},
];

View File

@@ -22,28 +22,28 @@ export default function DispatchHubView({ locationid, tenantId }: DispatchHubVie
<button
onClick={() => setActiveTab('map')}
className={`flex items-center !gap-1.5 !px-3 !py-1.5 text-[11px] font-extrabold !rounded-md transition-all duration-300 uppercase tracking-wide ${
activeTab === 'map' ? 'bg-white text-[#581c87] shadow-sm ring-1 ring-black/5' : 'text-slate-500 hover:text-slate-800 hover:bg-slate-200/50'
activeTab === 'map' ? 'bg-white text-[#662582] shadow-sm ring-1 ring-black/5' : 'text-slate-500 hover:text-slate-800 hover:bg-slate-200/50'
}`}
>
<Route size={13} className={activeTab === 'map' ? 'text-[#581c87]' : 'text-slate-400'} /> Map
<Route size={13} className={activeTab === 'map' ? 'text-[#662582]' : 'text-slate-400'} /> Map
</button>
<button
onClick={() => setActiveTab('orders')}
className={`flex items-center !gap-1.5 !px-3 !py-1.5 text-[11px] font-extrabold !rounded-md transition-all duration-300 uppercase tracking-wide ${
activeTab === 'orders' ? 'bg-white text-[#581c87] shadow-sm ring-1 ring-black/5' : 'text-slate-500 hover:text-slate-800 hover:bg-slate-200/50'
activeTab === 'orders' ? 'bg-white text-[#662582] shadow-sm ring-1 ring-black/5' : 'text-slate-500 hover:text-slate-800 hover:bg-slate-200/50'
}`}
>
<ShoppingBag size={13} className={activeTab === 'orders' ? 'text-[#581c87]' : 'text-slate-400'} /> Orders
<ShoppingBag size={13} className={activeTab === 'orders' ? 'text-[#662582]' : 'text-slate-400'} /> Orders
</button>
<button
onClick={() => setActiveTab('deliveries')}
className={`flex items-center !gap-1.5 !px-3 !py-1.5 text-[11px] font-extrabold !rounded-md transition-all duration-300 uppercase tracking-wide ${
activeTab === 'deliveries' ? 'bg-white text-[#581c87] shadow-sm ring-1 ring-black/5' : 'text-slate-500 hover:text-slate-800 hover:bg-slate-200/50'
activeTab === 'deliveries' ? 'bg-white text-[#662582] shadow-sm ring-1 ring-black/5' : 'text-slate-500 hover:text-slate-800 hover:bg-slate-200/50'
}`}
>
<Truck size={13} className={activeTab === 'deliveries' ? 'text-[#581c87]' : 'text-slate-400'} /> Deliveries
<Truck size={13} className={activeTab === 'deliveries' ? 'text-[#662582]' : 'text-slate-400'} /> Deliveries
</button>
</div>
);

View File

@@ -225,7 +225,7 @@ function MapController({ points, resizeKey }: { points: MapPoint[]; resizeKey: u
export default function DispatchMap({
points,
route,
routeColor = '#581c87',
routeColor = '#662582',
start,
resizeKey,
animateNonce = 0,

View File

@@ -10878,44 +10878,44 @@
/* ──────────────────────────────────────────────────────────────────────────────
Merchant theme override: the source console is blue-accented (#3b82f6); this
app's other pages (Orders/Deliveries/Reports) use purple #581c87 with purple
app's other pages (Orders/Deliveries/Reports) use purple #662582 with purple
selected buttons. Retint the accent so the Dispatch buttons + selected state
match the rest of the console. Appended last so it wins the cascade.
────────────────────────────────────────────────────────────────────────────── */
.dispatch-container {
--accent: #581c87;
--accent: #662582;
--accent-soft: rgba(88, 28, 135, 0.08);
--border-active: #581c87;
--border-active: #662582;
}
/* Selected view-mode tab (.sbt.active uses var(--accent) for bg/border) */
.dispatch-container .sbt.active {
box-shadow: 0 4px 12px rgba(88, 28, 135, 0.25);
}
.dispatch-container .sbt:hover:not(.active) {
border-color: #581c87;
color: #581c87;
border-color: #662582;
color: #662582;
}
/* Selected batch / wave chip (was a hardcoded blue gradient) */
.dispatch-container .batch-btn.batch-slot.active,
.dispatch-container .batch-btn.active {
background: linear-gradient(135deg, #581c87, #7c3aed);
background: linear-gradient(135deg, #662582, #7c3aed);
}
.dispatch-container .batch-btn:hover:not(.active) {
border-color: #581c87;
color: #581c87;
border-color: #662582;
color: #662582;
}
/* Brand badge + wordmark accent */
.dispatch-container .logo-badge {
background: linear-gradient(135deg, #581c87, #6b21a8);
background: linear-gradient(135deg, #662582, #6b21a8);
}
.dispatch-container .logo-name em {
color: #581c87;
color: #662582;
}
/* Operating-city pill → brand purple to match the rest of the console */
.dispatch-container .logo-city {
background: rgba(88, 28, 135, 0.08);
border-color: rgba(88, 28, 135, 0.25);
color: #581c87;
color: #662582;
}
.dispatch-container .logo-city:hover {
background: rgba(88, 28, 135, 0.14);
@@ -10940,7 +10940,7 @@
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #581c87;
color: #662582;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
@@ -10985,7 +10985,7 @@
display: flex;
align-items: center;
justify-content: center;
color: #581c87;
color: #662582;
opacity: 0.7;
}

View File

@@ -456,7 +456,7 @@ export default function DispatchView({ locationid, tenantId = FIESTA_TENANT_ID,
<DispatchMap
points={mapPoints}
route={Boolean(focused)}
routeColor={focused?.color || '#581c87'}
routeColor={focused?.color || '#662582'}
start={routeStart}
resizeKey={`${sidebarCollapsed}|${viewMode}|${focusedId}`}
animateNonce={animateNonce}

View File

@@ -23,6 +23,12 @@ interface HeaderProps {
onQrClick?: () => void;
/** Signed-in user shown in the profile dropdown. */
profile: { name: string; role: string; email: string };
/** Optional store context to display next to the sidebar toggle. */
storeContext?: {
storeName: string;
branchName?: string;
icon?: React.ElementType;
};
}
export default function Header({
@@ -32,7 +38,8 @@ export default function Header({
onLogoutClick,
onAccountClick,
onQrClick,
profile
profile,
storeContext
}: HeaderProps) {
const [showProfileDropdown, setShowProfileDropdown] = useState(false);
const profileRef = useRef<HTMLDivElement>(null);
@@ -63,7 +70,7 @@ export default function Header({
.toUpperCase() || 'NA';
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">
<header className="bg-[#662582] border-b border-[#662582] flex justify-between items-center w-full px-container-margin py-md fixed top-0 right-0 left-0 z-50 h-16 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 */}
@@ -71,7 +78,7 @@ export default function Header({
<img
src={isSidebarOpen ? '/logo.png' : '/favicon.png'}
alt="nearledaily logo"
className="h-9 w-auto object-contain"
className="h-7 w-auto object-contain"
/>
</span>
@@ -83,6 +90,21 @@ export default function Header({
>
<Menu size={18} />
</button>
{/* Dynamic Store Name Context */}
{storeContext && (
<div className="flex flex-col ml-1 border-l border-[#662582] pl-3 py-0.5 select-none">
<span className="flex items-center gap-2 text-lg font-bold tracking-tight text-white leading-tight">
{storeContext.icon && <storeContext.icon size={20} className="text-purple-300" />}
{storeContext.storeName}
</span>
{storeContext.branchName && (
<span className="text-[10px] font-medium text-purple-200 tracking-wider uppercase leading-tight">
{storeContext.branchName}
</span>
)}
</div>
)}
</div>
{/* Global Actions Bar */}
@@ -110,7 +132,7 @@ export default function Header({
<span className="w-9 h-9 rounded-full bg-white/15 ring-2 ring-white/30 flex items-center justify-center text-xs font-bold text-white tracking-wide">
{initials}
</span>
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full bg-emerald-400 ring-2 ring-[#581c87]" />
<span className="absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full bg-emerald-400 ring-2 ring-[#662582]" />
</span>
{/* Identity (hidden on small screens) */}
@@ -128,7 +150,7 @@ export default function Header({
{showProfileDropdown && (
<div className="absolute right-0 mt-2.5 w-72 bg-white border border-slate-200/80 rounded-2xl shadow-2xl shadow-purple-950/15 z-50 text-slate-700 animate-in fade-in slide-in-from-top-2 duration-200 overflow-hidden">
{/* Gradient profile header */}
<div className="relative px-4 py-4 bg-gradient-to-br from-[#581c87] via-purple-800 to-purple-950 text-white overflow-hidden">
<div className="relative px-4 py-4 bg-gradient-to-br from-[#662582] via-purple-800 to-purple-950 text-white overflow-hidden">
<div className="absolute -top-8 -right-8 w-28 h-28 bg-purple-400/20 rounded-full blur-2xl pointer-events-none" />
<div className="relative flex items-center gap-3">
<span className="relative shrink-0">
@@ -157,7 +179,7 @@ export default function Header({
onClick={() => { setShowProfileDropdown(false); onAccountClick(); }}
className="w-full flex items-center gap-2.5 px-2.5 py-2 rounded-xl text-xs font-semibold text-slate-700 hover:bg-slate-50 cursor-pointer transition-colors group/item"
>
<span className="h-7 w-7 rounded-lg bg-purple-50 text-[#581c87] ring-1 ring-purple-100 flex items-center justify-center group-hover/item:scale-110 transition-transform">
<span className="h-7 w-7 rounded-lg bg-purple-50 text-[#662582] ring-1 ring-purple-100 flex items-center justify-center group-hover/item:scale-110 transition-transform">
<User size={14} />
</span>
My Account

View File

@@ -92,7 +92,56 @@ export default function InventoryView({
const id = rowId(r);
if (id && !byId.has(id)) byId.set(id, r);
});
setProducts(Array.from(byId.values()).map(stockRowToProduct));
const initialProducts = Array.from(byId.values()).map(stockRowToProduct);
// Add 3 mock products with isNew: true
const mockProducts: ProductMatrixItem[] = [
{
id: 'mock-1',
name: 'Organic Honey 500g',
sku: 'GRO-HON-500G',
unitsSold: 0,
revenue: 0,
stockStatus: 'Healthy',
trend: 'flat',
image: 'https://images.unsplash.com/photo-1587049352847-4d4b1240c5f2?auto=format&fit=crop&q=80&w=200',
category: 'Groceries / Pantry',
exposure: 'All Outlets',
verified: true,
isNew: true
},
{
id: 'mock-2',
name: 'Premium Basmati Rice 5kg',
sku: 'STA-BAS-5KG',
unitsSold: 0,
revenue: 0,
stockStatus: 'Healthy',
trend: 'flat',
image: 'https://images.unsplash.com/photo-1586201375761-83865001e31c?auto=format&fit=crop&q=80&w=200',
category: 'Staples / Rice',
exposure: 'All Outlets',
verified: true,
isNew: true
},
{
id: 'mock-3',
name: 'Fresh Farm Eggs (Dozen)',
sku: 'FRE-EGG-12P',
unitsSold: 0,
revenue: 0,
stockStatus: 'Healthy',
trend: 'flat',
image: 'https://images.unsplash.com/photo-1587486913049-53fc88980cfc?auto=format&fit=crop&q=80&w=200',
category: 'Fresh Produce / Dairy',
exposure: 'All Outlets',
verified: true,
isNew: true
}
];
setProducts([...mockProducts, ...initialProducts]);
setSeeded(true);
}, [allStoreRows, seeded]);
@@ -217,7 +266,8 @@ export default function InventoryView({
image: newProduct.image,
category: newProduct.category,
exposure: 'All Outlets',
verified: true
verified: true,
isNew: true
};
setProducts([createdProd, ...products]);
@@ -270,7 +320,8 @@ export default function InventoryView({
image: 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&q=80&w=200',
category,
exposure: 'All Outlets',
verified: true
verified: true,
isNew: true
});
parsedCount++;
}
@@ -292,110 +343,52 @@ export default function InventoryView({
<div className="absolute top-10 left-1/4 w-96 h-96 bg-purple-400/5 rounded-full blur-[120px] pointer-events-none -z-10 animate-pulse" />
<div className="absolute top-40 right-1/4 w-[28rem] h-[28rem] bg-indigo-400/5 rounded-full blur-[140px] pointer-events-none -z-10 animate-pulse" style={{ animationDuration: '8s' }} />
{/* ── Immersive Analytics Banner (With Catalog Cover Image & Slate Gradient Overlay) ── */}
<div className="relative rounded-2xl p-6 md:p-8 text-white shadow-xl border border-purple-500/20 mb-8 animate-in fade-in duration-300 z-40">
{/* Cover Image Background & Decor */}
<div className="absolute inset-0 z-0 overflow-hidden rounded-2xl">
<img
src="https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&w=1200&q=80"
alt="Catalogue Command Center Banner"
className="w-full h-full object-cover object-center opacity-30"
/>
<div className="absolute inset-0 bg-gradient-to-r from-slate-950 via-slate-900/95 to-purple-950/85" />
{/* Background decorative glowing circles */}
<div className="absolute top-0 right-0 w-72 h-72 bg-purple-500/10 rounded-full blur-3xl -mr-20 -mt-20 pointer-events-none z-0" />
<div className="absolute bottom-0 left-0 w-56 h-56 bg-slate-500/5 rounded-full blur-2xl -ml-20 -mb-20 pointer-events-none z-0" />
</div>
{/* Content Row */}
<div className="relative z-20 flex flex-col md:flex-row md:items-center justify-between gap-6">
<div>
<h1 className="font-sans font-bold text-2xl tracking-tight text-white flex items-center gap-2">
<Layers size={24} className="text-purple-300" />
Product Catalogue
<span className="text-[10px] text-purple-200 font-bold bg-purple-900/60 border border-purple-500/30 px-2 py-0.5 rounded-full uppercase tracking-wider animate-pulse">
Global Sync
</span>
</h1>
<p className="text-purple-250 font-sans text-xs mt-1.5 font-medium max-w-2xl">
Master catalogue registry with regional assortment presets, brand styling studio, and live stock synchronization feeds.
</p>
</div>
{/* Navigation Tab pills styled like a modern control unit */}
<div className="flex items-center gap-1.5 bg-slate-950/60 backdrop-blur-md border border-white/10 p-1.5 rounded-xl shrink-0">
<button
onClick={() => setActiveTab('catalog')}
className={`px-4 py-2 rounded-lg text-xs font-bold transition-all duration-300 flex items-center gap-2 cursor-pointer ${
activeTab === 'catalog'
? 'bg-purple-900/50 text-purple-200 border border-purple-500/40 shadow-[0_0_15px_rgba(168,85,247,0.2)]'
: 'text-zinc-400 hover:text-white border border-transparent'
}`}
>
<Package size={13} />
<span>Catalogue & Stocks</span>
</button>
<button
onClick={() => setActiveTab('import_branding')}
className={`px-4 py-2 rounded-lg text-xs font-bold transition-all duration-300 flex items-center gap-2 cursor-pointer ${
activeTab === 'import_branding'
? 'bg-purple-900/50 text-purple-200 border border-purple-500/40 shadow-[0_0_15px_rgba(168,85,247,0.2)]'
: 'text-zinc-400 hover:text-white border border-transparent'
}`}
>
<UploadCloud size={13} />
<span>Import & Brand Studio</span>
</button>
</div>
</div>
{/* Small card metrics grid - 4 dynamic columns inside the banner */}
<div className="grid grid-cols-1 sm:grid-cols-4 gap-4 mt-8 pt-6 border-t border-slate-800/80 relative z-10">
{/* Header and Metrics */}
<div className="mb-8 z-40 relative">
{/* Small card metrics grid */}
<div className="grid grid-cols-1 sm:grid-cols-4 gap-4">
{/* Card 1: Total SKUs */}
<div className="bg-slate-900/40 backdrop-blur-sm rounded-xl p-4 border border-slate-800/80 hover:border-purple-500/30 transition-all group">
<div className="bg-white rounded-xl p-3 border border-slate-200 shadow-sm hover:border-purple-200 transition-all group">
<div className="flex justify-between items-start">
<span className="text-[10px] text-slate-400 uppercase tracking-widest font-bold">Total SKUs</span>
<div className="p-2 rounded-lg bg-purple-500/10 text-purple-400 border border-purple-500/20 group-hover:scale-110 transition-transform">
<Package className="w-4 h-4" />
<span className="text-[10px] text-slate-500 uppercase tracking-widest font-bold">Total SKUs</span>
<div className="p-1.5 rounded-lg bg-purple-50 text-purple-600 group-hover:scale-110 transition-transform">
<Package className="w-3.5 h-3.5" />
</div>
</div>
<div className="mt-2">
<h3 className="text-xl font-extrabold tracking-tight font-mono">
<div className="mt-1">
<h3 className="text-lg font-extrabold tracking-tight font-mono text-slate-900">
{products.length}
</h3>
<p className="text-[10px] text-purple-400 font-semibold mt-1">Master catalogue</p>
<p className="text-[10px] text-slate-400 font-semibold mt-0.5">Master catalogue</p>
</div>
</div>
{/* Card 2: Synced Outlets */}
<div className="bg-slate-900/40 backdrop-blur-sm rounded-xl p-4 border border-slate-800/80 hover:border-purple-500/30 transition-all group">
<div className="bg-white rounded-xl p-3 border border-slate-200 shadow-sm hover:border-indigo-200 transition-all group">
<div className="flex justify-between items-start">
<span className="text-[10px] text-slate-400 uppercase tracking-widest font-bold">Active Outlets</span>
<div className="p-2 rounded-lg bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 group-hover:scale-110 transition-transform">
<Layers className="w-4 h-4" />
<span className="text-[10px] text-slate-500 uppercase tracking-widest font-bold">Active Outlets</span>
<div className="p-1.5 rounded-lg bg-indigo-50 text-indigo-600 group-hover:scale-110 transition-transform">
<Layers className="w-3.5 h-3.5" />
</div>
</div>
<div className="mt-2">
<h3 className="text-xl font-extrabold tracking-tight font-mono">
<div className="mt-1">
<h3 className="text-lg font-extrabold tracking-tight font-mono text-slate-900">
{locations.length}
</h3>
<p className="text-[10px] text-indigo-400 font-semibold mt-1">Synced locations</p>
<p className="text-[10px] text-slate-400 font-semibold mt-0.5">Synced locations</p>
</div>
</div>
{/* Card 3: Total On-Hand Volume */}
<div className="bg-slate-900/40 backdrop-blur-sm rounded-xl p-4 border border-slate-800/80 hover:border-purple-500/30 transition-all group">
<div className="bg-white rounded-xl p-3 border border-slate-200 shadow-sm hover:border-emerald-200 transition-all group">
<div className="flex justify-between items-start">
<span className="text-[10px] text-slate-400 uppercase tracking-widest font-bold">Total Stock</span>
<div className="p-2 rounded-lg bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 group-hover:scale-110 transition-transform">
<Check className="w-4 h-4" />
<span className="text-[10px] text-slate-500 uppercase tracking-widest font-bold">Total Stock</span>
<div className="p-1.5 rounded-lg bg-emerald-50 text-emerald-600 group-hover:scale-110 transition-transform">
<Check className="w-3.5 h-3.5" />
</div>
</div>
<div className="mt-2">
<h3 className="text-xl font-extrabold tracking-tight font-mono">
<div className="mt-1">
<h3 className="text-lg font-extrabold tracking-tight font-mono text-slate-900">
{storesStockWithOverrides.reduce((total, store) => {
return total + (store.rows || []).reduce((subTotal, r) => {
const inv = stockRowToInventory(r, store.locationname);
@@ -403,26 +396,25 @@ export default function InventoryView({
}, 0);
}, 0).toLocaleString('en-IN')}
</h3>
<p className="text-[10px] text-emerald-400 font-semibold mt-1">Units on hand</p>
<p className="text-[10px] text-slate-400 font-semibold mt-0.5">Units on hand</p>
</div>
</div>
{/* Card 4: Catalog Health */}
<div className="bg-slate-900/40 backdrop-blur-sm rounded-xl p-4 border border-slate-800/80 hover:border-purple-500/30 transition-all group">
<div className="bg-white rounded-xl p-3 border border-slate-200 shadow-sm hover:border-amber-200 transition-all group">
<div className="flex justify-between items-start">
<span className="text-[10px] text-slate-400 uppercase tracking-widest font-bold">Catalogue Sync Ratio</span>
<div className="p-2 rounded-lg bg-amber-500/10 text-amber-400 border border-amber-500/20 group-hover:scale-110 transition-transform">
<ShieldCheck className="w-4 h-4" />
<span className="text-[10px] text-slate-500 uppercase tracking-widest font-bold">Catalogue Sync Ratio</span>
<div className="p-1.5 rounded-lg bg-amber-50 text-amber-600 group-hover:scale-110 transition-transform">
<ShieldCheck className="w-3.5 h-3.5" />
</div>
</div>
<div className="mt-2">
<h3 className="text-xl font-extrabold tracking-tight font-mono">
<div className="mt-1">
<h3 className="text-lg font-extrabold tracking-tight font-mono text-slate-900">
{products.length > 0 ? `${Math.round((products.filter(p => p.verified).length / products.length) * 100)}%` : '100%'}
</h3>
<p className="text-[10px] text-amber-400 font-semibold mt-1">Active Portfolio</p>
<p className="text-[10px] text-slate-400 font-semibold mt-0.5">Active Portfolio</p>
</div>
</div>
</div>
</div>
@@ -476,14 +468,14 @@ export default function InventoryView({
<div className="flex items-center gap-sm">
<button
onClick={() => setActiveTab('import_branding')}
className="bg-white text-[#581c87] border border-purple-200 px-4 py-2 rounded-lg text-xs font-bold uppercase tracking-wider flex items-center justify-center gap-xs cursor-pointer hover:bg-[#faf5ff] transition shadow-sm"
className="bg-white text-[#662582] border border-purple-200 px-4 py-2 rounded-lg text-xs font-bold uppercase tracking-wider flex items-center justify-center gap-xs cursor-pointer hover:bg-[#faf5ff] transition shadow-sm"
>
<UploadCloud size={14} />
Import SKUs
</button>
<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"
className="bg-[#662582] 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 SKU
@@ -496,7 +488,7 @@ export default function InventoryView({
<div className="flex items-center justify-between mb-sm">
<div>
<h3 className="font-sans font-bold text-sm text-[#0f172a] flex items-center gap-1.5">
<Package size={15} className="text-[#581c87]" /> Global Catalogue Assortment
<Package size={15} className="text-[#662582]" /> Global Catalogue Assortment
</h3>
<p className="text-[10px] text-zinc-400 font-medium mt-0.5">Pick products & set quantities selected items appear in every store's catalogue.</p>
</div>
@@ -504,7 +496,7 @@ export default function InventoryView({
<span className="text-[10px] text-emerald-700 font-bold bg-emerald-50 px-2 py-0.5 rounded-lg border border-emerald-100">
{storeCat.items.length} in store catalogue
</span>
<span className="text-[10px] text-[#581c87] font-bold bg-purple-50 px-2 py-0.5 rounded-lg border border-purple-100/50">
<span className="text-[10px] text-[#662582] font-bold bg-purple-50 px-2 py-0.5 rounded-lg border border-purple-100/50">
{filteredProducts.length} item{filteredProducts.length === 1 ? '' : 's'} loaded
</span>
</div>
@@ -515,91 +507,202 @@ export default function InventoryView({
) : filteredProducts.length === 0 ? (
<div className="text-center py-xl text-zinc-400 text-xs font-bold">No catalogue products match your selection.</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-md">
{filteredProducts.map((prod) => (
<div key={prod.id} className="bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-2xl p-md flex flex-col justify-between gap-sm shadow-sm hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-purple-200 hover:-translate-y-0.5 transition-all duration-300 relative group">
<div className="flex gap-md">
{/* Image zoom effect on hover */}
<div className="w-16 h-16 rounded-xl border border-zinc-100 shrink-0 overflow-hidden bg-zinc-50 relative">
<img
src={prod.image}
alt={prod.name}
referrerPolicy="no-referrer"
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
/>
</div>
<div className="flex-1 space-y-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<h4 className="font-bold text-[#0f172a] leading-tight text-xs truncate group-hover:text-[#581c87] transition-colors">{prod.name}</h4>
<span className="text-[10px] text-zinc-400 font-bold font-mono tracking-tight">{prod.sku}</span>
<div className="flex flex-col xl:flex-row gap-6">
{/* Left Side: Normal Catalogue */}
<div className="flex-1 min-w-0">
<div className="grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-3 gap-md">
{filteredProducts.filter(p => !p.isNew).map((prod) => (
<div key={prod.id} className="bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-2xl p-md flex flex-col justify-between gap-sm shadow-sm hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-purple-200 hover:-translate-y-0.5 transition-all duration-300 relative group">
<div className="flex gap-md">
{/* Image zoom effect on hover */}
<div className="w-24 h-24 rounded-xl border border-zinc-100 shrink-0 overflow-hidden bg-zinc-50 relative">
<img
src={prod.image}
alt={prod.name}
referrerPolicy="no-referrer"
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
/>
</div>
{/* Categorized pill badge */}
<span className={`px-1.5 py-0.5 rounded text-[8px] font-extrabold uppercase shrink-0 ${
prod.category.startsWith('Staples') ? 'bg-amber-50 text-amber-600 border border-amber-100' :
prod.category.startsWith('Groceries') ? 'bg-emerald-50 text-emerald-600 border border-emerald-100' :
prod.category.startsWith('Beverages') ? 'bg-sky-50 text-sky-600 border border-sky-100' :
'bg-rose-50 text-rose-600 border border-rose-100'
<div className="flex-1 space-y-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex items-center gap-2">
<h4 className="font-bold text-[#0f172a] leading-tight text-xs truncate group-hover:text-[#662582] transition-colors">{prod.name}</h4>
</div>
<div>
<span className="text-[10px] text-zinc-400 font-bold font-mono tracking-tight">{prod.sku}</span>
</div>
{/* Categorized pill badge */}
<span className={`px-1.5 py-0.5 rounded text-[8px] font-extrabold uppercase shrink-0 ${
prod.category.startsWith('Staples') ? 'bg-amber-50 text-amber-600 border border-amber-100' :
prod.category.startsWith('Groceries') ? 'bg-emerald-50 text-emerald-600 border border-emerald-100' :
prod.category.startsWith('Beverages') ? 'bg-sky-50 text-sky-600 border border-sky-100' :
'bg-rose-50 text-rose-600 border border-rose-100'
}`}>
{prod.category.split(' / ')[0]}
</span>
</div>
<div className="flex justify-between items-center pt-2">
<div>
<span className="text-[8px] text-zinc-400 block uppercase tracking-wider font-extrabold">Units Sold</span>
<span className="font-extrabold text-zinc-755 font-mono text-xs">{prod.unitsSold.toLocaleString()}</span>
</div>
<div className="text-right">
<span className="text-[8px] text-zinc-400 block uppercase tracking-wider font-extrabold">Revenue</span>
<span className="font-black text-emerald-600 font-mono text-xs">₹{prod.revenue.toLocaleString()}</span>
</div>
</div>
</div>
</div>
{/* Exposure toggle row */}
<div className="flex justify-between items-center pt-2.5 border-t border-[#f1f5f9] mt-1 select-none">
<span className={`inline-flex items-center gap-1.5 text-[10px] font-bold tracking-tight ${
prod.verified ? 'text-emerald-600' : 'text-zinc-400'
}`}>
{prod.category.split(' / ')[0]}
<span className={`w-1.5 h-1.5 rounded-full ${prod.verified ? 'bg-emerald-500 animate-pulse' : 'bg-zinc-300'}`} />
{prod.verified ? 'Active Portfolio' : 'Under Inspection'}
</span>
<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-8 h-4.5 bg-slate-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-350 after:border after:rounded-full after:h-3.5 after:w-3.5 after:transition-all peer-checked:bg-emerald-500 shadow-inner"></div>
</label>
</div>
<div className="flex justify-between items-center pt-2">
<div>
<span className="text-[8px] text-zinc-400 block uppercase tracking-wider font-extrabold">Units Sold</span>
<span className="font-extrabold text-zinc-755 font-mono text-xs">{prod.unitsSold.toLocaleString()}</span>
{/* Store-catalogue curation: pick the product to show to store users */}
{storeCat.has(prod.id) ? (
<div className="flex items-center justify-between gap-2 pt-2.5 border-t border-[#f1f5f9] mt-1">
<span className="inline-flex items-center gap-1 text-[10px] font-bold text-emerald-600"><CheckCircle size={12} /> In Store Catalogue</span>
<button onClick={() => storeCat.remove(prod.id)} title="Remove from store catalogue" className="flex items-center gap-1 px-2 py-1 rounded-lg text-rose-500 hover:bg-rose-50 font-bold text-[10px] cursor-pointer transition-colors">
<X size={13} /> Remove
</button>
</div>
<div className="text-right">
<span className="text-[8px] text-zinc-400 block uppercase tracking-wider font-extrabold">Revenue</span>
<span className="font-black text-emerald-600 font-mono text-xs">₹{prod.revenue.toLocaleString()}</span>
</div>
</div>
) : (
<button
onClick={() => storeCat.add({ productid: prod.id, name: prod.name, image: prod.image, category: prod.category, sku: prod.sku, price: prod.unitsSold > 0 ? Math.round(prod.revenue / prod.unitsSold) : 0, unit: prod.exposure, qty: 1 })}
disabled={!prod.verified}
className={`w-full flex items-center justify-center gap-1.5 pt-2.5 mt-1 border-t border-[#f1f5f9] text-[11px] font-bold transition-colors ${
prod.verified
? 'text-[#662582] hover:text-purple-800 cursor-pointer'
: 'text-zinc-400 cursor-not-allowed opacity-70'
}`}
title={!prod.verified ? "Product must be Active to be added" : "Add to Store Catalogue"}
>
<Plus size={13} /> Add to Store Catalogue
</button>
)}
</div>
</div>
{/* Exposure toggle row */}
<div className="flex justify-between items-center pt-2.5 border-t border-[#f1f5f9] mt-1 select-none">
<span className={`inline-flex items-center gap-1.5 text-[10px] font-bold tracking-tight ${
prod.verified ? 'text-emerald-600' : 'text-zinc-400'
}`}>
<span className={`w-1.5 h-1.5 rounded-full ${prod.verified ? 'bg-emerald-500 animate-pulse' : 'bg-zinc-300'}`} />
{prod.verified ? 'Active Portfolio' : 'Under Inspection'}
</span>
<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-8 h-4.5 bg-slate-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-350 after:border after:rounded-full after:h-3.5 after:w-3.5 after:transition-all peer-checked:bg-emerald-500 shadow-inner"></div>
</label>
</div>
{/* Store-catalogue curation: pick the product + quantity to show to store users */}
{storeCat.has(prod.id) ? (
<div className="flex items-center justify-between gap-2 pt-2.5 border-t border-[#f1f5f9] mt-1">
<span className="inline-flex items-center gap-1 text-[10px] font-bold text-emerald-600"><CheckCircle size={12} /> In Store Catalogue</span>
<div className="flex items-center gap-1">
<button onClick={() => storeCat.setQty(prod.id, storeCat.getQty(prod.id) - 1)} className="w-6 h-6 rounded-lg border border-[#e2e8f0] text-zinc-500 hover:bg-zinc-50 font-bold cursor-pointer leading-none"></button>
<span className="w-8 text-center font-mono font-bold text-xs text-[#0f172a]">{storeCat.getQty(prod.id)}</span>
<button onClick={() => storeCat.setQty(prod.id, storeCat.getQty(prod.id) + 1)} className="w-6 h-6 rounded-lg border border-[#e2e8f0] text-zinc-500 hover:bg-zinc-50 font-bold cursor-pointer leading-none">+</button>
<button onClick={() => storeCat.remove(prod.id)} title="Remove from store catalogue" className="ml-1 w-6 h-6 rounded-lg text-rose-500 hover:bg-rose-50 flex items-center justify-center cursor-pointer"><X size={13} /></button>
</div>
</div>
) : (
<button
onClick={() => storeCat.add({ productid: prod.id, name: prod.name, image: prod.image, category: prod.category, sku: prod.sku, price: prod.unitsSold > 0 ? Math.round(prod.revenue / prod.unitsSold) : 0, unit: prod.exposure, qty: 1 })}
className="w-full flex items-center justify-center gap-1.5 pt-2.5 mt-1 border-t border-[#f1f5f9] text-[11px] font-bold text-[#581c87] hover:text-purple-800 cursor-pointer"
>
<Plus size={13} /> Add to Store Catalogue
</button>
)}
))}
</div>
))}
</div>
{/* Right Side: Newly Added Items */}
{filteredProducts.filter(p => p.isNew).length > 0 && (
<div className="w-full xl:w-96 shrink-0 xl:border-l xl:border-purple-100/50 xl:pl-6 relative">
<div className="bg-[#f8fafc] px-2 flex items-center gap-2 mb-4 xl:-ml-2">
<Sparkles size={14} className="text-purple-600" />
<h4 className="text-xs font-bold text-purple-900 uppercase tracking-widest">Newly Added</h4>
</div>
<div className="flex flex-col gap-md">
{filteredProducts.filter(p => p.isNew).map((prod) => (
<div key={prod.id} className="bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-2xl p-md flex flex-col justify-between gap-sm shadow-sm hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-purple-200 hover:-translate-y-0.5 transition-all duration-300 relative group">
<div className="flex gap-md">
{/* Image zoom effect on hover */}
<div className="w-24 h-24 rounded-xl border border-zinc-100 shrink-0 overflow-hidden bg-zinc-50 relative">
<img
src={prod.image}
alt={prod.name}
referrerPolicy="no-referrer"
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
/>
</div>
<div className="flex-1 space-y-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0 flex items-center gap-2">
<h4 className="font-bold text-[#0f172a] leading-tight text-xs truncate group-hover:text-[#662582] transition-colors">{prod.name}</h4>
</div>
<div>
<span className="text-[10px] text-zinc-400 font-bold font-mono tracking-tight">{prod.sku}</span>
</div>
{/* Categorized pill badge */}
<span className={`px-1.5 py-0.5 rounded text-[8px] font-extrabold uppercase shrink-0 ${
prod.category.startsWith('Staples') ? 'bg-amber-50 text-amber-600 border border-amber-100' :
prod.category.startsWith('Groceries') ? 'bg-emerald-50 text-emerald-600 border border-emerald-100' :
prod.category.startsWith('Beverages') ? 'bg-sky-50 text-sky-600 border border-sky-100' :
'bg-rose-50 text-rose-600 border border-rose-100'
}`}>
{prod.category.split(' / ')[0]}
</span>
</div>
<div className="flex justify-between items-center pt-2">
<div>
<span className="text-[8px] text-zinc-400 block uppercase tracking-wider font-extrabold">Units Sold</span>
<span className="font-extrabold text-zinc-755 font-mono text-xs">{prod.unitsSold.toLocaleString()}</span>
</div>
<div className="text-right">
<span className="text-[8px] text-zinc-400 block uppercase tracking-wider font-extrabold">Revenue</span>
<span className="font-black text-emerald-600 font-mono text-xs">₹{prod.revenue.toLocaleString()}</span>
</div>
</div>
</div>
</div>
{/* Exposure toggle row */}
<div className="flex justify-between items-center pt-2.5 border-t border-[#f1f5f9] mt-1 select-none">
<span className={`inline-flex items-center gap-1.5 text-[10px] font-bold tracking-tight ${
prod.verified ? 'text-emerald-600' : 'text-zinc-400'
}`}>
<span className={`w-1.5 h-1.5 rounded-full ${prod.verified ? 'bg-emerald-500 animate-pulse' : 'bg-zinc-300'}`} />
{prod.verified ? 'Active Portfolio' : 'Under Inspection'}
</span>
<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-8 h-4.5 bg-slate-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-350 after:border after:rounded-full after:h-3.5 after:w-3.5 after:transition-all peer-checked:bg-emerald-500 shadow-inner"></div>
</label>
</div>
{/* Store-catalogue curation: pick the product to show to store users */}
{storeCat.has(prod.id) ? (
<div className="flex items-center justify-between gap-2 pt-2.5 border-t border-[#f1f5f9] mt-1">
<span className="inline-flex items-center gap-1 text-[10px] font-bold text-emerald-600"><CheckCircle size={12} /> In Store Catalogue</span>
<button onClick={() => storeCat.remove(prod.id)} title="Remove from store catalogue" className="flex items-center gap-1 px-2 py-1 rounded-lg text-rose-500 hover:bg-rose-50 font-bold text-[10px] cursor-pointer transition-colors">
<X size={13} /> Remove
</button>
</div>
) : (
<button
onClick={() => storeCat.add({ productid: prod.id, name: prod.name, image: prod.image, category: prod.category, sku: prod.sku, price: prod.unitsSold > 0 ? Math.round(prod.revenue / prod.unitsSold) : 0, unit: prod.exposure, qty: 1 })}
disabled={!prod.verified}
className={`w-full flex items-center justify-center gap-1.5 pt-2.5 mt-1 border-t border-[#f1f5f9] text-[11px] font-bold transition-colors ${
prod.verified
? 'text-[#662582] hover:text-purple-800 cursor-pointer'
: 'text-zinc-400 cursor-not-allowed opacity-70'
}`}
title={!prod.verified ? "Product must be Active to be added" : "Add to Store Catalogue"}
>
<Plus size={13} /> Add to Store Catalogue
</button>
)}
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
@@ -916,7 +1019,7 @@ export default function InventoryView({
{/* Custom CSV Parsing Box */}
<div className="bg-white/80 backdrop-blur-md border border-[#e2e8f0] p-md rounded-2xl shadow-sm space-y-md">
<div className="flex items-center gap-xs text-[#0f172a] font-bold text-sm">
<FileSpreadsheet className="text-[#581c87]" size={18} />
<FileSpreadsheet className="text-[#662582]" size={18} />
<h3>Manual CSV Direct-Entry Console</h3>
</div>
@@ -930,7 +1033,7 @@ export default function InventoryView({
<textarea
value={csvText}
onChange={(e) => setCsvText(e.target.value)}
className="w-full h-28 p-sm font-mono text-[11px] bg-slate-50/50 outline-none focus:bg-white focus:ring-1 focus:ring-[#581c87] leading-relaxed border-none"
className="w-full h-28 p-sm font-mono text-[11px] bg-slate-50/50 outline-none focus:bg-white focus:ring-1 focus:ring-[#662582] leading-relaxed border-none"
/>
</div>
@@ -938,7 +1041,7 @@ export default function InventoryView({
<span className="text-[9px] text-zinc-400 font-extrabold uppercase">Header row ignored on parse</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 shadow-sm"
className="bg-[#662582] text-white px-xl py-2 rounded-lg text-xs font-bold uppercase tracking-wider cursor-pointer hover:bg-purple-800 transition shadow-sm"
>
Parse CSV Data & Sync
</button>
@@ -966,7 +1069,7 @@ export default function InventoryView({
<div className="bg-white/80 backdrop-blur-md border border-[#e2e8f0] p-md rounded-2xl shadow-sm space-y-md">
<div className="flex items-center gap-xs text-[#0f172a] font-bold text-sm">
<Palette className="text-[#581c87]" size={18} />
<Palette className="text-[#662582]" size={18} />
<h3>Packaging Branding Studio</h3>
</div>
@@ -987,7 +1090,7 @@ export default function InventoryView({
<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]" />
<Package size={15} className="text-[#662582]" />
Introduce New Grocery Catalogue SKU
</h4>
<button

View File

@@ -80,7 +80,7 @@ export default function LoginView({ onLogin }: LoginViewProps) {
return (
<div className="h-screen w-full flex bg-white font-sans text-slate-800 overflow-hidden">
{/* ── Left brand / hero panel (desktop only) ── */}
<div className="hidden lg:flex lg:w-1/2 shrink-0 relative overflow-hidden bg-gradient-to-br from-[#5b1d8c] via-purple-950 to-slate-950 text-white flex-col p-12 xl:p-16 justify-center">
<div className="hidden lg:flex lg:w-1/2 shrink-0 relative overflow-hidden bg-gradient-to-br from-[#662582] via-[#4c1d66] to-slate-950 text-white flex-col p-12 xl:p-16 justify-center">
{/* Layered ambient glows */}
<div className="absolute -top-24 -right-24 w-96 h-96 bg-purple-500/25 rounded-full blur-[120px] pointer-events-none" />
<div className="absolute -bottom-24 -left-24 w-[26rem] h-[26rem] bg-indigo-500/20 rounded-full blur-[140px] pointer-events-none animate-pulse" style={{ animationDuration: '9s' }} />
@@ -269,7 +269,7 @@ export default function LoginView({ onLogin }: LoginViewProps) {
<button
type="submit"
disabled={loading || checkingEmail}
className="w-full h-12 flex items-center justify-center gap-2 bg-[#581c87] hover:bg-purple-800 text-white font-bold text-sm rounded-xl shadow-sm hover:shadow-lg transition-all active:scale-[0.98] cursor-pointer border-none disabled:opacity-70 disabled:cursor-not-allowed disabled:active:scale-100"
className="w-full h-12 flex items-center justify-center gap-2 bg-[#662582] hover:bg-purple-800 text-white font-bold text-sm rounded-xl shadow-sm hover:shadow-lg transition-all active:scale-[0.98] cursor-pointer border-none disabled:opacity-70 disabled:cursor-not-allowed disabled:active:scale-100"
>
{loading || checkingEmail ? (
<>

View File

@@ -194,13 +194,13 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
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-[#662582] 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" />
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-[#662582] animate-in slide-in-from-left-2 duration-100" />
)}
</button>
))}
@@ -225,7 +225,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
{productList.length} SKUs · {locationName}
</p>
</div>
<div className="p-2.5 rounded-lg bg-purple-50 text-[#581c87]">
<div className="p-2.5 rounded-lg bg-purple-50 text-[#662582]">
<DollarSign size={18} />
</div>
</div>
@@ -312,7 +312,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
<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]'
item.status === 'Critical' ? 'bg-rose-500' : item.status === 'Low Stock' ? 'bg-amber-500' : 'bg-[#662582]'
}`}
style={{ width: `${Math.min(percentage, 100)}%` }}
/>
@@ -341,7 +341,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
});
setShowTransferModal(true);
}}
className="text-xs font-semibold text-[#581c87] hover:underline cursor-pointer"
className="text-xs font-semibold text-[#662582] hover:underline cursor-pointer"
>
Transfer
</button>
@@ -393,9 +393,9 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
<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"
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-[#662582] hover:border-[#662582] transition-all cursor-pointer group"
>
<PlusSquare size={16} className="text-zinc-400 group-hover:text-[#581c87]" />
<PlusSquare size={16} className="text-zinc-400 group-hover:text-[#662582]" />
<span className="text-[10px] font-sans font-bold uppercase">Add SKU</span>
</button>
@@ -404,9 +404,9 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
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"
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-[#662582] hover:border-[#662582] transition-all cursor-pointer group"
>
<ArrowRightLeft size={16} className="text-zinc-400 group-hover:text-[#581c87]" />
<ArrowRightLeft size={16} className="text-zinc-400 group-hover:text-[#662582]" />
<span className="text-[10px] font-sans font-bold uppercase">Transfer</span>
</button>
@@ -417,9 +417,9 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
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"
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-[#662582] hover:border-[#662582] transition-all cursor-pointer group"
>
<XCircle size={16} className="text-zinc-400 group-hover:text-[#581c87]" />
<XCircle size={16} className="text-zinc-400 group-hover:text-[#662582]" />
<span className="text-[10px] font-sans font-bold uppercase">Returns</span>
</button>
@@ -427,9 +427,9 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
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"
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-[#662582] hover:border-[#662582] transition-all cursor-pointer group"
>
<FolderSync size={16} className="text-zinc-400 group-hover:text-[#581c87]" />
<FolderSync size={16} className="text-zinc-400 group-hover:text-[#662582]" />
<span className="text-[10px] font-sans font-bold uppercase">Audit CSV</span>
</button>
</div>
@@ -476,7 +476,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
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"
className="bg-[#662582] 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>
@@ -531,7 +531,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
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>
<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-[#662582]"></div>
</label>
</td>
</tr>
@@ -552,9 +552,9 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
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"
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-[#662582] 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">
<div className="h-14 w-14 bg-purple-50 text-[#662582] rounded-full flex items-center justify-center mb-md shadow-sm">
<UploadCloud size={24} />
</div>
@@ -620,7 +620,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
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]"
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
required
/>
</div>
@@ -632,7 +632,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
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]"
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
required
/>
</div>
@@ -645,7 +645,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
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]"
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
required
/>
</div>
@@ -657,7 +657,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
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]"
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
required
/>
</div>
@@ -667,7 +667,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
<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]"
className="w-full border border-[#e2e8f0] rounded-lg p-2 bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
>
<option value="CBE-NORTH">Coimbatore North (CBE-NORTH)</option>
<option value="CBE-SOUTH">Coimbatore South (CBE-SOUTH)</option>
@@ -688,7 +688,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
</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"
className="px-4 py-2 bg-[#662582] text-white rounded-lg font-semibold hover:bg-purple-800 cursor-pointer shadow-sm"
>
Commit Ledger SKU
</button>
@@ -721,7 +721,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
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]"
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] font-mono focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
required
/>
</div>
@@ -732,7 +732,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
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]"
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-[#662582]"
required
/>
</div>
@@ -744,7 +744,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
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]"
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
required
/>
</div>
@@ -755,7 +755,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
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]"
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f1f5f9] focus:bg-white outline-none focus:ring-1 focus:ring-[#662582]"
required
/>
</div>
@@ -770,7 +770,7 @@ export default function OperationsView({ searchQuery, isCoimbatoreView }: Operat
</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"
className="px-4 py-2 bg-[#662582] text-white rounded-lg font-semibold hover:bg-purple-800 cursor-pointer shadow-sm"
>
Approve Routing
</button>

View File

@@ -21,14 +21,16 @@ import {
ShoppingBag,
AlertTriangle,
MapPin,
Calendar
Calendar,
Trophy,
Store
} from 'lucide-react';
import { LeaderboardNode } from '../types';
import {
useFiestaOrderSummary,
useFiestaLocationSummary,
useFiestaOrderInsight,
useFiestaStockStatement,
useFiestaStoresStock,
useFiestaOrderSummary,
useFiestaRevenueSummary,
useFiestaTimeSeries,
} from '../services/fiestaQueries';
@@ -91,35 +93,52 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
const prevEnd = new Date(yearStart.getTime() - 86400000);
const prevStart = new Date(prevEnd.getTime() - periodDays * 86400000);
// Helper matching local hubs belonging to Coimbatore
const isCoimbatoreNode = (name: string) => {
const coimbatoreZones = ['gandhipuram', 'rs puram', 'peelamedu', 'saravanampatti', 'coimbatore'];
return coimbatoreZones.some(zone => name.toLowerCase().includes(zone));
};
const locSummaryQ = useFiestaLocationSummary(tenantId);
const filteredLocations = useMemo(() => {
const rawLocations = [...(locSummaryQ.data ?? [])];
if (selectedRegion === 'coimbatore') {
return rawLocations.filter(r => isCoimbatoreNode(r.locationname || ''));
}
if (selectedRegion === 'chennai' || selectedRegion === 'bangalore') {
return [];
}
return rawLocations;
}, [locSummaryQ.data, selectedRegion]);
const mappedLocations = useMemo(() => {
return filteredLocations.map(l => ({ locationid: Number(l.locationid), locationname: l.locationname || '' }));
}, [filteredLocations]);
const summaryQ = useFiestaOrderSummary(tenantId, ymd(yearStart), todate);
const prevSummaryQ = useFiestaOrderSummary(tenantId, ymd(prevStart), ymd(prevEnd));
const locSummaryQ = useFiestaLocationSummary(tenantId);
const insightQ = useFiestaOrderInsight(tenantId);
const revSummaryQ = useFiestaRevenueSummary({ tenantid: tenantId, fromdate: ymd(yearStart), todate });
const prevRevSummaryQ = useFiestaRevenueSummary({ tenantid: tenantId, fromdate: ymd(prevStart), todate: ymd(prevEnd) });
const stockQ = useFiestaStockStatement({
tenantid: tenantId,
locationid: FIESTA_PRIMARY_LOCATION_ID,
keyword: '',
pageno: 1,
pagesize: 100,
});
const storesStock = useFiestaStoresStock(tenantId, mappedLocations);
const s = summaryQ.data;
const prevS = prevSummaryQ.data;
const activeSkus = (stockQ.data ?? []).length;
const revS = revSummaryQ.data;
const prevRevS = prevRevSummaryQ.data;
// Real period-over-period % change (null when we can't compute it yet).
const pctChange = (current: number, previous: number): number | null => {
if (previous <= 0) return null;
return ((current - previous) / previous) * 100;
};
const ordersDelta = s && prevS ? pctChange(s.total, prevS.total) : null;
const cancelledDelta = s && prevS ? pctChange(s.cancelled, prevS.cancelled) : null;
const revS = revSummaryQ.data;
const prevRevS = prevRevSummaryQ.data;
const revenueDelta = revS && prevRevS ? pctChange(revS.grossrevenue, prevRevS.grossrevenue) : null;
const isAllRegions = selectedRegion === 'all';
const ordersDelta = (s && prevS && isAllRegions) ? pctChange(s.total, prevS.total) : null;
const cancelledDelta = (s && prevS && isAllRegions) ? pctChange(s.cancelled, prevS.cancelled) : null;
const revenueDelta = (revS && prevRevS && isAllRegions) ? pctChange(revS.grossrevenue, prevRevS.grossrevenue) : null;
const fmtDelta = (d: number) => `${d >= 0 ? '+' : ''}${d.toFixed(1)}%`;
@@ -174,11 +193,42 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
};
const theme = getChartColors();
// Live KPI values (tenant-wide; region scaling removed — no per-region API).
const totalOrdersVal = s?.total ?? 0;
const deliveredVal = s?.delivered ?? 0;
const cancelledVal = s?.cancelled ?? 0;
const activeSkusVal = activeSkus;
let totalOrdersVal = 0;
let deliveredVal = 0;
let cancelledVal = 0;
let grossRevenueVal = 0;
let avgOrderVal = 0;
let activeSkusVal = 0;
if (isAllRegions) {
totalOrdersVal = s?.total ?? 0;
deliveredVal = s?.delivered ?? 0;
cancelledVal = s?.cancelled ?? 0;
grossRevenueVal = revS?.grossrevenue ?? 0;
avgOrderVal = revS?.avgordervalue ?? 0;
} else {
const locIds = new Set(filteredLocations.map(l => Number(l.locationid)));
(locSummaryQ.data ?? []).forEach(loc => {
if (locIds.has(Number(loc.locationid))) {
totalOrdersVal += Number(loc.total || 0);
deliveredVal += Number(loc.delivered || 0);
cancelledVal += Number(loc.cancelled || 0);
}
});
// We do not have location-level revenue from the API yet.
grossRevenueVal = 0;
avgOrderVal = 0;
}
const skuSet = new Set<string>();
storesStock.forEach(store => {
if (store.rows) {
store.rows.forEach(row => skuSet.add(`SKU-${row.productid}`));
}
});
activeSkusVal = skuSet.size;
// KPI Row Configuration. `awaiting` cards have no live value (rendered via
// AwaitingApi). `trend` is only set where a REAL delta could be derived.
@@ -197,13 +247,13 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
{
id: 'revenue' as const,
title: 'Revenue',
value: `${(revS?.grossrevenue ?? 0).toLocaleString('en-IN')}`,
value: `${grossRevenueVal.toLocaleString('en-IN')}`,
trend: revenueDelta !== null ? fmtDelta(revenueDelta) : null,
status: `${(revS?.avgordervalue ?? 0).toLocaleString('en-IN')} avg. order`,
status: `${avgOrderVal.toLocaleString('en-IN')} avg. order`,
isPositive: revenueDelta === null ? true : revenueDelta >= 0,
spark: [20, 30, 25, 45, 40, 55, 50, 68],
color: 'emerald',
awaiting: false,
awaiting: !isAllRegions,
},
{
id: 'cancelled' as const,
@@ -231,30 +281,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
},
];
// Helper matching local hubs belonging to Coimbatore
const isCoimbatoreNode = (name: string) => {
const coimbatoreZones = ['gandhipuram', 'rs puram', 'peelamedu', 'saravanampatti', 'coimbatore'];
return coimbatoreZones.some(zone => name.toLowerCase().includes(zone));
};
const getFilteredLocations = () => {
const rawLocations = [...(locSummaryQ.data ?? [])];
// Only Coimbatore can be filtered from live data; Chennai/Bangalore have no
// live tenant locations (their stub data was removed). Selecting them yields
// an empty list rather than fabricated hubs.
if (selectedRegion === 'coimbatore') {
return rawLocations.filter(r => isCoimbatoreNode(r.locationname || ''));
}
if (selectedRegion === 'chennai' || selectedRegion === 'bangalore') {
return [];
}
return rawLocations;
};
const filteredLocations = getFilteredLocations();
// Leaderboard — outlets ranked by total live orders.
const leaderboard: LeaderboardNode[] = (() => {
@@ -294,7 +321,30 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
);
// Live product performance matrix.
const liveProducts = (stockQ.data ?? []).map(stockRowToProduct);
const activeRegionStock = storesStock;
const liveProducts = useMemo(() => {
const agg = new Map<string, any>();
activeRegionStock.forEach(store => {
if (store.rows) {
store.rows.forEach(row => {
const sku = `SKU-${row.productid}`;
if (!agg.has(sku)) {
agg.set(sku, { ...row, closing: 0, debit: 0, credit: 0, opening: 0 });
}
const existing = agg.get(sku);
existing.closing += Number(row.closing || 0);
existing.debit += Number(row.debit || 0);
existing.credit += Number(row.credit || 0);
existing.opening += Number(row.opening || 0);
});
}
});
return Array.from(agg.values()).map(stockRowToProduct);
}, [activeRegionStock]);
const filteredProducts = liveProducts.filter((prod) => {
const matchesSearch =
prod.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
@@ -303,6 +353,32 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
return matchesSearch && matchesStock;
});
const ITEMS_PER_PAGE = 10;
const totalPages = Math.max(1, Math.ceil(filteredProducts.length / ITEMS_PER_PAGE));
useEffect(() => {
if (currentPage > totalPages) {
setCurrentPage(1);
}
}, [totalPages, currentPage]);
const paginatedProducts = filteredProducts.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE);
// Derived Top Products
const topOverallProducts = useMemo(() => {
return [...liveProducts].sort((a, b) => b.unitsSold - a.unitsSold).slice(0, 3);
}, [liveProducts]);
const topProductsByStore = useMemo(() => {
return activeRegionStock.map(store => {
const prods = (store.rows || []).map(stockRowToProduct);
return {
locationname: store.locationname || `Location ${store.locationid}`,
topProducts: prods.sort((a, b) => b.unitsSold - a.unitsSold).slice(0, 3)
};
}).filter(s => s.topProducts.length > 0);
}, [activeRegionStock]);
// Heatmap cell color gradient scale (multi-stop violet theme)
const getHeatmapColorClass = (val: number) => {
const ratio = val / heatmapMax;
@@ -318,7 +394,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
if (ratio <= 0.9) {
return 'bg-[#7c3aed]/20 text-[#7c3aed] border border-[#7c3aed]/20 hover:bg-[#7c3aed]/30 hover:scale-105';
}
return 'bg-gradient-to-br from-[#7c3aed] to-[#581c87] text-white border-none shadow-sm hover:scale-108 hover:shadow-md hover:brightness-110';
return 'bg-gradient-to-br from-[#7c3aed] to-[#662582] text-white border-none shadow-sm hover:scale-108 hover:shadow-md hover:brightness-110';
};
// Triggers progress bar simulated exporting
@@ -424,170 +500,9 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
</div>
{/* Action picker filters inside the banner */}
<div className="flex items-center gap-sm flex-wrap text-xs font-sans text-zinc-800">
{/* Custom Timeframe Dropdown */}
<div className="relative text-xs">
<button
onClick={() => {
setShowTimeframeDropdown(!showTimeframeDropdown);
setShowRegionDropdown(false);
}}
className="bg-slate-900/40 backdrop-blur-md border border-white/10 rounded-xl px-sm py-2 flex items-center justify-between gap-md shadow-sm font-bold text-white min-w-[135px] cursor-pointer hover:bg-slate-900/60 transition-colors"
>
<span>{selectedTimeframe}</span>
<ChevronDown size={12} className={`text-purple-300 transition-transform duration-200 ${showTimeframeDropdown ? 'rotate-180' : ''}`} />
</button>
{showTimeframeDropdown && (
<>
<div className="fixed inset-0 z-40" onClick={() => setShowTimeframeDropdown(false)} />
<div className="absolute right-0 mt-2 w-44 bg-zinc-950 border border-zinc-800 rounded-2xl shadow-xl py-2 z-50 animate-in zoom-in-95 duration-150 backdrop-blur-md text-white">
{['This Month', 'This Year (YTD)', 'Last 12 Months', 'All Time'].map((opt) => (
<button
key={opt}
onClick={() => {
setSelectedTimeframe(opt);
setShowTimeframeDropdown(false);
}}
className={`w-full text-left px-md py-2 text-xs font-semibold hover:bg-zinc-900 transition-colors ${selectedTimeframe === opt ? 'text-purple-400 bg-purple-950/40' : 'text-zinc-300'}`}
>
{opt}
</button>
))}
</div>
</>
)}
</div>
{/* Custom Region Dropdown */}
<div className="relative text-xs">
<button
onClick={() => {
setShowRegionDropdown(!showRegionDropdown);
setShowTimeframeDropdown(false);
}}
className="bg-slate-900/40 backdrop-blur-md border border-white/10 rounded-xl px-sm py-2 flex items-center justify-between gap-md shadow-sm font-bold text-white min-w-[155px] cursor-pointer hover:bg-slate-900/60 transition-colors"
>
<div className="flex items-center gap-sm">
<Filter size={12} className="text-purple-300" />
<span>
{selectedRegion === 'all' ? 'All Regions (12)' :
selectedRegion === 'coimbatore' ? 'Coimbatore (5)' :
selectedRegion === 'chennai' ? 'Chennai (4)' : 'Bangalore (3)'}
</span>
</div>
<ChevronDown size={12} className={`text-purple-300 transition-transform duration-200 ${showRegionDropdown ? 'rotate-180' : ''}`} />
</button>
{showRegionDropdown && (
<>
<div className="fixed inset-0 z-40" onClick={() => setShowRegionDropdown(false)} />
<div className="absolute right-0 mt-2 w-48 bg-zinc-950 border border-zinc-800 rounded-2xl shadow-xl py-2 z-50 animate-in zoom-in-95 duration-150 backdrop-blur-md text-white">
{[
{ id: 'all' as const, label: 'All Regions (12 Hubs)' },
{ id: 'coimbatore' as const, label: 'Coimbatore (5 Hubs)' },
{ id: 'chennai' as const, label: 'Chennai (4 Hubs)' },
{ id: 'bangalore' as const, label: 'Bangalore (3 Hubs)' }
].map((opt) => (
<button
key={opt.id}
onClick={() => {
handleRegionChange(opt.id);
setShowRegionDropdown(false);
}}
className={`w-full text-left px-md py-2 text-xs font-semibold hover:bg-zinc-900 transition-colors ${selectedRegion === opt.id ? 'text-purple-400 bg-purple-950/40' : 'text-zinc-300'}`}
>
{opt.label}
</button>
))}
</div>
</>
)}
</div>
{/* Export PDF action */}
<button
onClick={() => startExportSim('PDF')}
className="bg-[#581c87] border border-purple-500/30 text-white font-sans font-bold px-4 py-2 rounded-xl flex items-center gap-sm cursor-pointer transition-all hover:bg-purple-800 hover:shadow-lg active:bg-purple-900 shadow-sm text-xs hover:-translate-y-0.5 animate-in"
>
<Download size={13} />
Export PDF
</button>
</div>
</div>
{/* Small cards metrics grid relative to reports (similar to StoreDetailView) */}
<div className="grid grid-cols-1 sm:grid-cols-4 gap-4 mt-8 pt-6 border-t border-slate-800/80 relative z-10">
{/* Card 1: Active Region */}
<div className="bg-slate-900/40 backdrop-blur-sm rounded-xl p-4 border border-slate-800/80 hover:border-purple-500/30 transition-all group">
<div className="flex justify-between items-start">
<span className="text-[10px] text-slate-400 uppercase tracking-widest font-bold">Active Region</span>
<div className="p-2 rounded-lg bg-purple-500/10 text-purple-400 border border-purple-500/20 group-hover:scale-110 transition-transform">
<MapPin className="w-4 h-4" />
</div>
</div>
<div className="mt-2">
<h3 className="text-xl font-extrabold tracking-tight font-sans truncate">
{selectedRegion === 'all' ? 'All Regions' :
selectedRegion === 'coimbatore' ? 'Coimbatore' :
selectedRegion === 'chennai' ? 'Chennai' : 'Bangalore'}
</h3>
<p className="text-[10px] text-purple-400 font-semibold mt-1">
{filteredLocations.length} hubs active
</p>
</div>
</div>
{/* Card 2: Selected Horizon */}
<div className="bg-slate-900/40 backdrop-blur-sm rounded-xl p-4 border border-slate-800/80 hover:border-purple-500/30 transition-all group">
<div className="flex justify-between items-start">
<span className="text-[10px] text-slate-400 uppercase tracking-widest font-bold">Time Horizon</span>
<div className="p-2 rounded-lg bg-purple-500/10 text-purple-400 border border-purple-500/20 group-hover:scale-110 transition-transform">
<Calendar className="w-4 h-4" />
</div>
</div>
<div className="mt-2">
<h3 className="text-xl font-extrabold tracking-tight font-sans truncate">
{selectedTimeframe}
</h3>
<p className="text-[10px] text-slate-400 font-semibold mt-1">Historical Period</p>
</div>
</div>
{/* Card 3: Total Segment Orders */}
<div className="bg-slate-900/40 backdrop-blur-sm rounded-xl p-4 border border-slate-800/80 hover:border-purple-500/30 transition-all group">
<div className="flex justify-between items-start">
<span className="text-[10px] text-slate-400 uppercase tracking-widest font-bold">Total Orders</span>
<div className="p-2 rounded-lg bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 group-hover:scale-110 transition-transform">
<ShoppingBag className="w-4 h-4" />
</div>
</div>
<div className="mt-2">
<h3 className="text-xl font-extrabold tracking-tight font-mono">
{totalOrdersVal.toLocaleString('en-IN')}
</h3>
<p className="text-[10px] text-slate-400 font-semibold mt-1">Segment Volume</p>
</div>
</div>
{/* Card 4: Gross Revenue — no revenue API ([R1]) */}
<div className="bg-slate-900/40 backdrop-blur-sm rounded-xl p-4 border border-slate-800/80 hover:border-purple-500/30 transition-all group">
<div className="flex justify-between items-start">
<span className="text-[10px] text-slate-400 uppercase tracking-widest font-bold">Gross Revenue</span>
<div className="p-2 rounded-lg bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 group-hover:scale-110 transition-transform">
<Activity className="w-4 h-4" />
</div>
</div>
<div className="mt-2">
<h3 className="text-xl font-extrabold tracking-tight font-mono">
{(revS?.grossrevenue ?? 0).toLocaleString('en-IN')}
</h3>
<p className="text-[10px] text-slate-400 font-semibold mt-1">{(revS?.netrevenue ?? 0).toLocaleString('en-IN')} net</p>
</div>
</div>
</div>
</div>
{/* Primary KPI Row - 4 Key Tab buttons with Sparklines */}
@@ -823,7 +738,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
</span>
<div className="flex items-center gap-2 text-[9px] font-bold text-zinc-400 uppercase tracking-tight">
<span className="w-1.5 h-1.5 rounded-full bg-[#581c87] animate-pulse"></span>
<span className="w-1.5 h-1.5 rounded-full bg-[#662582] animate-pulse"></span>
<span>Busiest Month</span>
</div>
</div>
@@ -932,12 +847,12 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
<div className="flex-1 min-w-0">
<div className="flex justify-between items-center font-medium mb-1">
<span className="text-[#0f172a] font-bold truncate max-w-[12rem]">{node.name}</span>
<span className="text-[#581c87] font-mono font-bold">{node.revenue}</span>
<span className="text-[#662582] font-mono font-bold">{node.revenue}</span>
</div>
<div className="w-full bg-slate-100 h-2 rounded-full overflow-hidden">
<div
className="bg-gradient-to-r from-purple-500 via-indigo-600 to-[#581c87] h-full rounded-full transition-all duration-500 ease-out origin-left group-hover:brightness-110"
className="bg-gradient-to-r from-purple-500 via-indigo-600 to-[#662582] h-full rounded-full transition-all duration-500 ease-out origin-left group-hover:brightness-110"
style={{ width: `${node.percentage}%` }}
/>
</div>
@@ -950,14 +865,121 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
</div>
</div>
{/* Top Products Section */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-5 text-xs font-sans relative z-10 mb-8">
{/* Top 3 Overall Products - 4 Cols */}
<div className="lg:col-span-4 bg-gradient-to-br from-white/95 to-purple-50/80 backdrop-blur-xl border border-purple-100/60 rounded-[1.5rem] flex flex-col shadow-xl shadow-purple-900/5 relative overflow-hidden">
{/* Subtle glow background */}
<div className="absolute top-0 right-0 w-32 h-32 bg-purple-400/20 rounded-full blur-[40px] -mr-10 -mt-10 pointer-events-none" />
<div className="px-5 py-4 border-b border-purple-100/40 flex items-center justify-between relative z-10">
<span className="text-[13px] font-sans font-black text-slate-800 tracking-tight flex items-center gap-2">
<div className="w-6 h-6 rounded-full bg-amber-100 flex items-center justify-center shadow-inner">
<Trophy className="text-amber-500 fill-amber-100" size={12} />
</div>
Overall Top 3
</span>
</div>
<div className="p-5 flex-1 space-y-3 flex flex-col relative z-10">
{topOverallProducts.length === 0 ? (
<div className="text-center py-6 text-slate-400 font-medium text-[11px]">No sales data available.</div>
) : (
topOverallProducts.map((prod, index) => {
const isFirst = index === 0;
return (
<div key={prod.id} className={`flex items-center gap-3 text-[11px] p-2.5 rounded-xl transition-all duration-300 group cursor-default relative overflow-hidden ${isFirst ? 'bg-gradient-to-r from-amber-50 to-orange-50/30 border border-amber-200/60 shadow-sm hover:shadow-[0_4px_15px_rgba(245,158,11,0.2)] hover:-translate-y-0.5' : 'bg-white border border-slate-100/80 shadow-sm hover:shadow-md hover:border-purple-200/60 hover:-translate-y-0.5'}`}>
{/* Rank Badge */}
<div className={`w-7 h-7 rounded-full flex items-center justify-center shrink-0 shadow-inner ${isFirst ? 'bg-gradient-to-br from-amber-300 to-amber-500 text-amber-950 text-xs font-black ring-2 ring-amber-100 ring-offset-1' : index === 1 ? 'bg-gradient-to-br from-slate-200 to-slate-400 text-slate-800 text-[10px] font-bold' : 'bg-gradient-to-br from-orange-300 to-orange-500 text-orange-950 text-[10px] font-bold'}`}>
#{index + 1}
</div>
{/* Image */}
<div className={`w-10 h-10 rounded-lg overflow-hidden shrink-0 bg-white ${isFirst ? 'border-2 border-amber-300 shadow-sm' : 'border border-slate-200 shadow-sm'}`}>
<img src={prod.image} alt={prod.name} referrerPolicy="no-referrer" className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500" />
</div>
<div className="flex-1 min-w-0 py-0.5">
<div className={`font-black truncate ${isFirst ? 'text-slate-900 text-sm' : 'text-slate-800 text-xs'}`}>{prod.name}</div>
<div className="flex items-center gap-1.5 mt-1">
<span className={`text-[10px] font-black tracking-wide ${isFirst ? 'text-amber-600 bg-amber-100/50 px-1 py-0.5 rounded' : 'text-purple-600 bg-purple-50 px-1 py-0.5 rounded'}`}>
{prod.unitsSold.toLocaleString()} units
</span>
<span className="w-1 h-1 rounded-full bg-slate-300" />
<span className="text-[9px] text-slate-500 font-bold uppercase tracking-wider">{prod.category}</span>
</div>
</div>
{isFirst && (
<div className="absolute right-0 top-0 bottom-0 w-24 bg-gradient-to-l from-amber-100/40 to-transparent pointer-events-none" />
)}
</div>
);
})
)}
</div>
</div>
{/* Store-wise Top Products - 8 Cols */}
<div className="lg:col-span-8 bg-white/95 backdrop-blur-xl border border-slate-200/80 rounded-[1.5rem] flex flex-col shadow-xl shadow-slate-200/20 relative overflow-hidden">
<div className="px-5 py-4 border-b border-slate-100 flex items-center justify-between">
<span className="text-[13px] font-sans font-black text-slate-800 tracking-tight flex items-center gap-2">
<div className="w-6 h-6 rounded-full bg-indigo-50 flex items-center justify-center shadow-inner">
<Store className="text-indigo-500" size={12} />
</div>
Store-Wise Top Performers
</span>
</div>
<div className="p-5 flex-1 overflow-x-auto scrollbar-thin scrollbar-thumb-slate-300 scrollbar-track-transparent pb-4">
{topProductsByStore.length === 0 ? (
<div className="text-center py-6 text-slate-400 font-medium text-[11px] flex items-center justify-center h-full">No store data available.</div>
) : (
<div className="flex gap-4 min-w-max h-full items-stretch px-0.5 pb-1">
{topProductsByStore.map(store => (
<div key={store.locationname} className="w-[230px] rounded-[1rem] bg-gradient-to-b from-slate-50/80 to-white border border-slate-200/80 p-4 shrink-0 shadow-sm hover:shadow-[0_4px_15px_rgba(99,102,241,0.08)] hover:-translate-y-1 hover:border-indigo-200 transition-all duration-300 group">
<div className="font-black text-slate-800 text-xs mb-4 pb-3 border-b border-slate-100 flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-indigo-500 shadow-[0_0_8px_rgba(99,102,241,0.6)] animate-pulse" />
<span className="truncate">{store.locationname}</span>
</div>
<div className="space-y-3">
{store.topProducts.map((prod, idx) => (
<div key={prod.id} className="flex items-center gap-2.5 group/item">
<div className={`w-5 h-5 rounded-md flex items-center justify-center shrink-0 font-black text-[9px] shadow-sm ${idx === 0 ? 'bg-amber-100 text-amber-600' : idx === 1 ? 'bg-slate-100 text-slate-600' : 'bg-orange-50 text-orange-600'}`}>
{idx + 1}
</div>
<div className="w-9 h-9 rounded-lg border border-slate-200 bg-white shrink-0 overflow-hidden shadow-sm group-hover/item:shadow-md group-hover/item:border-indigo-200 transition-all">
<img src={prod.image} alt={prod.name} referrerPolicy="no-referrer" className="w-full h-full object-cover group-hover/item:scale-110 transition-transform duration-500" />
</div>
<div className="flex-1 min-w-0 flex flex-col justify-center">
<div className="font-bold text-slate-800 text-[11px] truncate leading-tight mb-0.5 group-hover/item:text-indigo-600 transition-colors">{prod.name}</div>
<div className="flex items-center">
<span className="text-[9px] text-indigo-700 bg-indigo-50 border border-indigo-100/50 px-1.5 py-0.5 rounded font-black tracking-wider">{prod.unitsSold.toLocaleString()} units</span>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
{/* Detailed Performance Matrix table */}
<div className="bg-white/70 backdrop-blur-md border border-[#e2e8f0] rounded-2xl overflow-hidden shadow-sm relative z-10">
{/* 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 select-none">
<div>
<h3 className="font-sans font-bold text-sm text-[#0f172a] flex items-center gap-1.5">
<Activity size={15} className="text-[#581c87]" /> Product Performance Matrix
<Activity size={15} className="text-[#662582]" /> Product Performance Matrix
</h3>
</div>
@@ -968,7 +990,7 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
key={filter}
onClick={() => setStockFilter(filter)}
className={`px-3 py-1.5 rounded-lg cursor-pointer transition-colors border ${stockFilter === filter
? 'bg-[#581c87] text-white border-[#581c87] shadow-sm'
? 'bg-[#662582] text-white border-[#662582] shadow-sm'
: 'bg-white border-[#e2e8f0] text-zinc-655 hover:bg-zinc-55'
}`}
>
@@ -1000,14 +1022,14 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
</tr>
</thead>
<tbody className="divide-y divide-[#f1f5f9]">
{filteredProducts.length === 0 ? (
{paginatedProducts.length === 0 ? (
<tr>
<td colSpan={7} className="text-center py-8 text-zinc-400 font-medium">
No matching items matching stock filter criteria.
</td>
</tr>
) : (
filteredProducts.map((prod) => {
paginatedProducts.map((prod) => {
const isExpanded = expandedProductId === prod.id;
return (
<React.Fragment key={prod.id}>
@@ -1091,33 +1113,25 @@ export default function ReportsView({ searchQuery, isCoimbatoreView, setIsCoimba
{/* Matrix table pagination */}
<div className="p-md bg-[#f8fafc] border-t border-[#e2e8f0] flex justify-between items-center text-[10px] text-zinc-500 font-bold font-sans select-none">
<span>Showing 1-{filteredProducts.length} of {liveProducts.length} live products</span>
<span>Showing {Math.min((currentPage - 1) * ITEMS_PER_PAGE + 1, filteredProducts.length)}-{Math.min(currentPage * ITEMS_PER_PAGE, filteredProducts.length)} of {filteredProducts.length} filtered products (from {liveProducts.length} total)</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"
disabled={currentPage === 1}
className={`w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center cursor-pointer transition-colors ${currentPage === 1 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-white active:bg-[#f8fafc]'}`}
>
<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>
<div className="flex items-center px-2 font-bold text-[10px] text-zinc-600">
Page {currentPage} of {totalPages}
</div>
<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-550 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-555 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"
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages}
className={`w-8 h-8 border border-[#e2e8f0] rounded-lg flex items-center justify-center cursor-pointer transition-colors ${currentPage === totalPages ? 'opacity-50 cursor-not-allowed' : 'hover:bg-white active:bg-[#f8fafc]'}`}
>
<ChevronRight size={12} />
</button>

View File

@@ -130,7 +130,8 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett
const [activeTab, setActiveTab] = useState<TabKey>('profile');
// Live tenant profile + outlets.
const tenantsQ = useFiestaAllTenants({ pagesize: 50 });
// Fetch a larger page size to ensure we find our specific tenant (ID 1087).
const tenantsQ = useFiestaAllTenants({ pagesize: 5000, status: '' });
const tenant = (tenantsQ.data ?? []).find((t) => Number(t.tenantid) === tenantId) || null;
const locationsQ = useFiestaTenantLocations(tenantId);
const outlets = locationsQ.data ?? [];
@@ -272,11 +273,11 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett
onClick={() => setActiveTab(t.key)}
className={`flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all duration-200 whitespace-nowrap cursor-pointer border-none ${
active
? 'bg-purple-50 text-[#581c87] shadow-sm border-l-2 border-purple-650'
? 'bg-purple-50 text-[#662582] shadow-sm border-l-2 border-purple-650'
: 'text-slate-600 hover:text-slate-900 hover:bg-white bg-transparent'
}`}
>
<Icon size={16} className={active ? 'text-[#581c87]' : 'text-slate-450'} />
<Icon size={16} className={active ? 'text-[#662582]' : 'text-slate-450'} />
<span>{t.label}</span>
</button>
);
@@ -411,7 +412,7 @@ export default function SettingsView({ tenantId = FIESTA_TENANT_ID, user }: Sett
<button
type="button"
onClick={() => setShowStoreOnboarding(!showStoreOnboarding)}
className="bg-[#581c87] hover:bg-purple-800 text-white px-4 py-2.5 rounded-xl text-xs font-bold uppercase tracking-wider flex items-center gap-1.5 cursor-pointer shadow-sm active:scale-95 transition-all border-none"
className="bg-[#662582] hover:bg-purple-800 text-white px-4 py-2.5 rounded-xl text-xs font-bold uppercase tracking-wider flex items-center gap-1.5 cursor-pointer shadow-sm active:scale-95 transition-all border-none"
>
{showStoreOnboarding ? 'View Store Directory' : '+ Add Store Branch'}
</button>

View File

@@ -35,14 +35,14 @@ export default function Sidebar({
const navItems = [
{ id: 'dashboard' as MainSection, label: 'Dashboard', icon: LayoutDashboard },
{ id: 'stores' as MainSection, label: 'Stores', icon: Store },
{ id: 'inventory' as MainSection, label: 'Product Catalogue', icon: Layers },
{ id: 'inventory' as MainSection, label: 'Products', icon: Layers },
{ id: 'reports' as MainSection, label: 'Reports', icon: TrendingUp },
{ 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-20 z-40 hidden md:flex transition-all duration-300 ${
className={`fixed left-0 top-0 h-screen bg-[#662582] border-r border-[#662582] text-white flex-col py-xl pt-16 z-40 hidden md:flex transition-all duration-300 ${
isOpen ? 'w-64' : 'w-20'
}`}
>
@@ -60,8 +60,8 @@ export default function Sidebar({
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'
? 'bg-black/20 text-white font-semibold' + (isOpen ? ' border-l-4 border-white' : '')
: 'text-purple-200 hover:bg-white/10 hover:text-white'
}`}
>
<IconComponent size={18} className={isActive ? 'text-white' : 'text-purple-300'} />

View File

@@ -147,7 +147,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
<div>
<h1 className="font-sans font-bold text-2xl tracking-tight text-[#0f172a]">Product Catalogue</h1>
<p className="text-zinc-500 text-xs mt-1">
Products your admin published for <span className="font-semibold text-[#581c87]">{storeName}</span> choose what you need and set quantities.
Products your admin published for <span className="font-semibold text-[#662582]">{storeName}</span> choose what you need and set quantities.
</p>
</div>
@@ -156,7 +156,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
<button
onClick={() => setView('catalogue')}
className={`flex-1 sm:flex-none flex items-center justify-center gap-1.5 px-4 py-2 rounded-lg text-xs font-bold transition-all ${
view === 'catalogue' ? 'bg-white text-[#581c87] shadow-sm' : 'text-zinc-500 hover:text-zinc-800'
view === 'catalogue' ? 'bg-white text-[#662582] shadow-sm' : 'text-zinc-500 hover:text-zinc-800'
}`}
>
<Boxes size={14} /> Browse Catalogue ({products.length})
@@ -164,7 +164,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
<button
onClick={() => setView('inventory')}
className={`flex-1 sm:flex-none flex items-center justify-center gap-1.5 px-4 py-2 rounded-lg text-xs font-bold transition-all ${
view === 'inventory' ? 'bg-white text-[#581c87] shadow-sm' : 'text-zinc-500 hover:text-zinc-800'
view === 'inventory' ? 'bg-white text-[#662582] shadow-sm' : 'text-zinc-500 hover:text-zinc-800'
}`}
>
<Store size={14} /> My Store Inventory ({inventory.length})
@@ -180,7 +180,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
placeholder={view === 'catalogue' ? 'Search catalogue products…' : 'Search your stock…'}
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full pl-9 pr-9 py-2.5 border border-[#e2e8f0] rounded-xl text-xs outline-none bg-[#f8fafc] focus:bg-white focus:ring-1 focus:ring-[#581c87] transition-all"
className="w-full pl-9 pr-9 py-2.5 border border-[#e2e8f0] rounded-xl text-xs outline-none bg-[#f8fafc] focus:bg-white focus:ring-1 focus:ring-[#662582] transition-all"
/>
{search && (
<button onClick={() => setSearch('')} className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-400 hover:text-zinc-600"><X size={13} /></button>
@@ -188,11 +188,11 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
</div>
{view === 'catalogue' && categories.length > 0 && (
<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"><Layers size={13} className="text-[#581c87]" /> Filter</span>
<span className="flex items-center gap-1.5 text-[10px] font-bold text-zinc-400 uppercase tracking-widest"><Layers size={13} className="text-[#662582]" /> Filter</span>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="border border-[#e2e8f0] rounded-lg px-3 py-2 text-xs font-semibold text-zinc-700 bg-[#f8fafc] outline-none focus:ring-1 focus:ring-[#581c87] cursor-pointer"
className="border border-[#e2e8f0] rounded-lg px-3 py-2 text-xs font-semibold text-zinc-700 bg-[#f8fafc] outline-none focus:ring-1 focus:ring-[#662582] cursor-pointer"
>
<option value="ALL">All categories</option>
{categories.map((c) => <option key={c} value={c}>{c}</option>)}
@@ -220,7 +220,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
action={
<button
onClick={() => { setSearch(''); setCategory('ALL'); }}
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-bold text-white bg-[#581c87] hover:bg-purple-800 transition shadow-sm cursor-pointer"
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-bold text-white bg-[#662582] hover:bg-purple-800 transition shadow-sm cursor-pointer"
>
<X size={13} /> Clear filters
</button>
@@ -235,7 +235,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
<div key={p.id} className="bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-2xl p-md flex flex-col justify-between gap-sm shadow-sm hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-purple-200 hover:-translate-y-0.5 transition-all duration-300 relative group">
<div className="flex gap-md">
{/* Thumbnail with hover zoom */}
<div className="w-16 h-16 rounded-xl border border-zinc-100 shrink-0 overflow-hidden bg-zinc-50 relative">
<div className="w-32 h-32 rounded-xl border border-zinc-100 shrink-0 overflow-hidden bg-zinc-50 relative">
<img src={p.image} alt={p.name} referrerPolicy="no-referrer" className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500" />
{stocked && (
<span className="absolute top-1 right-1 inline-flex items-center justify-center w-4 h-4 rounded-full bg-emerald-500 text-white shadow" title="In your store"><CheckCircle2 size={10} /></span>
@@ -244,7 +244,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
<div className="flex-1 space-y-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<h4 className="font-bold text-[#0f172a] leading-tight text-xs truncate group-hover:text-[#581c87] transition-colors">{p.name}</h4>
<h4 className="font-bold text-[#0f172a] leading-tight text-xs truncate group-hover:text-[#662582] transition-colors">{p.name}</h4>
<span className="text-[10px] text-zinc-400 font-bold font-mono tracking-tight">{p.sku}</span>
</div>
{/* Category pill badge */}
@@ -258,10 +258,6 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
<span className="text-[8px] text-zinc-400 block uppercase tracking-wider font-extrabold">Price</span>
<span className="font-extrabold text-zinc-700 font-mono text-xs">{p.price > 0 ? `${p.price.toLocaleString('en-IN')}` : '—'}</span>
</div>
<div className="text-right">
<span className="text-[8px] text-zinc-400 block uppercase tracking-wider font-extrabold">Admin Stock</span>
<span className="font-black text-emerald-600 font-mono text-xs">{p.adminQty}{p.unit ? ` ${p.unit}` : ''}</span>
</div>
</div>
</div>
</div>
@@ -278,7 +274,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
{/* Pick action: quantity stepper when selected, else add button */}
{picked ? (
<div className="flex items-center justify-between gap-2 pt-2.5 border-t border-[#f1f5f9] mt-1">
<span className="inline-flex items-center gap-1 text-[10px] font-bold text-[#581c87]"><Check size={12} /> Selected</span>
<span className="inline-flex items-center gap-1 text-[10px] font-bold text-[#662582]"><Check size={12} /> Selected</span>
<div className="flex items-center gap-1">
<button onClick={() => setPickQty(p.id, picks[p.id] - 1)} className="w-6 h-6 rounded-lg border border-[#e2e8f0] text-zinc-500 hover:bg-zinc-50 font-bold cursor-pointer leading-none flex items-center justify-center"><Minus size={12} /></button>
<span className="w-8 text-center font-mono font-bold text-xs text-[#0f172a]">{picks[p.id]}</span>
@@ -289,7 +285,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
) : (
<button
onClick={() => togglePick(p.id)}
className="w-full flex items-center justify-center gap-1.5 pt-2.5 mt-1 border-t border-[#f1f5f9] text-[11px] font-bold text-[#581c87] hover:text-purple-800 cursor-pointer"
className="w-full flex items-center justify-center gap-1.5 pt-2.5 mt-1 border-t border-[#f1f5f9] text-[11px] font-bold text-[#662582] hover:text-purple-800 cursor-pointer"
>
<Plus size={13} /> Add to Store
</button>
@@ -315,7 +311,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
title="No stock matches your search"
sub="Try a different keyword to find an item in your store."
action={
<button onClick={() => setSearch('')} className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-bold text-white bg-[#581c87] hover:bg-purple-800 transition shadow-sm cursor-pointer">
<button onClick={() => setSearch('')} className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-bold text-white bg-[#662582] hover:bg-purple-800 transition shadow-sm cursor-pointer">
<X size={13} /> Clear search
</button>
}
@@ -326,14 +322,14 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
<div key={it.id || i} className="bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-2xl p-md flex flex-col justify-between gap-sm shadow-sm hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-purple-200 hover:-translate-y-0.5 transition-all duration-300 relative group">
<div className="flex gap-md">
{/* Thumbnail with status corner dot */}
<div className="w-16 h-16 rounded-xl border border-zinc-100 shrink-0 overflow-hidden bg-zinc-50 relative">
<div className="w-32 h-32 rounded-xl border border-zinc-100 shrink-0 overflow-hidden bg-zinc-50 relative">
<img src={it.image} alt={it.name} referrerPolicy="no-referrer" className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500" />
<span className="absolute top-1 right-1 w-3 h-3 rounded-full border-2 border-white shadow" style={{ background: it.color }} title={it.label} />
</div>
<div className="flex-1 space-y-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<h4 className="font-bold text-[#0f172a] leading-tight text-xs truncate group-hover:text-[#581c87] transition-colors">{it.name}</h4>
<h4 className="font-bold text-[#0f172a] leading-tight text-xs truncate group-hover:text-[#662582] transition-colors">{it.name}</h4>
<span className="text-[10px] text-zinc-400 font-bold font-mono tracking-tight">{it.sku}</span>
</div>
<span className={`px-1.5 py-0.5 rounded text-[8px] font-extrabold uppercase shrink-0 ${catBadgeClass(it.category)}`}>
@@ -391,7 +387,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
</div>
<div className="flex items-center gap-2 shrink-0">
<button onClick={() => setPicks({})} className="px-3 py-2 rounded-xl text-[11px] font-bold text-purple-200 hover:text-white hover:bg-white/10 transition cursor-pointer">Clear</button>
<button onClick={commitSelectionToStore} className="px-4 py-2 rounded-xl text-[11px] font-bold bg-white text-[#581c87] hover:bg-purple-50 transition cursor-pointer flex items-center gap-1.5">
<button onClick={commitSelectionToStore} className="px-4 py-2 rounded-xl text-[11px] font-bold bg-white text-[#662582] hover:bg-purple-50 transition cursor-pointer flex items-center gap-1.5">
<Check size={13} /> Request for Store
</button>
</div>
@@ -415,7 +411,7 @@ function CenterState({ icon, title, sub, action }: { icon: React.ReactNode; titl
{/* Icon with halo */}
<div className="relative mb-5">
<span className="absolute inset-0 -m-3 rounded-full bg-purple-300/25 blur-xl" />
<span className="relative flex items-center justify-center w-20 h-20 rounded-3xl bg-gradient-to-br from-[#581c87] to-indigo-500 text-white shadow-lg shadow-purple-500/20 ring-8 ring-white">
<span className="relative flex items-center justify-center w-20 h-20 rounded-3xl bg-gradient-to-br from-[#662582] to-indigo-500 text-white shadow-lg shadow-purple-500/20 ring-8 ring-white">
{icon}
</span>
</div>

View File

@@ -386,7 +386,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
<div className="flex items-center justify-between mb-4">
<button
onClick={onBack}
className="flex items-center gap-xs text-xs font-bold text-[#581c87] hover:text-[#4c1d95] bg-purple-50 hover:bg-purple-100/80 px-xl py-2 rounded-lg transition-all shadow-sm border border-purple-100 cursor-pointer"
className="flex items-center gap-xs text-xs font-bold text-[#662582] hover:text-[#4c1d95] bg-purple-50 hover:bg-purple-100/80 px-xl py-2 rounded-lg transition-all shadow-sm border border-purple-100 cursor-pointer"
>
<ArrowLeft size={14} />
<span>Back to Registry</span>
@@ -505,7 +505,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
onClick={() => setActiveTab('overview')}
className={`flex items-center gap-xs px-md pb-sm text-xs font-bold uppercase tracking-wider cursor-pointer border-b-2 transition-all ${
activeTab === 'overview'
? 'border-b-[#581c87] text-[#581c87]'
? 'border-b-[#662582] text-[#662582]'
: 'border-b-transparent text-zinc-400 hover:text-zinc-600'
}`}
>
@@ -516,7 +516,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
onClick={() => setActiveTab('inventory')}
className={`flex items-center gap-xs px-md pb-sm text-xs font-bold uppercase tracking-wider cursor-pointer border-b-2 transition-all ${
activeTab === 'inventory'
? 'border-b-[#581c87] text-[#581c87]'
? 'border-b-[#662582] text-[#662582]'
: 'border-b-transparent text-zinc-400 hover:text-zinc-600'
}`}
>
@@ -530,7 +530,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
onClick={() => setActiveTab('customers')}
className={`flex items-center gap-xs px-md pb-sm text-xs font-bold uppercase tracking-wider cursor-pointer border-b-2 transition-all ${
activeTab === 'customers'
? 'border-b-[#581c87] text-[#581c87]'
? 'border-b-[#662582] text-[#662582]'
: 'border-b-transparent text-zinc-400 hover:text-zinc-600'
}`}
>
@@ -541,7 +541,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
onClick={() => setActiveTab('orders')}
className={`flex items-center gap-xs px-md pb-sm text-xs font-bold uppercase tracking-wider cursor-pointer border-b-2 transition-all ${
activeTab === 'orders'
? 'border-b-[#581c87] text-[#581c87]'
? 'border-b-[#662582] text-[#662582]'
: 'border-b-transparent text-zinc-400 hover:text-zinc-600'
}`}
>
@@ -552,7 +552,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
onClick={() => setActiveTab('qr')}
className={`flex items-center gap-xs px-md pb-sm text-xs font-bold uppercase tracking-wider cursor-pointer border-b-2 transition-all ${
activeTab === 'qr'
? 'border-b-[#581c87] text-[#581c87]'
? 'border-b-[#662582] text-[#662582]'
: 'border-b-transparent text-zinc-400 hover:text-zinc-600'
}`}
>
@@ -569,7 +569,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
{/* Top Metric Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-gutter">
<div className="bg-white border border-[#eceef2] rounded-2xl p-md shadow-sm hover:shadow-md transition-all duration-200 hover:-translate-y-0.5 relative group overflow-hidden">
<div className="w-8 h-8 rounded-xl bg-purple-50 text-[#581c87] flex items-center justify-center mb-sm group-hover:scale-110 transition-transform">
<div className="w-8 h-8 rounded-xl bg-purple-50 text-[#662582] flex items-center justify-center mb-sm group-hover:scale-110 transition-transform">
<Activity size={16} />
</div>
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest block">Monthly Revenue</span>
@@ -628,7 +628,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
<div className="flex justify-between items-start">
<div>
<h3 className="font-sans font-bold text-sm text-[#0f172a] flex items-center gap-xs">
<Clock size={15} className="text-[#581c87]" /> Dispatch Flow Pipeline
<Clock size={15} className="text-[#662582]" /> Dispatch Flow Pipeline
</h3>
<p className="text-zinc-450 text-[10px] font-sans mt-0.5">Audit orders & revenue progression by selecting nodes along the daily operational path.</p>
</div>
@@ -645,8 +645,8 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
<AreaChart data={intradayChartData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
<defs>
<linearGradient id="intraRev" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#581c87" stopOpacity={0.3} />
<stop offset="95%" stopColor="#581c87" stopOpacity={0} />
<stop offset="5%" stopColor="#662582" stopOpacity={0.3} />
<stop offset="95%" stopColor="#662582" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#eceef2" />
@@ -660,7 +660,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
<p className="text-[10px] font-bold text-zinc-400 uppercase tracking-wider mb-2">{label}</p>
<div className="flex flex-col gap-1">
<span className="flex items-center gap-2 text-xs font-medium text-zinc-600">
<span className="w-2 h-2 rounded-full bg-[#581c87]"></span>
<span className="w-2 h-2 rounded-full bg-[#662582]"></span>
Revenue: <strong className="text-zinc-900">{payload[0].value.toLocaleString('en-IN')}</strong>
</span>
<span className="flex items-center gap-2 text-xs font-medium text-zinc-600">
@@ -674,7 +674,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
return null;
}}
/>
<Area type="monotone" dataKey="revenue" stroke="#581c87" strokeWidth={2} fillOpacity={1} fill="url(#intraRev)" activeDot={{ r: 4, strokeWidth: 2, stroke: '#fff', fill: '#581c87' }} />
<Area type="monotone" dataKey="revenue" stroke="#662582" strokeWidth={2} fillOpacity={1} fill="url(#intraRev)" activeDot={{ r: 4, strokeWidth: 2, stroke: '#fff', fill: '#662582' }} />
</AreaChart>
</ResponsiveContainer>
)}
@@ -686,7 +686,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
<div className="bg-white border border-[#eceef2] rounded-2xl p-md shadow-sm space-y-md flex flex-col justify-between">
<div>
<h3 className="font-sans font-bold text-sm text-[#0f172a] flex items-center gap-xs">
<ShieldCheck size={15} className="text-[#581c87]" /> Node Operations Command
<ShieldCheck size={15} className="text-[#662582]" /> Node Operations Command
</h3>
<p className="text-zinc-450 text-[10px] font-sans mt-0.5">Automated actions for local outlet hubs.</p>
</div>
@@ -694,17 +694,17 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
<div className="space-y-sm">
<button
onClick={() => setActiveTab('inventory')}
className="w-full flex items-center justify-between p-sm border border-[#e2e8f0] rounded-xl hover:border-purple-300 hover:bg-purple-50/20 text-left text-xs font-semibold text-zinc-700 hover:text-[#581c87] transition cursor-pointer"
className="w-full flex items-center justify-between p-sm border border-[#e2e8f0] rounded-xl hover:border-purple-300 hover:bg-purple-50/20 text-left text-xs font-semibold text-zinc-700 hover:text-[#662582] transition cursor-pointer"
>
<span className="flex items-center gap-sm">
<Layers size={14} className="text-[#581c87]" /> Replenish Critical Stock
<Layers size={14} className="text-[#662582]" /> Replenish Critical Stock
</span>
<span className="px-1.5 py-0.5 rounded text-[8px] bg-rose-100 text-rose-700 font-black animate-pulse">ALERTS</span>
</button>
<button
onClick={handleExportLedger}
className="w-full flex items-center justify-between p-sm border border-[#e2e8f0] rounded-xl hover:border-purple-300 hover:bg-purple-50/20 text-left text-xs font-semibold text-zinc-700 hover:text-[#581c87] transition cursor-pointer"
className="w-full flex items-center justify-between p-sm border border-[#e2e8f0] rounded-xl hover:border-purple-300 hover:bg-purple-50/20 text-left text-xs font-semibold text-zinc-700 hover:text-[#662582] transition cursor-pointer"
>
<span className="flex items-center gap-sm">
<Download size={14} className="text-zinc-500" /> Export Compliance Ledger
@@ -714,12 +714,12 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
<button
onClick={handleStaffBroadcast}
className="w-full flex items-center justify-between p-sm border border-[#e2e8f0] rounded-xl hover:border-purple-300 hover:bg-purple-50/20 text-left text-xs font-semibold text-zinc-700 hover:text-[#581c87] transition cursor-pointer"
className="w-full flex items-center justify-between p-sm border border-[#e2e8f0] rounded-xl hover:border-purple-300 hover:bg-purple-50/20 text-left text-xs font-semibold text-zinc-700 hover:text-[#662582] transition cursor-pointer"
>
<span className="flex items-center gap-sm">
<Send size={14} className="text-zinc-500" /> Broadcast Terminal SMS
</span>
<span className="text-[8px] font-bold text-[#581c87] uppercase tracking-wider">SMS Blast</span>
<span className="text-[8px] font-bold text-[#662582] uppercase tracking-wider">SMS Blast</span>
</button>
</div>
</div>
@@ -744,7 +744,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
placeholder="Search inventory by product or SKU..."
value={stockSearch}
onChange={(e) => setStockSearch(e.target.value)}
className="w-full pl-9 pr-4 py-2.5 border border-[#e2e8f0] rounded-xl text-xs outline-none bg-[#f8fafc] focus:bg-white focus:ring-1 focus:ring-[#581c87] transition-all"
className="w-full pl-9 pr-4 py-2.5 border border-[#e2e8f0] rounded-xl text-xs outline-none bg-[#f8fafc] focus:bg-white focus:ring-1 focus:ring-[#662582] transition-all"
/>
</div>
@@ -754,7 +754,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
<>
<button
onClick={() => { setImportState('idle'); setShowImportModal(true); }}
className="px-3 py-2 bg-purple-50 text-[#581c87] hover:bg-purple-100/80 border border-purple-100 rounded-xl font-bold flex items-center gap-xs cursor-pointer transition shadow-sm"
className="px-3 py-2 bg-purple-50 text-[#662582] hover:bg-purple-100/80 border border-purple-100 rounded-xl font-bold flex items-center gap-xs cursor-pointer transition shadow-sm"
>
<UploadCloud size={14} />
<span>Import Manual (CSV)</span>
@@ -779,7 +779,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
<h4 className="font-sans font-bold text-sm text-[#0f172a]">
Product Stock Levels
</h4>
<span className="text-[10px] font-bold text-[#581c87] bg-purple-50 px-2 py-0.5 rounded border border-purple-100 uppercase tracking-wide">Live list</span>
<span className="text-[10px] font-bold text-[#662582] bg-purple-50 px-2 py-0.5 rounded border border-purple-100 uppercase tracking-wide">Live list</span>
</div>
<div className="overflow-x-auto text-xs font-sans">
@@ -870,7 +870,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
className={`px-3 py-1 rounded-lg text-[10px] font-bold hover:shadow-sm transition cursor-pointer ${
item.status === 'Critical'
? 'bg-rose-500 text-white hover:bg-rose-600'
: 'border border-zinc-200 text-zinc-700 hover:border-[#581c87] hover:text-[#581c87]'
: 'border border-zinc-200 text-zinc-700 hover:border-[#662582] hover:text-[#662582]'
}`}
>
Replenish
@@ -971,7 +971,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
</p>
</div>
{customerSearch && (
<button onClick={() => setCustomerSearch('')} className="mt-2 text-[13px] font-bold text-[#581c87] hover:text-[#4c1d95] bg-purple-50 px-4 py-2 rounded-lg transition-colors cursor-pointer">Clear Search</button>
<button onClick={() => setCustomerSearch('')} className="mt-2 text-[13px] font-bold text-[#662582] hover:text-[#4c1d95] bg-purple-50 px-4 py-2 rounded-lg transition-colors cursor-pointer">Clear Search</button>
)}
</div>
) : (
@@ -980,7 +980,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
const tone = toneFor(c.name || `c${idx}`);
const locality = localityOf(c.address);
return (
<div key={c.id ?? idx} className="group relative bg-white/70 backdrop-blur-md border border-[#e2e8f0] rounded-2xl p-5 hover:shadow-[0_12px_40px_rgba(88,28,135,0.08)] hover:border-[#581c87]/30 transition-all duration-300 flex flex-col">
<div key={c.id ?? idx} className="group relative bg-white/70 backdrop-blur-md border border-[#e2e8f0] rounded-2xl p-5 hover:shadow-[0_12px_40px_rgba(88,28,135,0.08)] hover:border-[#662582]/30 transition-all duration-300 flex flex-col">
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-3 min-w-0">
<span
@@ -990,7 +990,7 @@ export default function StoreDetailView({ store, onBack, canManage = true, only,
{initialsOf(c.name)}
</span>
<div className="min-w-0">
<h3 className="font-extrabold text-[#0f172a] text-[15px] leading-tight group-hover:text-[#581c87] transition-colors truncate" title={c.name}>{c.name}</h3>
<h3 className="font-extrabold text-[#0f172a] text-[15px] leading-tight group-hover:text-[#662582] transition-colors truncate" title={c.name}>{c.name}</h3>
{locality && (
<p className="text-[11px] text-zinc-500 mt-0.5 flex items-center gap-1 font-medium truncate" title={locality}>
<MapPin size={10} className="shrink-0 text-zinc-400" /> {locality}

View File

@@ -78,7 +78,7 @@ export default function StoreQRView({
<div className="absolute top-0 right-0 w-32 h-32 bg-purple-500/5 rounded-full blur-xl -mr-6 -mt-6 pointer-events-none group-hover:bg-purple-500/10 transition-colors" />
{/* Gradient Header banner */}
<div className="relative bg-gradient-to-br from-[#581c87] via-[#6b21a8] to-purple-950 px-6 py-8 text-center text-white overflow-hidden">
<div className="relative bg-gradient-to-br from-[#662582] via-[#6b21a8] to-purple-950 px-6 py-8 text-center text-white overflow-hidden">
<div className="absolute -top-12 -right-10 w-36 h-36 bg-purple-400/25 rounded-full blur-2xl pointer-events-none" />
<span className="relative inline-flex items-center gap-1.5 text-[9px] font-extrabold uppercase tracking-[0.16em] text-purple-100 bg-white/10 border border-white/20 rounded-full px-3.5 py-1.5 backdrop-blur-sm shadow-inner-sm">
<ScanLine size={11} className="text-purple-200 animate-pulse" /> Scan &amp; Shop
@@ -121,7 +121,7 @@ export default function StoreQRView({
<div className="w-full mt-6">
<button
onClick={downloadPng}
className="w-full flex items-center justify-center gap-2 py-3 px-5 bg-gradient-to-r from-[#581c87] to-indigo-650 hover:from-purple-800 hover:to-indigo-700 text-white rounded-xl text-xs font-bold uppercase tracking-wider cursor-pointer active:scale-95 transition-all shadow-md border-none"
className="w-full flex items-center justify-center gap-2 py-3 px-5 bg-gradient-to-r from-[#662582] to-indigo-650 hover:from-purple-800 hover:to-indigo-700 text-white rounded-xl text-xs font-bold uppercase tracking-wider cursor-pointer active:scale-95 transition-all shadow-md border-none"
>
<Download size={14} />
Download QR Code

View File

@@ -46,7 +46,7 @@ interface UserStorePageProps {
// gets a matching branch in `renderSection` below.
const NAV_ITEMS: UserNavItem[] = [
{ id: 'console', label: 'Store Console', icon: LayoutDashboard },
{ id: 'inventory', label: 'Product Catalogue', icon: Layers },
{ id: 'inventory', label: 'Products', icon: Layers },
{ id: 'customers', label: 'Customers', icon: Users },
{ id: 'dispatch', label: 'Dispatch', icon: Route },
{ id: 'reports', label: 'Reports', icon: ClipboardList },
@@ -145,7 +145,7 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) {
<p className="text-zinc-500 font-sans text-xs mb-6">Your profile and the store youre assigned to.</p>
<div className="bg-white border border-slate-200/70 rounded-2xl shadow-sm overflow-hidden">
<div className="bg-gradient-to-br from-[#581c87] via-purple-800 to-purple-950 p-6 text-white flex items-center gap-4">
<div className="bg-gradient-to-br from-[#662582] via-purple-800 to-purple-950 p-6 text-white flex items-center gap-4">
<span className="w-14 h-14 rounded-full bg-white/15 ring-2 ring-white/30 flex items-center justify-center text-lg font-bold tracking-wide">
{initials}
</span>
@@ -201,7 +201,7 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) {
if (locationsQ.isLoading || locSummaryQ.isLoading) {
return (
<div className="flex flex-col items-center justify-center gap-3 py-24">
<div className="w-7 h-7 border-2 border-[#581c87] border-t-transparent rounded-full animate-spin" />
<div className="w-7 h-7 border-2 border-[#662582] border-t-transparent rounded-full animate-spin" />
<span className="text-xs font-semibold text-slate-500">Loading your store</span>
</div>
);
@@ -225,7 +225,7 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) {
locationsQ.refetch();
locSummaryQ.refetch();
}}
className="px-5 py-2.5 bg-[#581c87] hover:bg-purple-800 text-white text-sm font-bold rounded-xl cursor-pointer transition-colors shadow-sm"
className="px-5 py-2.5 bg-[#662582] hover:bg-purple-800 text-white text-sm font-bold rounded-xl cursor-pointer transition-colors shadow-sm"
>
Retry
</button>
@@ -272,9 +272,29 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) {
onAccountClick={() => setActiveSection('account')}
onQrClick={() => setShowQrModal(true)}
profile={profile}
storeContext={{
storeName: activeSection === 'console'
? storeName
: activeSection === 'inventory'
? 'Products'
: activeSection === 'account'
? 'My Account'
: activeSection.charAt(0).toUpperCase() + activeSection.slice(1),
icon: activeSection === 'console'
? LayoutDashboard
: activeSection === 'inventory'
? Layers
: activeSection === 'customers'
? Users
: activeSection === 'dispatch'
? Route
: activeSection === 'reports'
? ClipboardList
: undefined
}}
/>
<div className="flex pt-20">
<div className="flex pt-16">
<UserStoreSidebar
items={NAV_ITEMS}
activeId={activeSection}
@@ -286,7 +306,7 @@ export default function UserStorePage({ onLogout, user }: UserStorePageProps) {
className={`flex-1 min-w-0 transition-all duration-300 ${
// Dispatch is a full-bleed cockpit — fill the area exactly (no page
// padding) so it sits flush under the header. Other pages stay padded.
activeSection === 'dispatch' ? 'h-[calc(100vh-80px)] overflow-hidden' : 'min-h-[calc(100vh-80px)]'
activeSection === 'dispatch' ? 'h-[calc(100vh-64px)] overflow-hidden' : 'min-h-[calc(100vh-64px)]'
} ${sidebarOpen ? 'md:pl-64' : 'md:pl-20'}`}
>
{activeSection === 'dispatch' ? (

View File

@@ -29,7 +29,7 @@ interface UserStoreSidebarProps {
export default function UserStoreSidebar({ items, activeId, onSelect, isOpen }: UserStoreSidebarProps) {
return (
<aside
className={`fixed left-0 top-0 h-screen bg-[#581c87] border-r border-[#4c1d95] text-white flex-col py-xl pt-20 z-40 hidden md:flex transition-all duration-300 ${
className={`fixed left-0 top-0 h-screen bg-[#662582] border-r border-[#662582] text-white flex-col py-xl pt-16 z-40 hidden md:flex transition-all duration-300 ${
isOpen ? 'w-64' : 'w-20'
}`}
>
@@ -47,8 +47,8 @@ export default function UserStoreSidebar({ items, activeId, onSelect, isOpen }:
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'
? 'bg-black/20 text-white font-semibold' + (isOpen ? ' border-l-4 border-white' : '')
: 'text-purple-200 hover:bg-white/10 hover:text-white'
}`}
>
<IconComponent size={18} className={isActive ? 'text-white' : 'text-purple-300'} />

View File

@@ -973,6 +973,20 @@ export async function createTenantLocation(input: CreateTenantLocationInput): Pr
return fiestaSend<Row>('tenants/createtenantlocation', 'POST', input);
}
export interface UpdateTenantLocationInput {
locationid: number;
tenantid?: number;
locationname?: string;
contactno?: string;
email?: string;
status?: string;
}
/** PUT /tenants/updatetenantlocation — Update store location details/status. */
export async function updateTenantLocation(input: UpdateTenantLocationInput): Promise<Row> {
return fiestaSend<Row>('tenants/updatetenantlocation', 'PUT', input);
}
// ════════════════════════════════════════════════════════════════════════════
// RIDERS / DISPATCH
// ════════════════════════════════════════════════════════════════════════════

View File

@@ -59,6 +59,8 @@ import {
createTenantLocation,
CreateTenantInput,
CreateTenantLocationInput,
updateTenantLocation,
UpdateTenantLocationInput,
} from './fiestaApi';
export const fiestaKeys = {
@@ -466,6 +468,112 @@ export function useFiestaStoresStock(
}));
}
export function useFiestaStoresOrderSummary(
tenantid: number,
locations: Array<{ locationid: number; locationname: string }>,
fromdate: string,
todate: string
) {
const results = useQueries({
queries: locations.map((loc) => ({
queryKey: fiestaKeys.orderSummary(tenantid, fromdate, todate, loc.locationid),
queryFn: () => getOrderSummary(tenantid, fromdate, todate, loc.locationid),
enabled: Boolean(tenantid && loc.locationid && fromdate && todate),
})),
});
const agg = { total: 0, created: 0, pending: 0, processing: 0, delivered: 0, cancelled: 0 };
let isLoading = false;
results.forEach(res => {
if (res.isLoading) isLoading = true;
if (res.data) {
agg.total += res.data.total;
agg.created += res.data.created;
agg.pending += res.data.pending;
agg.processing += res.data.processing;
agg.delivered += res.data.delivered;
agg.cancelled += res.data.cancelled;
}
});
return { isLoading, data: agg };
}
export function useFiestaStoresRevenueSummary(
tenantid: number,
locations: Array<{ locationid: number; locationname: string }>,
fromdate: string,
todate: string
) {
const results = useQueries({
queries: locations.map((loc) => ({
queryKey: fiestaKeys.revenueSummary({ scope: 'stores', tenantid, fromdate, todate, locationid: loc.locationid }),
queryFn: () => getRevenueSummary({ tenantid, fromdate, todate, locationid: loc.locationid }),
enabled: Boolean(tenantid && loc.locationid && fromdate && todate),
})),
});
const agg = { grossrevenue: 0, netrevenue: 0, profit: 0, ordercount: 0, avgordervalue: 0 };
let isLoading = false;
results.forEach(res => {
if (res.isLoading) isLoading = true;
if (res.data) {
agg.grossrevenue += res.data.grossrevenue;
agg.netrevenue += res.data.netrevenue;
agg.profit += res.data.profit;
agg.ordercount += res.data.ordercount;
}
});
if (agg.ordercount > 0) {
agg.avgordervalue = Math.round(agg.grossrevenue / agg.ordercount);
}
return { isLoading, data: agg };
}
export function useFiestaStoresTimeSeries(
tenantid: number,
locations: Array<{ locationid: number; locationname: string }>,
granularity: 'day' | 'month' | 'year',
fromdate: string,
todate: string
) {
const results = useQueries({
queries: locations.map((loc) => ({
queryKey: fiestaKeys.timeSeries({ scope: 'stores', tenantid, granularity, fromdate, todate, locationid: loc.locationid }),
queryFn: () => getTimeSeries({ tenantid, granularity, fromdate, todate, locationid: loc.locationid }),
enabled: Boolean(tenantid && loc.locationid && fromdate && todate),
})),
});
const map = new Map<string, any>();
let isLoading = false;
results.forEach(res => {
if (res.isLoading) isLoading = true;
if (res.data) {
res.data.forEach((d: any) => {
const label = d.label || d.date || d.createdat || 'Unknown';
if (!map.has(label)) {
map.set(label, { label, orders: 0, revenue: 0, cancelled: 0, skus: 0 });
}
const existing = map.get(label);
existing.orders += Number(d.orders || d.totalorders || d.total || 0);
existing.revenue += Number(d.revenue || d.grossrevenue || d.overallrevenue || 0);
existing.cancelled += Number(d.cancelled || 0);
existing.skus += Number(d.activeskus || d.skus || 0);
});
}
});
const sortedData = Array.from(map.values()).sort((a, b) => {
if (a.label === 'Unknown') return 1;
if (b.label === 'Unknown') return -1;
return new Date(a.label).getTime() - new Date(b.label).getTime();
});
return { isLoading, data: sortedData };
}
// ── Order details / customer history ───────────────────────────────────────────
export function useFiestaOrderDetails(orderheaderid: number | string | null | undefined) {
return useQuery({
@@ -639,6 +747,18 @@ export function useFiestaCreateLocation() {
});
}
/** Update a tenant location, then refresh tenant locations list on success. */
export function useFiestaUpdateLocation() {
const qc = useQueryClient();
return useMutation({
mutationFn: (input: UpdateTenantLocationInput) => updateTenantLocation(input),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['fiesta', 'tenantLocations'] });
qc.invalidateQueries({ queryKey: ['fiesta', 'locationSummary'] });
},
});
}
// ── Auth ──────────────────────────────────────────────────────────────────────
/**

View File

@@ -47,6 +47,7 @@ export interface ProductMatrixItem {
category: string;
exposure: string;
verified: boolean;
isNew?: boolean;
}
export interface InventoryItem {