implemented the sales and revenue report

This commit is contained in:
2026-06-23 19:18:01 +05:30
parent 8707004405
commit 93df333df1
17 changed files with 2410 additions and 712 deletions

View File

@@ -20,14 +20,14 @@
*/
import React, { useEffect, useMemo, useState } from 'react';
import { Search, Boxes, Layers, Plus, Minus, Check, CheckCircle2, X, Store, PackageSearch, Activity } from 'lucide-react';
import { useFiestaStockStatement, FIESTA_TENANT_ID } from '../services/fiestaQueries';
import { num as fnum, str as fstr, type Row } from '../services/fiestaApi';
import { Search, Boxes, Layers, Plus, Minus, Check, CheckCircle2, X, Store, PackageSearch, Activity, Info, Inbox } from 'lucide-react';
import { useFiestaStockStatement, useFiestaCreateStockRequest, FIESTA_TENANT_ID } from '../services/fiestaQueries';
import { num as fnum, str as fstr, type Row, FIESTA_PRIMARY_LOCATION_ID } from '../services/fiestaApi';
import { categoryName } from '../services/fiestaMappers';
import { useStoreCatalogue } from '../services/storeCatalogue';
import AwaitingApi from './AwaitingApi';
import { SlideDrawer } from './consoleUi';
import { useCompare } from '../contexts/CompareContext';
import { SlideDrawer, StatusChip, TH_STYLE, SURFACE_ALT, TEXT, TEXT_2, TEXT_3, BORDER, BRAND } from './consoleUi';
import FMCGHoverOverlay from './FMCGHoverOverlay';
const PLACEHOLDER = 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&q=80&w=200';
@@ -46,7 +46,7 @@ function stockStatus(closing: number): { label: string; color: string } {
/** Category → pill badge classes (mirrors the admin Global Catalogue card). */
function catBadgeClass(category: string): string {
const c = category.toLowerCase();
const c = String(category || '').toLowerCase();
if (c.startsWith('staple')) return 'bg-amber-50 text-amber-600 border border-amber-100';
if (c.includes('grocer')) return 'bg-emerald-50 text-emerald-600 border border-emerald-100';
if (c.includes('beverage')) return 'bg-sky-50 text-sky-600 border border-sky-100';
@@ -54,13 +54,15 @@ function catBadgeClass(category: string): string {
}
export default function StoreCatalogView({ locationid, storeName = 'your store', tenantId = FIESTA_TENANT_ID }: StoreCatalogViewProps) {
const { selectedProducts, toggleProduct, setIsComparing } = useCompare();
const tenantid = tenantId;
const [view, setView] = useState<'catalogue' | 'inventory'>('catalogue');
const [view, setView] = useState<'catalogue' | 'inventory' | 'requests'>('catalogue');
const [search, setSearch] = useState('');
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [stockHealthFilter, setStockHealthFilter] = useState<string[]>([]);
const [selectedProduct, setSelectedProduct] = useState<any>(null);
const [hoveredProduct, setHoveredProduct] = useState<any>(null);
const [activeQtyProduct, setActiveQtyProduct] = useState<any>(null);
const [tempQty, setTempQty] = useState(1);
const [notice, setNotice] = useState(false);
// The admin-curated catalogue (what the user is allowed to pick from).
@@ -80,38 +82,109 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
[storeCat.items],
);
// The user's picks: productid → quantity they need. Persisted per store.
// The user's picks: productid → request data. Persisted per store.
const storageKey = `nearledaily.catalogue.request.${locationid ?? 'na'}`;
const [picks, setPicks] = useState<Record<string, number>>(() => {
const [picks, setPicks] = useState<Record<string, { qty: number; status: 'Pending' | 'Approved' | 'Rejected'; requestedAt: string; resolvedAt?: string }>>(() => {
try {
const raw = localStorage.getItem(storageKey);
return raw ? (JSON.parse(raw) as Record<string, number>) : {};
if (!raw) return {};
const parsed = JSON.parse(raw);
// Migrate old format (Record<string, number>) to new format
const migrated: Record<string, any> = {};
for (const [k, v] of Object.entries(parsed)) {
if (typeof v === 'number') {
migrated[k] = { qty: v, status: 'Pending', requestedAt: new Date().toISOString() };
} else {
migrated[k] = v;
if (migrated[k].status === 'Approve') migrated[k].status = 'Approved';
if (migrated[k].status === 'Reject') migrated[k].status = 'Rejected';
}
}
return migrated;
} catch {
return {};
}
});
// Listen for storage events so approvals from Admin update dynamically
useEffect(() => {
try { localStorage.setItem(storageKey, JSON.stringify(picks)); } catch { /* ignore */ }
const handler = (e: StorageEvent) => {
if (e.key === storageKey) {
if (e.newValue) {
const parsed = JSON.parse(e.newValue);
for (const key of Object.keys(parsed)) {
if (parsed[key].status === 'Approve') parsed[key].status = 'Approved';
if (parsed[key].status === 'Reject') parsed[key].status = 'Rejected';
}
setPicks(parsed);
}
else setPicks({});
}
};
window.addEventListener('storage', handler);
return () => window.removeEventListener('storage', handler);
}, [storageKey]);
useEffect(() => {
localStorage.setItem(storageKey, JSON.stringify(picks));
}, [picks, storageKey]);
const createRequestMutation = useFiestaCreateStockRequest();
const togglePick = (id: string) => {
setNotice(false);
setPicks((prev) => {
const next = { ...prev };
if (next[id] != null) delete next[id];
else next[id] = 1;
if (next[id] != null && next[id].status !== 'Cancelled') {
next[id] = { ...next[id], status: 'Cancelled', resolvedAt: new Date().toISOString() };
} else {
next[id] = { qty: 1, status: 'Pending', requestedAt: new Date().toISOString() };
createRequestMutation.mutate({
tenantid,
locationid: locationid ?? FIESTA_PRIMARY_LOCATION_ID,
productid: Number(id),
qty: 1,
status: 'Pending'
});
}
return next;
});
};
const setPickQty = (id: string, qty: number) => setPicks((prev) => ({ ...prev, [id]: Math.max(1, Math.round(qty) || 1) }));
const setPickQty = (id: string, qty: number) => setPicks((prev) => {
const existing = prev[id] || {};
const safeQty = Math.max(1, Math.round(qty) || 1);
createRequestMutation.mutate({
tenantid,
locationid: locationid ?? FIESTA_PRIMARY_LOCATION_ID,
productid: Number(id),
qty: safeQty,
status: 'Pending'
});
return {
...prev,
[id]: {
...existing,
qty: safeQty,
status: 'Pending',
requestedAt: new Date().toISOString()
}
};
});
const pickCount = Object.keys(picks).length;
// Store inventory (live stock) for the "My Store Inventory" tab + "In Store" tags.
const stockQ = useFiestaStockStatement({ tenantid, locationid: locationid ?? 0, pagesize: 200 });
const inStore = useMemo(() => new Set((stockQ.data ?? []).map((r) => fstr(r.productid))), [stockQ.data]);
const inStore = useMemo(() => {
const set = new Set((stockQ.data ?? []).map((r) => fstr(r.productid)));
Object.entries(picks).forEach(([pid, data]: [string, any]) => {
if (data.status === 'Approved') set.add(pid);
});
return set;
}, [stockQ.data, picks]);
const inventory = useMemo(
() =>
(stockQ.data ?? []).map((r: Row) => {
() => {
const baseInventory = (stockQ.data ?? []).map((r: Row) => {
const closing = fnum(r.closing) ?? 0;
return {
id: fstr(r.productid),
@@ -121,10 +194,45 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
category: categoryName(fnum(r.categoryid)),
closing,
...stockStatus(closing),
price: Math.floor(Math.random() * 50) + 10, // mock price
qty: closing, // fallback if actual qty not mapped
};
}),
[stockQ.data],
});
// Merge approved picks into the inventory
const inventoryMap = new Map(baseInventory.map(item => [item.id, item]));
Object.entries(picks).forEach(([pid, data]: [string, any]) => {
if (data.status === 'Approved') {
if (inventoryMap.has(pid)) {
const item = inventoryMap.get(pid)!;
item.qty += data.qty;
item.closing += data.qty;
Object.assign(item, stockStatus(item.closing));
} else {
// Find product info from catalogue
const prod = products.find(p => p.id === pid);
if (prod) {
inventoryMap.set(pid, {
id: prod.id,
name: prod.name,
sku: prod.sku,
image: prod.image,
category: prod.category,
...stockStatus(data.qty),
price: prod.price,
qty: data.qty,
closing: data.qty
});
}
}
}
});
return Array.from(inventoryMap.values());
},
[stockQ.data, picks, products],
);
const filteredInventory = useMemo(() => {
const term = search.toLowerCase();
if (!term) return inventory;
@@ -160,9 +268,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
}, [filteredInventory, selectedCategories, stockHealthFilter]);
// ── Integration point ──────────────────────────────────────────────────────────
// Replace with the real request/stock POST (selected productids + quantities),
// then invalidate stockQ.
const commitSelectionToStore = () => setNotice(true);
// The request is saved to localStorage automatically via the useEffect on `picks`.
return (
<div className="space-y-lg animate-in fade-in duration-300 font-sans pb-28">
@@ -186,15 +292,24 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
>
<Store size={14} /> My Store Inventory ({inventory.length})
</button>
<button
onClick={() => setView('requests')}
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 === 'requests' ? 'bg-white text-[#662582] shadow-sm' : 'text-zinc-500 hover:text-zinc-800'
}`}
>
<Inbox size={14} /> My Requests ({Object.keys(picks).length})
</button>
</div>
<div className="flex flex-col md:flex-row gap-6 items-start mt-6">
{/* Sticky Sidebar Filter */}
<div className="w-full md:w-64 shrink-0 bg-white border border-slate-200 rounded-2xl p-5 sticky top-24 shadow-[0_4px_20px_-4px_rgba(0,0,0,0.05)] z-10 hidden md:flex flex-col gap-6">
<div>
<h3 className="text-[11px] font-extrabold text-slate-400 uppercase tracking-widest mb-3 flex items-center gap-1.5">
<Search size={14} className="text-[#662582]" /> Search
</h3>
{view !== 'requests' && (
<div className="w-full md:w-64 shrink-0 bg-white border border-slate-200 rounded-2xl p-5 sticky top-24 shadow-[0_4px_20px_-4px_rgba(0,0,0,0.05)] z-10 hidden md:flex flex-col gap-6">
<div>
<h3 className="text-[11px] font-extrabold text-slate-400 uppercase tracking-widest mb-3 flex items-center gap-1.5">
<Search size={14} className="text-[#662582]" /> Search
</h3>
<div className="relative">
<input
type="text"
@@ -271,6 +386,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
</button>
)}
</div>
)}
{/* Product Grid Area */}
<div className="flex-1 min-w-0 max-h-[850px] overflow-y-auto custom-scrollbar pr-4">
@@ -297,11 +413,13 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
<div className="flex justify-between items-end mb-4 px-1">
<div>
<h2 className="text-lg font-bold text-slate-900">
{view === 'catalogue' ? 'Global Catalogue' : 'My Inventory'}
{view === 'catalogue' ? 'Store Catalogue' : view === 'inventory' ? 'My Inventory' : 'My Requests'}
</h2>
<p className="text-xs text-slate-500 mt-0.5">
Showing {view === 'catalogue' ? filtered.length : finalFilteredInventory.length} results
</p>
{view !== 'requests' && (
<p className="text-xs text-slate-500 mt-0.5">
Showing {view === 'catalogue' ? filtered.length : finalFilteredInventory.length} results
</p>
)}
</div>
{/* Can add sorting dropdown here if needed */}
</div>
@@ -324,9 +442,10 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 pb-8">
{filtered.map((p) => {
const stocked = inStore.has(p.id);
const picked = picks[p.id] != null;
const pick = picks[p.id];
const picked = pick != null && pick.status !== 'Cancelled' && pick.status !== 'Approved';
return (
<div key={p.id} onClick={() => setSelectedProduct(p)} className="bg-white border border-slate-200 rounded-2xl flex flex-col shadow-sm hover:shadow-xl hover:border-purple-300 hover:-translate-y-1.5 transition-all duration-300 relative group cursor-pointer">
<div key={p.id} onClick={() => setSelectedProduct(p)} className="bg-white border border-slate-200 rounded-2xl flex flex-col shadow-sm hover:shadow-xl hover:border-purple-300 hover:-translate-y-1.5 transition-all duration-300 relative group cursor-pointer overflow-hidden">
{/* Image Bento Block */}
<div className="w-full h-32 bg-slate-50 rounded-t-2xl relative overflow-hidden shrink-0 border-b border-slate-100 p-2">
<img src={p.image} alt={p.name} referrerPolicy="no-referrer" className="w-full h-full object-cover rounded-xl group-hover:scale-105 transition-transform duration-500 ease-out" />
@@ -353,31 +472,6 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
<h4 className="font-bold text-slate-900 text-xs leading-snug group-hover:text-purple-700 transition-colors line-clamp-2">{p.name}</h4>
<p className="text-[10px] text-slate-400 font-bold font-mono tracking-tight mt-1">{p.sku}</p>
</div>
<div
className="shrink-0"
onClick={(e) => {
e.stopPropagation();
toggleProduct({
id: p.id,
name: p.name,
sku: p.sku,
category: p.category.split(' / ')[0],
price: p.price,
image: p.image,
unit: p.unit
});
}}
>
<label className="relative flex items-center justify-center cursor-pointer group/cb">
<input
type="checkbox"
className="peer appearance-none w-5 h-5 bg-slate-50 border border-slate-300 rounded-md checked:bg-[#662582] checked:border-[#662582] transition-all"
checked={selectedProducts.some(sp => String(sp.id) === String(p.id))}
readOnly
/>
<Check size={12} className="absolute text-white opacity-0 peer-checked:opacity-100 pointer-events-none" strokeWidth={3} />
</label>
</div>
</div>
<div className="mt-auto bg-slate-50 rounded-xl p-2.5 border border-slate-100 flex justify-between items-center">
@@ -394,18 +488,35 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
{/* Action Bento Block */}
<div className="pt-1">
{picked ? (
<div className="flex items-center justify-between gap-1 p-1.5 bg-emerald-50 rounded-xl border border-emerald-200 shadow-inner">
<span className="inline-flex items-center justify-center w-6 h-6 rounded-lg bg-emerald-100 text-emerald-600"><Check size={14} /></span>
<div className="flex items-center gap-1 bg-white rounded-lg p-0.5 shadow-sm border border-emerald-100">
<button onClick={(e) => { e.stopPropagation(); setPickQty(p.id, picks[p.id] - 1); }} className="w-6 h-6 rounded-md bg-slate-50 hover:bg-slate-100 text-slate-600 font-bold flex items-center justify-center transition-colors"><Minus size={12} /></button>
<span className="w-6 text-center font-mono font-bold text-xs text-slate-900">{picks[p.id]}</span>
<button onClick={(e) => { e.stopPropagation(); setPickQty(p.id, picks[p.id] + 1); }} className="w-6 h-6 rounded-md bg-slate-50 hover:bg-slate-100 text-slate-600 font-bold flex items-center justify-center transition-colors"><Plus size={12} /></button>
<div className={`flex items-center justify-between gap-1 p-1.5 rounded-xl border shadow-inner ${
picks[p.id].status === 'Approved' ? 'bg-emerald-50 border-emerald-200' :
picks[p.id].status === 'Rejected' ? 'bg-rose-50 border-rose-200' :
'bg-amber-50 border-amber-200'
}`}>
<span className={`inline-flex items-center gap-1.5 px-2 text-[11px] font-bold ${
picks[p.id].status === 'Approved' ? 'text-emerald-700' :
picks[p.id].status === 'Rejected' ? 'text-rose-700' :
'text-amber-700'
}`}>
{picks[p.id].status === 'Approved' ? <CheckCircle2 size={13} /> :
picks[p.id].status === 'Rejected' ? <X size={13} /> :
<div className="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse" />}
{picks[p.id].status === 'Pending' ? 'Requested' : picks[p.id].status} ({picks[p.id].qty})
</span>
<div className="flex gap-1">
{picks[p.id].status === 'Pending' && (
<button onClick={(e) => { e.stopPropagation(); setTempQty(picks[p.id].qty); setActiveQtyProduct(p); }} title="Edit Quantity" className="px-2 h-7 rounded-lg text-amber-600 hover:bg-amber-100 bg-white border border-amber-100 flex items-center justify-center transition-colors shadow-sm text-[10px] font-bold">Edit</button>
)}
<button onClick={(e) => { e.stopPropagation(); togglePick(p.id); }} title="Remove" className={`w-7 h-7 rounded-lg bg-white border flex items-center justify-center transition-colors shadow-sm ${
picks[p.id].status === 'Approved' ? 'text-emerald-500 hover:bg-emerald-50 hover:text-emerald-600 border-emerald-100' :
picks[p.id].status === 'Rejected' ? 'text-rose-500 hover:bg-rose-50 hover:text-rose-600 border-rose-100' :
'text-amber-500 hover:bg-amber-50 hover:text-amber-600 border-amber-100'
}`}><X size={14} /></button>
</div>
<button onClick={(e) => { e.stopPropagation(); togglePick(p.id); }} title="Remove" className="w-7 h-7 rounded-lg text-rose-500 hover:bg-rose-50 hover:text-rose-600 bg-white border border-rose-100 flex items-center justify-center transition-colors shadow-sm ml-1"><X size={14} /></button>
</div>
) : (
<button
onClick={(e) => { e.stopPropagation(); togglePick(p.id); }}
onClick={(e) => { e.stopPropagation(); setTempQty(1); setActiveQtyProduct(p); }}
className="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl text-[11px] font-bold transition-all bg-white text-purple-700 hover:bg-purple-600 hover:text-white hover:shadow-md border border-purple-200 hover:border-purple-600"
>
<Plus size={14} /> Add to Store
@@ -420,6 +531,77 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
)
)}
{/* ── My Requests ── */}
{view === 'requests' && (
Object.keys(picks).length === 0 ? (
<CenterState
icon={<Inbox size={34} />}
title="No stock requested yet"
sub="Browse the catalogue and add items to your store to request stock."
/>
) : (
<div className="bg-white border rounded-2xl overflow-hidden" style={{ borderColor: BORDER }}>
<div className="overflow-x-auto">
<table className="w-full" style={{ minWidth: 800 }}>
<thead>
<tr>
{['Requested At', 'Product', 'Qty', 'Status', 'Resolved At'].map((h, i) => (
<th key={i} className="px-3 py-2.5 text-left" style={TH_STYLE}>{h}</th>
))}
</tr>
</thead>
<tbody>
{Object.entries(picks).map(([pid, data]: [string, any]) => {
const prod = products.find(p => p.id === pid);
const isApproved = data.status === 'Approved';
const isRejected = data.status === 'Rejected';
const color = isApproved ? '#10b981' : isRejected ? '#f43f5e' : data.status === 'Cancelled' ? '#94a3b8' : '#f59e0b';
const DIVIDER_C = '#f1f5f9';
return (
<tr key={pid} className="transition-colors" style={{ borderBottom: `1px solid ${DIVIDER_C}`, background: 'transparent' }}
onMouseEnter={(e) => { e.currentTarget.style.background = SURFACE_ALT; }} onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}>
<td className="px-3 py-2.5">
<span className="text-xs font-mono" style={{ color: TEXT_3 }}>
{data.requestedAt ? new Date(data.requestedAt).toLocaleString('en-IN', { dateStyle: 'medium', timeStyle: 'short' }) : '—'}
</span>
</td>
<td className="px-3 py-2.5">
<div className="flex items-center gap-3">
{prod ? (
<>
<img src={prod.image} alt={prod.name} className="w-8 h-8 rounded-md object-cover border" style={{ borderColor: BORDER }} />
<div>
<p className="font-bold text-[12px] truncate max-w-[200px]" style={{ color: TEXT }}>{prod.name}</p>
<p className="text-[10px] truncate max-w-[200px]" style={{ color: TEXT_2 }}>{prod.sku}</p>
</div>
</>
) : (
<span className="text-xs text-slate-400">Product Not Found ({pid})</span>
)}
</div>
</td>
<td className="px-3 py-2.5 font-mono text-[12px]" style={{ color: TEXT }}>
{data.qty || '—'}
</td>
<td className="px-3 py-2.5">
<StatusChip label={data.status || '—'} color={color} />
</td>
<td className="px-3 py-2.5">
<span className="text-xs font-mono" style={{ color: TEXT_3 }}>
{data.resolvedAt ? new Date(data.resolvedAt).toLocaleString('en-IN', { dateStyle: 'medium', timeStyle: 'short' }) : '—'}
</span>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
)
)}
{/* ── My Store Inventory ── */}
{view === 'inventory' && (
stockQ.isLoading ? (
@@ -442,7 +624,7 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
) : (
<div className="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{finalFilteredInventory.map((it, i) => (
<div key={it.id || i} onClick={() => setSelectedProduct(it)} className="bg-white border border-slate-200 rounded-2xl flex flex-col shadow-sm hover:shadow-xl hover:border-purple-300 hover:-translate-y-1.5 transition-all duration-300 relative group cursor-pointer">
<div key={it.id || i} onClick={() => setSelectedProduct(it)} className="bg-white border border-slate-200 rounded-2xl flex flex-col shadow-sm hover:shadow-xl hover:border-purple-300 hover:-translate-y-1.5 transition-all duration-300 relative group cursor-pointer overflow-hidden">
{/* Image Bento Block */}
<div className="w-full h-32 bg-slate-50 rounded-t-2xl relative overflow-hidden shrink-0 border-b border-slate-100 p-2">
<img src={it.image} alt={it.name} referrerPolicy="no-referrer" className="w-full h-full object-cover rounded-xl group-hover:scale-105 transition-transform duration-500 ease-out" />
@@ -467,34 +649,6 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
<h4 className="font-bold text-slate-900 text-xs leading-snug group-hover:text-purple-700 transition-colors line-clamp-2">{it.name}</h4>
<p className="text-[10px] text-slate-400 font-bold font-mono tracking-tight mt-1">{it.sku}</p>
</div>
<div
className="shrink-0"
onClick={(e) => {
e.stopPropagation();
toggleProduct({
id: it.id,
name: it.name,
sku: it.sku,
category: it.category.split(' / ')[0],
price: it.price,
image: it.image,
closing: it.closing,
label: it.label,
color: it.color,
unit: it.unit
});
}}
>
<label className="relative flex items-center justify-center cursor-pointer group/cb">
<input
type="checkbox"
className="peer appearance-none w-5 h-5 bg-slate-50 border border-slate-300 rounded-md checked:bg-[#662582] checked:border-[#662582] transition-all"
checked={selectedProducts.some(sp => String(sp.id) === String(it.id))}
readOnly
/>
<Check size={12} className="absolute text-white opacity-0 peer-checked:opacity-100 pointer-events-none" strokeWidth={3} />
</label>
</div>
</div>
<div className="mt-auto bg-slate-50 rounded-xl p-3 border border-slate-100 text-center">
@@ -510,95 +664,235 @@ export default function StoreCatalogView({ locationid, storeName = 'your store',
</div>
</div>
{/* ── Selection FAB ── */}
{view === 'catalogue' && pickCount > 0 && (
{/* ── Auto-Submit Toast ── */}
{notice && (
<div className="fixed bottom-6 right-6 z-[120]">
{notice ? (
<div className="bg-[#0f172a] text-white rounded-2xl shadow-2xl border border-white/10 px-5 py-4 w-80 animate-in slide-in-from-bottom-4 duration-300">
<div className="flex items-center justify-between mb-3">
<span className="text-xs font-bold">{pickCount} product{pickCount > 1 ? 's' : ''} requested</span>
<button onClick={() => { setPicks({}); setNotice(false); }} className="text-[11px] font-semibold text-purple-200 hover:text-white cursor-pointer">Clear</button>
</div>
<AwaitingApi label="Submitting to store" api="stock-request API" compact className="bg-white/5 border-white/15 text-purple-100" />
<div className="bg-[#0f172a] text-white rounded-2xl shadow-2xl border border-white/10 px-5 py-4 w-80 animate-in slide-in-from-bottom-4 duration-300">
<div className="flex items-center justify-between mb-3">
<span className="text-xs font-bold">{pickCount} product{pickCount > 1 ? 's' : ''} requested</span>
<button onClick={() => { setPicks({}); setNotice(false); }} className="text-[11px] font-semibold text-purple-200 hover:text-white cursor-pointer transition-colors">Clear All</button>
</div>
) : (
<button
onClick={commitSelectionToStore}
className="flex items-center gap-3 bg-gradient-to-r from-purple-700 to-[#662582] text-white px-6 py-4 rounded-full shadow-[0_8px_30px_rgba(102,37,130,0.4)] hover:shadow-[0_12px_40px_rgba(102,37,130,0.6)] hover:-translate-y-1 transition-all duration-300 cursor-pointer group"
>
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-white/20 text-white font-bold text-sm shadow-inner group-hover:scale-110 transition-transform">
{pickCount}
</div>
<div className="text-left">
<span className="block text-sm font-bold uppercase tracking-wider">Save Picks</span>
<span className="block text-[10px] text-purple-200">{Object.values(picks).reduce((a: number, b: number) => a + b, 0)} items total</span>
</div>
<div className="ml-2 w-8 h-8 rounded-full bg-white text-purple-700 flex items-center justify-center shadow-md">
<Check size={16} strokeWidth={3} />
</div>
</button>
)}
<AwaitingApi label="Request submitted to Admin" api="stock-request API" compact className="bg-white/5 border-white/15 text-emerald-300" />
</div>
</div>
)}
{/* ADD/EDIT PRODUCT MODAL (simulated) */}
<SlideDrawer
isOpen={!!selectedProduct}
onClose={() => setSelectedProduct(null)}
title={view === 'catalogue' ? 'Catalogue Product Details' : 'Store Inventory Status'}
>
{selectedProduct && (
<div className="flex flex-col gap-5">
<div className="w-full h-64 bg-slate-50 rounded-2xl overflow-hidden border border-slate-100 relative">
<img src={selectedProduct.image} alt={selectedProduct.name} className="w-full h-full object-cover" />
<div className="absolute top-3 left-3">
<span className={`px-2.5 py-1.5 rounded-lg text-[10px] font-black uppercase shadow-sm tracking-wider ${catBadgeClass(selectedProduct.category)}`}>
{selectedProduct.category.split(' / ')[0]}
</span>
{/* QUANTITY SELECTION CENTERED MODAL */}
{activeQtyProduct && (
<div className="fixed inset-0 z-[300] flex items-center justify-center p-4">
<div
className="absolute inset-0 bg-slate-900/60 backdrop-blur-md animate-in fade-in duration-300"
onClick={() => setActiveQtyProduct(null)}
/>
<div className="relative w-[340px] max-w-[95vw] bg-white rounded-2xl shadow-[0_24px_60px_rgba(0,0,0,0.15)] border border-white/50 p-5 animate-in zoom-in-[0.97] duration-300 flex flex-col gap-4">
{/* Header */}
<div className="flex justify-between items-center pb-2 border-b border-slate-100/80">
<div>
<h2 className="text-base font-bold text-slate-900 tracking-tight">Request Stock</h2>
<p className="text-[9px] font-bold text-slate-400 uppercase tracking-widest mt-0.5">Bulk Order</p>
</div>
</div>
<div>
<h3 className="text-xl font-black text-slate-900 leading-tight mb-2">{selectedProduct.name}</h3>
<p className="text-xs font-bold text-slate-400 font-mono tracking-wider">{selectedProduct.sku}</p>
</div>
<div className="bg-slate-50 p-4 rounded-xl border border-slate-200 flex justify-between items-center">
<div>
<span className="text-[10px] font-extrabold uppercase tracking-widest text-slate-400 block mb-1">Pricing</span>
<span className="text-2xl font-black font-mono text-slate-900">
{selectedProduct.price > 0 ? `${selectedProduct.price.toLocaleString('en-IN')}` : '—'}
</span>
</div>
<div className="text-right">
<span className="text-[10px] font-extrabold uppercase tracking-widest text-slate-400 block mb-1">Unit</span>
<span className="text-sm font-bold text-slate-600 bg-white px-3 py-1 rounded-lg border border-slate-200 shadow-sm">{selectedProduct.unit || 'Piece'}</span>
</div>
<button
onClick={() => setActiveQtyProduct(null)}
className="w-7 h-7 flex items-center justify-center rounded-full bg-slate-50 hover:bg-slate-100 text-slate-400 hover:text-slate-700 transition-colors"
>
<X size={14} strokeWidth={2.5} />
</button>
</div>
{selectedProduct.closing !== undefined && (
<div className="bg-purple-50 p-4 rounded-xl border border-purple-100 flex justify-between items-center">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full flex items-center justify-center shadow-inner" style={{ backgroundColor: selectedProduct.color, color: 'white' }}>
<Boxes size={18} />
</div>
<div>
<span className="text-[10px] font-extrabold uppercase tracking-widest text-purple-400 block mb-0.5">Live Stock Level</span>
<span className="text-sm font-black text-purple-900 flex items-center gap-1.5">
{selectedProduct.label}
</span>
</div>
</div>
<span className="text-2xl font-black font-mono text-purple-700">{selectedProduct.closing.toLocaleString('en-IN')}</span>
</div>
)}
{/* Product Card summary */}
<div className="flex gap-3 items-center bg-gradient-to-r from-slate-50 to-white p-2.5 rounded-xl border border-slate-100 shadow-sm">
<div className="w-12 h-12 rounded-lg bg-white border border-slate-200 overflow-hidden shrink-0 shadow-sm">
<img src={activeQtyProduct.image} alt={activeQtyProduct.name} className="w-full h-full object-cover" />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-slate-900 text-xs leading-tight truncate">{activeQtyProduct.name}</h3>
<p className="text-[9px] font-mono font-medium text-slate-400 mt-0.5">{activeQtyProduct.sku}</p>
<div className="mt-1 inline-flex items-center gap-1 bg-purple-50 px-1.5 py-0.5 rounded text-[9px] font-semibold text-purple-700 border border-purple-100/50">
<span>{activeQtyProduct.price.toLocaleString('en-IN')}</span>
<span className="text-purple-300">/</span>
<span>{activeQtyProduct.unit || 'Pc'}</span>
</div>
</div>
</div>
{/* Quantity Selector */}
<div className="flex flex-col gap-4">
<div>
<label className="text-[9px] font-bold uppercase tracking-widest text-slate-400 block mb-2">Quick Select</label>
<div className="grid grid-cols-4 gap-2">
{[10, 25, 50, 100].map(qty => (
<button
key={qty}
onClick={() => setTempQty(qty)}
className={`relative overflow-hidden py-1.5 rounded-lg text-[11px] font-bold transition-all duration-300 ${
tempQty === qty
? 'bg-[#662582] text-white shadow-sm ring-1 ring-[#662582] ring-offset-1'
: 'bg-white text-slate-600 border border-slate-200 hover:border-[#662582]/40 hover:bg-purple-50/50'
}`}
>
{qty}
</button>
))}
</div>
</div>
<div>
<label className="text-[9px] font-bold uppercase tracking-widest text-slate-400 block mb-2">Custom Amount</label>
<div className="relative group">
<div className="absolute inset-y-0 left-3 flex items-center pointer-events-none">
<Boxes size={14} className="text-slate-400 group-focus-within:text-[#662582] transition-colors" />
</div>
<input
type="number"
min="1"
value={tempQty}
onChange={(e) => setTempQty(Math.max(1, parseInt(e.target.value) || 1))}
className="w-full h-10 pl-9 pr-12 text-right text-lg font-bold font-mono text-[#662582] bg-slate-50/50 rounded-lg border border-slate-200 outline-none focus:border-[#662582] focus:bg-white transition-all shadow-inner"
/>
<div className="absolute inset-y-0 right-3 flex items-center pointer-events-none">
<span className="text-[9px] font-bold text-slate-400 uppercase tracking-widest pl-2 border-l border-slate-200">Units</span>
</div>
</div>
</div>
</div>
{/* Total & Submit */}
<div className="mt-1 pt-4 border-t border-slate-100">
<div className="flex justify-between items-end mb-4 px-1">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Estimated Total</span>
<span className="text-xl font-bold text-slate-900 tracking-tight">
{(tempQty * activeQtyProduct.price).toLocaleString('en-IN')}
</span>
</div>
<button
onClick={() => {
setPickQty(activeQtyProduct.id, tempQty);
setActiveQtyProduct(null);
setNotice(true);
setTimeout(() => setNotice(false), 3000);
}}
className="w-full flex items-center justify-center gap-2 py-3 rounded-xl text-xs font-bold transition-all bg-gradient-to-r from-[#662582] to-purple-800 text-white shadow-[0_4px_12px_rgba(102,37,130,0.2)] hover:shadow-[0_6px_16px_rgba(102,37,130,0.3)] hover:-translate-y-0.5"
>
<CheckCircle2 size={14} strokeWidth={2.5} /> Confirm Request
</button>
</div>
<p className="text-sm text-slate-500 leading-relaxed mt-2 border-t border-slate-100 pt-5">
This is a shared product available in the Global Catalogue. Make sure to keep adequate stock to prevent customer order cancellations due to unavailability.
</p>
</div>
)}
</div>
)}
{/* Floating FMCG Details Panel */}
{hoveredProduct && (
<div className="fixed bottom-6 right-6 z-[200] pointer-events-none">
<div className="relative w-[320px] max-w-[90vw] shadow-[0_24px_60px_rgba(0,0,0,0.15)] rounded-2xl animate-in slide-in-from-bottom-4 fade-in duration-200">
<FMCGHoverOverlay productId={hoveredProduct.id} category={hoveredProduct.category} productName={hoveredProduct.name} />
</div>
</div>
)}
{/* ── Slide Drawer for Product Details ── */}
<SlideDrawer
isOpen={selectedProduct !== null}
onClose={() => setSelectedProduct(null)}
title="Product Details"
>
{selectedProduct && (() => {
const stocked = inStore.has(selectedProduct.id);
const pick = picks[selectedProduct.id];
const isPending = pick != null && pick.status === 'Pending';
const isApproved = pick != null && pick.status === 'Approved';
const isRejected = pick != null && pick.status === 'Rejected';
const isRequested = pick != null && pick.status !== 'Cancelled';
return (
<div className="flex flex-col gap-8 pb-8">
{/* Clean Image Container */}
<div className="w-full h-64 bg-white rounded-2xl border border-slate-200 overflow-hidden relative flex items-center justify-center p-4">
<img src={selectedProduct.image} alt={selectedProduct.name} className="w-auto h-full max-w-full object-contain mix-blend-multiply" />
</div>
{/* Title & Basics */}
<div>
<p className="text-xs font-semibold text-slate-500 tracking-wider mb-1">SKU: {selectedProduct.sku}</p>
<h3 className="text-2xl font-bold text-slate-900 leading-tight mb-3">{selectedProduct.name}</h3>
<div className="flex flex-wrap items-center gap-2">
<span className={`px-2.5 py-1 rounded bg-slate-100 text-slate-600 text-[10px] font-semibold uppercase tracking-widest border border-slate-200`}>
{String(selectedProduct.category || '').split(' / ')[0]}
</span>
{stocked && (
<span className="px-2.5 py-1 rounded bg-emerald-50 text-emerald-700 text-[10px] font-semibold uppercase tracking-widest border border-emerald-200 flex items-center gap-1.5">
<CheckCircle2 size={12} strokeWidth={2.5} /> In Store
</span>
)}
</div>
</div>
{/* Clean Pricing Card */}
<div className="bg-slate-50 p-6 rounded-2xl border border-slate-200 flex items-center justify-between">
<div>
<span className="text-xs font-semibold text-slate-500 tracking-wider uppercase block mb-1">Store Price</span>
<div className="flex items-end gap-1.5">
<span className="text-3xl font-bold text-slate-900 tracking-tight">{selectedProduct.price?.toLocaleString('en-IN') || 0}</span>
<span className="text-sm font-semibold text-slate-500 mb-1">/ {selectedProduct.unit || 'Pc'}</span>
</div>
</div>
</div>
{/* Action Area */}
<div className="space-y-4 pt-4 border-t border-slate-100">
<h4 className="text-sm font-semibold text-slate-800">Stock Management</h4>
{isApproved ? (
<div className="flex items-center justify-center gap-2 p-4 bg-emerald-50 rounded-xl border border-emerald-200">
<CheckCircle2 size={18} className="text-emerald-600" />
<span className="text-sm font-semibold text-emerald-800">Approved and stocked in your inventory</span>
</div>
) : isRejected ? (
<div className="flex flex-col gap-3">
<div className="flex items-center justify-center gap-2 p-4 bg-rose-50 rounded-xl border border-rose-200">
<X size={18} className="text-rose-600" />
<span className="text-sm font-semibold text-rose-800">Stock request was rejected</span>
</div>
<button onClick={() => { setSelectedProduct(null); togglePick(selectedProduct.id); }} className="w-full flex items-center justify-center gap-2 py-3.5 rounded-xl bg-white text-slate-700 hover:bg-slate-50 shadow-sm border border-slate-200 font-semibold text-sm transition-colors">
Clear Request
</button>
</div>
) : isPending ? (
<div className="flex flex-col gap-3">
<div className="flex items-center justify-between p-4 bg-amber-50 rounded-xl border border-amber-200">
<div className="flex items-center gap-2.5">
<div className="w-2 h-2 rounded-full bg-amber-500 animate-pulse" />
<span className="text-sm font-semibold text-amber-800">Requested <span className="font-bold">({pick.qty} units)</span></span>
</div>
<button onClick={() => { setSelectedProduct(null); setTempQty(pick.qty); setActiveQtyProduct(selectedProduct); }} className="px-4 py-2 rounded-lg text-amber-700 bg-white hover:bg-amber-100 border border-amber-200 text-sm font-semibold transition-colors shadow-sm">
Edit
</button>
</div>
<button onClick={() => { setSelectedProduct(null); togglePick(selectedProduct.id); }} className="w-full flex items-center justify-center gap-2 py-3.5 rounded-xl bg-white text-rose-600 hover:bg-rose-50 border border-slate-200 font-semibold text-sm transition-colors">
<X size={18} strokeWidth={2} /> Cancel Request
</button>
</div>
) : (
<button onClick={() => { setSelectedProduct(null); setTempQty(1); setActiveQtyProduct(selectedProduct); }} className="w-full flex items-center justify-center gap-2 py-3.5 rounded-xl text-sm font-semibold transition-colors bg-[#662582] text-white hover:bg-[#531e6a]">
<Plus size={18} strokeWidth={2} /> Request Stock
</button>
)}
</div>
{/* Retail Packaging Info */}
<div className="mt-4 pt-4 border-t border-slate-100">
<h4 className="text-sm font-semibold text-slate-800 mb-3">Retail Packaging Info</h4>
<FMCGHoverOverlay productId={selectedProduct.id} category={selectedProduct.category} productName={selectedProduct.name} />
</div>
</div>
);
})()}
</SlideDrawer>
</div>
);
}