dispatch testing

This commit is contained in:
2026-07-20 18:54:09 +05:30
parent 6b154997fb
commit 407574d62a
11 changed files with 663 additions and 518 deletions

View File

@@ -35,7 +35,9 @@ import {
Store,
Activity,
Award,
Filter
Filter,
Box,
ArrowLeft
} from 'lucide-react';
import { ProductMatrixItem } from '../types';
import {
@@ -45,13 +47,14 @@ import {
useFiestaUpdateStockRequest,
useFiestaGetStockRequests,
} from '../services/fiestaQueries';
import { FIESTA_TENANT_ID, str as fstr } from '../services/fiestaApi';
import { FIESTA_TENANT_ID, FIESTA_PRIMARY_LOCATION_ID, str as fstr } from '../services/fiestaApi';
import { stockRowToProduct, stockRowToInventory } from '../services/fiestaMappers';
import { useStoreCatalogue } from '../services/storeCatalogue';
import BulkCartDrawer from './BulkCartDrawer';
import AwaitingApi from './AwaitingApi';
import { SlideDrawer, Skeleton, TH_STYLE, SURFACE_ALT, TEXT, TEXT_2, TEXT_3, BORDER, BRAND, tint, edge, StatusChip } from './consoleUi';
import FMCGHoverOverlay from './FMCGHoverOverlay';
import CatalogueBrowser from './CatalogueBrowser';
import { useCompare } from '../contexts/CompareContext';
@@ -75,6 +78,7 @@ export default function InventoryView({
}: InventoryViewProps) {
const { setHideCompareBar } = useCompare();
const [searchTerm, setSearchTerm] = useState('');
const [showCatalogueModal, setShowCatalogueModal] = useState(false);
const navigate = useNavigate();
// ── Live stock across every outlet (Fiesta) ───────────────────────────────
@@ -108,7 +112,7 @@ export default function InventoryView({
const allStoreRows = storesStock.flatMap((s) => s.rows);
const [activeTab, setActiveTab] = useState<'catalog' | 'requests'>('catalog');
const [activeTab, setActiveTab] = useState<'catalog' | 'requests' | 'global_catalogue'>('catalog');
const [isLocalSidebarOpen, setIsLocalSidebarOpen] = useState(true);
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [hoveredAdminProduct, setHoveredAdminProduct] = useState<ProductMatrixItem | null>(null);
@@ -119,19 +123,22 @@ export default function InventoryView({
const [addingPriceProdId, setAddingPriceProdId] = useState<string | null>(null);
const [cardImportPrice, setCardImportPrice] = useState<string>('');
const selectedProductId = selectedAdminProduct?.id;
useEffect(() => {
if (selectedAdminProduct) {
const storeItem = storeCat.items.find(i => i.productid === selectedAdminProduct.id);
const existingPrice = storeItem?.price || selectedAdminProduct.price;
setImportPrice(
selectedAdminProduct.price
? String(selectedAdminProduct.price)
: String(selectedAdminProduct.unitsSold > 0 ? Math.round(selectedAdminProduct.revenue / selectedAdminProduct.unitsSold) : 0)
existingPrice && existingPrice > 0
? String(existingPrice)
: (selectedAdminProduct.unitsSold > 0 ? String(Math.round(selectedAdminProduct.revenue / selectedAdminProduct.unitsSold)) : '')
);
setIsSettingPrice(false);
} else {
setImportPrice('');
setIsSettingPrice(false);
}
}, [selectedAdminProduct]);
}, [selectedProductId]);
const products = useMemo(() => {
const byId = new Map<string, ProductMatrixItem>();
@@ -513,7 +520,7 @@ export default function InventoryView({
)}
</button>
<button
onClick={() => navigate('/admin/catalogue')}
onClick={() => setActiveTab('global_catalogue')}
className="flex items-center gap-1.5 bg-[#662582] hover:bg-purple-800 text-white px-2.5 py-1.5 rounded-lg text-[10px] font-bold transition-colors shadow-sm cursor-pointer h-[32px] whitespace-nowrap"
>
<Plus size={12} /> Import Product
@@ -544,8 +551,11 @@ export default function InventoryView({
{/* Left Side: Normal Catalogue */}
<div className="flex-1 min-w-0">
<div className={`grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 ${!isLocalSidebarOpen ? (!isSidebarOpen ? 'xl:grid-cols-5 2xl:grid-cols-6' : 'xl:grid-cols-4 2xl:grid-cols-5') : (!isSidebarOpen ? 'xl:grid-cols-4 2xl:grid-cols-5' : 'xl:grid-cols-3 2xl:grid-cols-4')} gap-4`}>
{filteredProducts.map((prod) => (
<div key={prod.id} onClick={() => setSelectedAdminProduct(prod)} className="cursor-pointer bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-2xl flex flex-col shadow-sm hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-[#662582]/40 hover:-translate-y-1 transition-all duration-300 relative group overflow-hidden">
{filteredProducts.map((prod) => {
const isPublished = storeCat.has(prod.id) && storeCat.items.find(i => i.productid === prod.id)?.status === 'Active';
return (
<div key={prod.id} onClick={() => setSelectedAdminProduct(prod)} className="bg-white/80 backdrop-blur-md border border-[#e2e8f0] rounded-none flex flex-col shadow-sm hover:shadow-[0_12px_24px_rgba(99,102,241,0.06)] hover:border-[#662582]/40 hover:-translate-y-1 transition-all duration-300 relative group overflow-hidden cursor-pointer">
{/* Image Section - Top */}
<div className="w-full h-48 bg-white p-4 relative overflow-hidden shrink-0 border-b border-slate-100 flex items-center justify-center">
<img
@@ -554,82 +564,51 @@ export default function InventoryView({
referrerPolicy="no-referrer"
className="w-full h-full object-contain mix-blend-multiply group-hover:scale-110 transition-transform duration-700 ease-in-out"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-900/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="absolute top-2 left-2 flex flex-col items-start gap-1">
<div className="flex items-center gap-2">
<span className={`px-1.5 py-0.5 rounded text-[8px] font-extrabold uppercase shadow-sm ${
prod.category.startsWith('Staples') ? 'bg-amber-100 text-amber-800' :
prod.category.startsWith('Groceries') ? 'bg-emerald-100 text-emerald-800' :
prod.category.startsWith('Beverages') ? 'bg-sky-100 text-sky-800' :
'bg-rose-100 text-rose-800'
}`}>
{prod.category.split(' / ')[0]}
<div className="absolute inset-0 bg-gradient-to-t from-slate-900/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none" />
{prod.isSample && (
<div className="absolute top-2 left-2 flex flex-col items-start gap-1 z-10">
<span className="px-1.5 py-0.5 rounded-none bg-indigo-100 text-indigo-800 text-[8px] font-extrabold uppercase shadow-sm flex items-center gap-1 border border-indigo-200">
<Package size={10} /> SAMPLE
</span>
{prod.isSample && (
<span className="px-1.5 py-0.5 rounded bg-indigo-100 text-indigo-800 text-[8px] font-extrabold uppercase shadow-sm flex items-center gap-1 border border-indigo-200">
<Package size={10} /> SAMPLE
</span>
)}
</div>
</div>
)}
</div>
{/* Cannot be removed from local catalogue if it is active in the store catalogue */}
{!(storeCat.has(prod.id) && storeCat.items.find(i => i.productid === prod.id)?.status === 'Active') && (
<button
onClick={(e) => {
e.stopPropagation();
if (storeCat.has(prod.id)) {
storeCat.remove(prod.id);
}
}}
className="absolute top-2 right-2 w-7 h-7 bg-white/90 border border-slate-200 rounded-lg flex items-center justify-center text-slate-400 hover:text-rose-500 hover:border-rose-200 hover:bg-rose-50 shadow-sm z-20 opacity-0 group-hover:opacity-100 transition-all cursor-pointer"
title="Remove from Product Catalogue"
>
<Trash2 size={14} />
</button>
)}
{/* Content Section */}
<div className="p-3 flex flex-col flex-1 gap-2">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0 pb-2">
<h4 className="font-bold text-[#0f172a] text-xs leading-snug group-hover:text-[#662582] transition-colors line-clamp-2">{prod.name}</h4>
<p className="text-[10px] text-zinc-500 font-bold font-mono tracking-tight mt-1">{prod.sku}</p>
</div>
<div className="flex-1 min-w-0 pb-2">
<h4 className="font-bold text-[#0f172a] text-xs leading-snug group-hover:text-[#662582] transition-colors line-clamp-2" title={prod.name}>{prod.name}</h4>
<p className="text-[10px] text-zinc-500 font-bold font-mono tracking-tight mt-1">{prod.sku}</p>
</div>
<div className="grid grid-cols-2 gap-1.5 p-2 bg-slate-50/80 rounded-xl border border-slate-100 mt-auto">
<div>
<span className="text-[9px] text-zinc-400 block uppercase tracking-wider font-extrabold mb-0.5">Units Sold</span>
<span className="font-extrabold text-slate-800 font-mono text-xs">{prod.unitsSold.toLocaleString()}</span>
<div className="mt-auto pt-2 border-t border-slate-100/50 flex flex-col gap-2">
<div className="flex items-center justify-between text-[10px] text-slate-500 font-medium">
<span>Sold: <strong className="text-slate-800 font-mono">{prod.unitsSold.toLocaleString()}</strong></span>
<span className="font-extrabold font-mono text-emerald-600">{prod.revenue.toLocaleString()}</span>
</div>
<div className="text-right">
<span className="text-[9px] text-zinc-400 block uppercase tracking-wider font-extrabold mb-0.5">Revenue</span>
<span className="font-black text-emerald-600 font-mono text-xs">₹{prod.revenue.toLocaleString()}</span>
</div>
</div>
<div className="space-y-2 pt-1 border-t border-slate-100">
{storeCat.has(prod.id) && storeCat.items.find(i => i.productid === prod.id)?.status === 'Active' ? (
<div className="flex flex-wrap items-center justify-between gap-2 p-2 bg-emerald-50 rounded-xl border border-emerald-100/50">
<span className="inline-flex items-center gap-1.5 text-[10px] font-bold text-emerald-700 whitespace-nowrap"><CheckCircle size={13} /> In Store Catalogue</span>
<button onClick={(e) => { e.stopPropagation(); storeCat.remove(prod.id); }} title="Remove from store catalogue" className="flex items-center gap-1 px-2.5 py-1.5 rounded-lg bg-white text-rose-600 hover:bg-rose-50 shadow-sm border border-rose-100 font-bold text-[10px] cursor-pointer transition-colors whitespace-nowrap">
<X size={12} /> Remove
</button>
</div>
{isPublished ? (
<button
onClick={(e) => { e.stopPropagation(); storeCat.remove(prod.id); }}
title="Remove from store catalogue"
className="w-full bg-emerald-50 hover:bg-rose-50 text-emerald-700 hover:text-rose-600 border border-emerald-200 hover:border-rose-200 px-3 py-2 rounded-none text-[10px] font-bold uppercase tracking-wider transition-all shadow-sm flex items-center justify-center gap-1.5 cursor-pointer"
>
<CheckCircle size={12} />
Remove from Store
</button>
) : addingPriceProdId === prod.id ? (
<div className="flex flex-col gap-2 p-2 bg-slate-50 border border-slate-200 rounded-xl" onClick={(e) => e.stopPropagation()}>
<label className="text-[10px] font-extrabold text-slate-600 uppercase tracking-widest text-center">Set Selling Price (₹)</label>
<div className="flex flex-col gap-1.5 p-2 bg-slate-50 border border-slate-200 rounded-none" onClick={(e) => e.stopPropagation()}>
<label className="text-[9px] font-extrabold text-slate-600 uppercase tracking-widest text-center">Set Selling Price (₹)</label>
<input
type="number"
value={cardImportPrice}
onChange={(e) => setCardImportPrice(e.target.value)}
placeholder="e.g. 50"
className="px-2 py-1.5 border border-slate-200 rounded-md text-[11px] font-bold focus:outline-none focus:border-[#662582]"
className="px-2 py-1 border border-slate-200 rounded-none text-[10px] font-bold focus:outline-none focus:border-[#662582]"
autoFocus
/>
<div className="flex gap-1.5">
<button onClick={(e) => { e.stopPropagation(); setAddingPriceProdId(null); }} className="flex-1 py-1 rounded bg-white text-slate-500 border border-slate-200 hover:bg-slate-100 text-[10px] font-bold transition-colors">Cancel</button>
<div className="flex gap-1">
<button onClick={(e) => { e.stopPropagation(); setAddingPriceProdId(null); }} className="flex-1 py-1 bg-white text-slate-500 border border-slate-200 text-[9px] font-bold">Cancel</button>
<button
disabled={!cardImportPrice || Number(cardImportPrice) <= 0}
onClick={(e) => {
@@ -637,7 +616,7 @@ export default function InventoryView({
storeCat.add({ productid: prod.id, name: prod.name, image: prod.image, category: prod.category, sku: prod.sku, price: Number(cardImportPrice), unit: prod.exposure, qty: 1, status: 'Active' });
setAddingPriceProdId(null);
}}
className="flex-1 py-1 rounded bg-[#662582] text-white hover:bg-[#531e6a] text-[10px] font-bold disabled:opacity-50 transition-colors"
className="flex-1 py-1 bg-[#662582] text-white text-[9px] font-bold disabled:opacity-50"
>
Confirm
</button>
@@ -650,16 +629,17 @@ export default function InventoryView({
setCardImportPrice(String(prod.price || (prod.unitsSold > 0 ? Math.round(prod.revenue / prod.unitsSold) : 0)));
setAddingPriceProdId(prod.id);
}}
className="w-full flex items-center justify-center gap-1.5 py-2.5 rounded-xl text-[11px] font-bold transition-all bg-[#662582]/5 text-[#662582] hover:bg-[#662582] hover:text-white cursor-pointer shadow-sm border border-[#662582]/20"
className="w-full bg-[#f8fafc] hover:bg-[#662582] text-slate-600 hover:text-white border border-slate-200 hover:border-[#662582] px-3 py-2 rounded-none text-[10px] font-bold uppercase tracking-wider transition-all shadow-sm flex items-center justify-center gap-1.5 cursor-pointer"
title="Add to Store Catalogue"
>
<Plus size={14} /> Add to Store Catalogue
<Plus size={12} /> Add to Store
</button>
)}
</div>
</div>
</div>
))}
);
})}
</div>
</div>
</div>
@@ -859,6 +839,14 @@ export default function InventoryView({
</div>
)}
</div>
) : activeTab === 'global_catalogue' ? (
<div className="flex-1 flex flex-col min-h-0 overflow-hidden animate-in fade-in duration-300">
<CatalogueBrowser
tenantid={tenantId}
locationid={FIESTA_PRIMARY_LOCATION_ID}
onClose={() => setActiveTab('catalog')}
/>
</div>
) : null}
{/* ── Slide Drawer ── */}
@@ -917,47 +905,71 @@ export default function InventoryView({
)}
</div>
{/* Action Area */}
<div className="space-y-3 pt-4 border-t border-slate-100">
<div className="space-y-3">
{storeCat.has(selectedAdminProduct.id) ? (
<button onClick={() => storeCat.remove(selectedAdminProduct.id)} className="w-full flex items-center justify-center gap-2 py-3.5 rounded-xl bg-white text-rose-600 border border-slate-200 font-semibold text-sm transition-colors hover:bg-slate-50">
<X size={18} strokeWidth={2} /> Remove from Store Catalogue
</button>
) : (
<div className="flex flex-col gap-3 p-4 bg-slate-50 border border-slate-200 rounded-xl">
<div className="flex flex-col gap-1.5">
<label className="text-xs font-semibold text-slate-600">Set Selling Price ()</label>
<input
type="number"
value={importPrice}
onChange={(e) => setImportPrice(e.target.value)}
placeholder="e.g. 50"
className="px-3 py-2 border border-slate-200 rounded-lg text-sm focus:outline-none focus:border-[#662582]"
/>
</div>
<button
disabled={!importPrice || Number(importPrice) <= 0}
onClick={() => {
storeCat.add({ productid: selectedAdminProduct.id, name: selectedAdminProduct.name, image: selectedAdminProduct.image, category: selectedAdminProduct.category, sku: selectedAdminProduct.sku, price: Number(importPrice) || (selectedAdminProduct.unitsSold > 0 ? Math.round(selectedAdminProduct.revenue / selectedAdminProduct.unitsSold) : 0), unit: selectedAdminProduct.exposure, qty: 1, status: 'Active' });
}}
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] disabled:opacity-50 disabled:cursor-not-allowed">
<Plus size={18} strokeWidth={2} /> Add to Store Catalogue
</button>
</div>
)}
{!storeCat.has(selectedAdminProduct.id) && (
<button
onClick={() => {
storeCat.remove(selectedAdminProduct.id);
setSelectedAdminProduct(null);
}}
className="w-full flex items-center justify-center gap-2 py-3.5 rounded-xl bg-white text-rose-600 border border-slate-200 font-semibold text-sm transition-colors hover:bg-slate-50 mt-2"
>
<Trash2 size={18} strokeWidth={2} /> Delete from Local Catalogue
</button>
)}
{/* Selling Price & Store Catalogue Action Area */}
<div className="bg-slate-50/90 border border-slate-200 rounded-2xl p-5 space-y-4 shadow-xs">
<div className="flex items-center justify-between border-b border-slate-200 pb-3">
<div>
<h4 className="font-extrabold text-slate-800 text-sm flex items-center gap-2">
<Tag size={16} className="text-[#662582]" /> Store Selling Price ()
</h4>
<p className="text-[10px] font-semibold text-slate-500 mt-0.5">
{storeCat.has(selectedAdminProduct.id) ? 'Currently active in store catalogue' : 'Set selling price to publish in store catalogue'}
</p>
</div>
{storeCat.has(selectedAdminProduct.id) && (
<span className="px-2 py-0.5 rounded bg-emerald-100 text-emerald-800 text-[10px] font-extrabold uppercase border border-emerald-200 flex items-center gap-1">
<CheckCircle size={10} /> Active
</span>
)}
</div>
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-1.5" onClick={(e) => e.stopPropagation()}>
<label className="text-[10px] font-extrabold text-slate-500 uppercase tracking-wider">Selling Price ()</label>
<div className="relative">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 font-bold pointer-events-none"></span>
<input
type="number"
step="0.01"
value={importPrice}
onChange={(e) => setImportPrice(e.target.value)}
onClick={(e) => e.stopPropagation()}
placeholder="e.g. 50"
className="w-full pl-7 pr-3 py-2.5 border border-slate-200 rounded-xl text-sm font-bold text-slate-900 focus:outline-none focus:ring-2 focus:ring-[#662582]/20 focus:border-[#662582] bg-white transition-all shadow-xs"
/>
</div>
</div>
<button
disabled={!importPrice || Number(importPrice) <= 0}
onClick={() => {
storeCat.add({
productid: selectedAdminProduct.id,
name: selectedAdminProduct.name,
image: selectedAdminProduct.image,
category: selectedAdminProduct.category,
sku: selectedAdminProduct.sku,
price: Number(importPrice),
unit: selectedAdminProduct.exposure || 'Pc',
qty: 1,
status: 'Active'
});
}}
className="w-full flex items-center justify-center gap-2 py-3 rounded-xl text-xs font-extrabold transition-all bg-[#662582] hover:bg-[#531e6a] text-white shadow-md disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
>
<CheckCircle size={16} strokeWidth={2.5} />
{storeCat.has(selectedAdminProduct.id) ? 'Save Price to Database' : 'Save Price & Publish to Store'}
</button>
{storeCat.has(selectedAdminProduct.id) && (
<button
onClick={() => storeCat.remove(selectedAdminProduct.id)}
className="w-full flex items-center justify-center gap-2 py-2.5 rounded-xl bg-white text-rose-600 border border-slate-200 font-bold text-xs transition-colors hover:bg-slate-50 cursor-pointer mt-1"
>
<X size={14} strokeWidth={2} /> Remove from Store Catalogue
</button>
)}
</div>
</div>
<div className="mt-2">