udpates on the ui changes on theprodut catalogs
This commit is contained in:
117
src/App.tsx
117
src/App.tsx
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ? (
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 & 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
|
||||
|
||||
@@ -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 you’re 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' ? (
|
||||
|
||||
@@ -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'} />
|
||||
|
||||
@@ -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
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────────────────────────
|
||||
/**
|
||||
|
||||
@@ -47,6 +47,7 @@ export interface ProductMatrixItem {
|
||||
category: string;
|
||||
exposure: string;
|
||||
verified: boolean;
|
||||
isNew?: boolean;
|
||||
}
|
||||
|
||||
export interface InventoryItem {
|
||||
|
||||
Reference in New Issue
Block a user