dispatch page

This commit is contained in:
Gokul
2026-06-12 14:45:06 +05:30
parent d8c1517239
commit 5378f2df1f
34 changed files with 4451 additions and 1744 deletions

View File

@@ -38,6 +38,7 @@ import {
} from '../services/fiestaQueries';
import { FIESTA_TENANT_ID, str as fstr } from '../services/fiestaApi';
import { stockRowToProduct, stockRowToInventory } from '../services/fiestaMappers';
import { useStoreCatalogue } from '../services/storeCatalogue';
import AwaitingApi from './AwaitingApi';
type StockRow = Record<string, unknown>;
@@ -46,18 +47,20 @@ const rowId = (r: StockRow) => String(r.productid ?? '') || String(r.productname
interface InventoryViewProps {
searchQuery: string;
isCoimbatoreView: boolean;
tenantId?: number;
}
export default function InventoryView({
searchQuery,
isCoimbatoreView
isCoimbatoreView,
tenantId = FIESTA_TENANT_ID
}: InventoryViewProps) {
// ── Live stock across every outlet (Fiesta) ───────────────────────────────
// This page is the admin's command surface. The GLOBAL CATALOG is the deduped
// union of products across all outlets the tenant owns (admin-only import adds
// to it); the STORE STOCK section shows each outlet's live stock so the admin
// can see all the stores under them at a glance.
const locationsQ = useFiestaTenantLocations(FIESTA_TENANT_ID);
const locationsQ = useFiestaTenantLocations(tenantId);
const locations = useMemo(
() =>
(locationsQ.data ?? []).map((l) => ({
@@ -69,7 +72,7 @@ export default function InventoryView({
);
const storesStock = useFiestaStoresStock(
FIESTA_TENANT_ID,
tenantId,
locations.map(({ locationid, locationname }) => ({ locationid, locationname })),
);
const storesLoading = locationsQ.isLoading || storesStock.some((s) => s.isLoading);
@@ -95,6 +98,8 @@ export default function InventoryView({
const [activeTab, setActiveTab] = useState<'catalog' | 'import_branding'>('catalog');
const [selectedCategory, setSelectedCategory] = useState<string>('ALL');
// The store catalogue the admin curates from the global catalogue (shown to users).
const storeCat = useStoreCatalogue();
const [showAddProductModal, setShowAddProductModal] = useState(false);
const [outletFilter, setOutletFilter] = useState<'all' | 'alerts'>('all');
const [outletSearch, setOutletSearch] = useState('');
@@ -197,7 +202,7 @@ export default function InventoryView({
const handleAddNewProduct = (e: React.FormEvent) => {
e.preventDefault();
if (!newProduct.name || !newProduct.sku) {
alert('Kindly supply correct product specifications and catalog SKU code.');
alert('Kindly supply correct product specifications and catalogue SKU code.');
return;
}
@@ -217,7 +222,7 @@ export default function InventoryView({
setProducts([createdProd, ...products]);
setShowAddProductModal(false);
alert(`Fresh product "${createdProd.name}" added to the Global Catalog. It is now available to roll out to all outlets.`);
alert(`Fresh product "${createdProd.name}" added to the Global Catalogue. It is now available to roll out to all outlets.`);
setNewProduct({
name: '',
@@ -274,9 +279,9 @@ export default function InventoryView({
if (parsedCount > 0) {
setProducts(prev => [...newProds, ...prev]);
alert(`Synchronized ${parsedCount} regional products into Catalog database successfully!`);
alert(`Synchronized ${parsedCount} regional products into Catalogue database successfully!`);
} else {
alert('All the specified SKU codes are already active in the catalog ledger.');
alert('All the specified SKU codes are already active in the catalogue ledger.');
}
};
@@ -293,7 +298,7 @@ export default function InventoryView({
<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="Catalog Command Center Banner"
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" />
@@ -308,13 +313,13 @@ export default function InventoryView({
<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 Catalog
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 catalog registry with regional assortment presets, brand styling studio, and live stock synchronization feeds.
Master catalogue registry with regional assortment presets, brand styling studio, and live stock synchronization feeds.
</p>
</div>
@@ -329,7 +334,7 @@ export default function InventoryView({
}`}
>
<Package size={13} />
<span>Catalog & Stocks</span>
<span>Catalogue & Stocks</span>
</button>
<button
@@ -361,7 +366,7 @@ export default function InventoryView({
<h3 className="text-xl font-extrabold tracking-tight font-mono">
{products.length}
</h3>
<p className="text-[10px] text-purple-400 font-semibold mt-1">Master catalog</p>
<p className="text-[10px] text-purple-400 font-semibold mt-1">Master catalogue</p>
</div>
</div>
@@ -405,7 +410,7 @@ export default function InventoryView({
{/* 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="flex justify-between items-start">
<span className="text-[10px] text-slate-400 uppercase tracking-widest font-bold">Catalog Sync Ratio</span>
<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" />
</div>
@@ -489,18 +494,26 @@ export default function InventoryView({
{/* Global Catalog — master assortment grid (full width) */}
<div className="bg-white/40 backdrop-blur-md border border-[#e2e8f0] p-md rounded-2xl text-xs font-sans shadow-sm">
<div className="flex items-center justify-between mb-sm">
<h3 className="font-sans font-bold text-sm text-[#0f172a] flex items-center gap-1.5">
<Package size={15} className="text-[#581c87]" /> Global Catalog Assortment
</h3>
<span className="text-[10px] text-[#581c87] 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>
<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
</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>
<div className="flex items-center gap-2">
<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">
{filteredProducts.length} item{filteredProducts.length === 1 ? '' : 's'} loaded
</span>
</div>
</div>
{storesLoading && products.length === 0 ? (
<div className="text-center py-xl text-zinc-400 text-xs font-bold">Synchronizing regional database...</div>
) : filteredProducts.length === 0 ? (
<div className="text-center py-xl text-zinc-400 text-xs font-bold">No catalog products match your selection.</div>
<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) => (
@@ -565,6 +578,26 @@ export default function InventoryView({
<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>
@@ -874,10 +907,10 @@ 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">
<Sparkles className="text-amber-500 animate-pulse" size={18} />
<h3>Cooperative Catalog Presets</h3>
<h3>Cooperative Catalogue Presets</h3>
</div>
<AwaitingApi label="Catalog presets" api="[R5]" compact />
<AwaitingApi label="Catalogue presets" api="[R5]" compact />
</div>
{/* Custom CSV Parsing Box */}
@@ -955,7 +988,7 @@ export default function InventoryView({
<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]" />
Introduce New Grocery Catalog SKU
Introduce New Grocery Catalogue SKU
</h4>
<button
onClick={() => setShowAddProductModal(false)}