udpates on the ui changesand api integration

This commit is contained in:
Gokul
2026-06-09 11:25:29 +05:30
parent 7dbae96b5f
commit 9f25c5f60a
26 changed files with 4324 additions and 2639 deletions

View File

@@ -19,25 +19,26 @@ import {
Trash2,
PackageCheck,
ShieldCheck,
Zap,
Tag,
UploadCloud,
FileSpreadsheet,
Palette,
ShoppingBag,
Info,
X,
Server,
ChevronDown,
ChevronUp,
RotateCw,
CheckCircle
} from 'lucide-react';
import { ProductMatrixItem, ImportLog } from '../types';
import { initialImportLogs } from '../data';
import { useFiestaTenantLocations, useFiestaStoresStock } from '../services/fiestaQueries';
import { ProductMatrixItem } from '../types';
import {
useFiestaTenantLocations,
useFiestaStoresStock,
useFiestaProductCategories,
} from '../services/fiestaQueries';
import { FIESTA_TENANT_ID, str as fstr } from '../services/fiestaApi';
import { stockRowToProduct, stockRowToInventory } from '../services/fiestaMappers';
import AwaitingApi from './AwaitingApi';
type StockRow = Record<string, unknown>;
const rowId = (r: StockRow) => String(r.productid ?? '') || String(r.productname ?? '');
@@ -78,7 +79,6 @@ export default function InventoryView({
// Global catalog = deduped union of every outlet's products, plus anything the
// admin adds/imports in-session. Seeded once from the live data.
const [products, setProducts] = useState<ProductMatrixItem[]>([]);
const [importLogs, setImportLogs] = useState<ImportLog[]>(initialImportLogs);
const [seeded, setSeeded] = useState(false);
const allStoreRows = storesStock.flatMap((s) => s.rows);
@@ -98,8 +98,8 @@ export default function InventoryView({
const [showAddProductModal, setShowAddProductModal] = useState(false);
const [outletFilter, setOutletFilter] = useState<'all' | 'alerts'>('all');
const [outletSearch, setOutletSearch] = useState('');
const [restockedOverrides, setRestockedOverrides] = useState<Record<number, Record<string, number>>>({});
const [loadingOutlets, setLoadingOutlets] = useState<Record<number, boolean>>({});
// Regional Hub Stocks is read-only for admins — overrides remain empty (no restock actions).
const [restockedOverrides] = useState<Record<number, Record<string, number>>>({});
const [expandedHubs, setExpandedHubs] = useState<Record<number, boolean>>({});
// Memoize storesStock query results merged with simulated restock overrides
@@ -155,89 +155,11 @@ export default function InventoryView({
};
}, [storesStockWithOverrides]);
// Simulated restock dispatch handler
const handleRestockOutlet = (locationId: number, storeRows: any[], locationName: string) => {
setLoadingOutlets(prev => ({ ...prev, [locationId]: true }));
setTimeout(() => {
setRestockedOverrides(prev => {
const currentOverrides = prev[locationId] || {};
const newOverrides = { ...currentOverrides };
storeRows.forEach(row => {
const sku = `SKU-${String(row.productid ?? '') || String(row.productname ?? '')}`;
newOverrides[sku] = 200; // Restock to safe optimal level (>= 120)
});
return {
...prev,
[locationId]: newOverrides
};
});
setLoadingOutlets(prev => ({ ...prev, [locationId]: false }));
const timestamp = new Date().toLocaleTimeString();
setImportLogs(prev => [
{
id: String(Date.now()),
timestamp,
file: 'SUPPLY_CHAIN_API',
status: 'SUCCESS',
count: storeRows.length,
note: `AUTO-RESTOCK: Dispatched emergency shipment to ${locationName}. Synchronized ${storeRows.length} SKUs to 200 units.`
},
...prev
]);
}, 1500);
};
// Simulated single SKU restock handler
const handleRestockSKU = (locationId: number, row: any, locationName: string) => {
if (!row) return;
const sku = `SKU-${String(row.productid ?? '') || String(row.productname ?? '')}`;
const prodName = String(row.productname || 'Unnamed product');
setRestockedOverrides(prev => {
const currentOverrides = prev[locationId] || {};
return {
...prev,
[locationId]: {
...currentOverrides,
[sku]: 200
}
};
});
const timestamp = new Date().toLocaleTimeString();
setImportLogs(prev => [
{
id: String(Date.now()),
timestamp,
file: 'SUPPLY_CHAIN_API',
status: 'SUCCESS',
count: 1,
note: `SKU-RESTOCK: Restocked ${prodName} (${sku}) at ${locationName} to 200 units.`
},
...prev
]);
};
// CSV Textarea input
const [csvText, setCsvText] = useState(
"Name, SKU, Category, Price, InitialStock\nAmma Ghee Pure Butter, GHEE-AMMA-1L, Groceries / Oils, 640, 200\nBhavani Ponni Sona Rice, ST-SONA-25K, Staples / Rice, 1350, 150"
);
// Brand designs state
const [brandStyle, setBrandStyle] = useState({
themeName: 'Coimbatore Kaveri Org',
primaryColor: '#16a34a', // Emerald
secondaryColor: '#f59e0b', // Amber
bagLabel: 'Freshly Harvested from Tamil Soil',
isEcoVerified: true,
stickerPattern: 'radial'
});
// Form state for individual adding
const [newProduct, setNewProduct] = useState({
name: '',
@@ -248,6 +170,16 @@ export default function InventoryView({
image: 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&q=80&w=200'
});
// Live product categories (for the Add-Product modal dropdown).
const productCategoriesQ = useFiestaProductCategories();
const productCategoryNames = useMemo(
() =>
(productCategoriesQ.data ?? [])
.map((c) => fstr(c.categoryname))
.filter((name): name is string => Boolean(name)),
[productCategoriesQ.data],
);
// Categories derived from the live catalog (falls back to ALL only).
const categorySet = new Set<string>();
products.forEach((p) => categorySet.add(p.category));
@@ -342,78 +274,12 @@ export default function InventoryView({
if (parsedCount > 0) {
setProducts(prev => [...newProds, ...prev]);
const logEntry: ImportLog = {
timestamp: new Date().toLocaleTimeString() + ' (IST)',
batchRef: `#IMP_CSV_${Math.floor(1000 + Math.random() * 9000)}`,
type: 'CSV Catalogue Import',
source: 'Console Upload',
result: `SUCCESS (Parsed ${parsedCount} rows)`,
status: 'SUCCESS'
};
setImportLogs([logEntry, ...importLogs]);
alert(`Synchronized ${parsedCount} regional products into Catalog database successfully!`);
} else {
alert('All the specified SKU codes are already active in the catalog ledger.');
}
};
// Preset import trigger
const handleImportPreset = (presetName: string, itemsList: Array<{name: string, sku: string, cat: string, price: number, stock: number, img: string}>) => {
let imported = 0;
const newProds: ProductMatrixItem[] = [];
itemsList.forEach((itm) => {
if (!products.some(p => p.sku === itm.sku)) {
newProds.push({
id: String(products.length + newProds.length + 20),
name: itm.name,
sku: itm.sku,
unitsSold: Math.floor(Math.random() * 45 + 15),
revenue: Math.floor(Math.random() * 20000 + 4000),
stockStatus: 'Healthy',
trend: 'up',
image: itm.img,
category: itm.cat,
exposure: 'All Outlets',
verified: true
});
imported++;
}
});
if (imported > 0) {
setProducts(prev => [...newProds, ...prev]);
const logEntry: ImportLog = {
timestamp: new Date().toLocaleTimeString() + ' (IST)',
batchRef: `#IMP_PST_${Math.floor(1000 + Math.random() * 9000)}`,
type: `${presetName} Import`,
source: 'Corporate Cloud Feed',
result: `SUCCESS Onboarded (${imported} SKUs)`,
status: 'SUCCESS'
};
setImportLogs([logEntry, ...importLogs]);
alert(`Successfully mapped and onboarded ${imported} brand SKUs from "${presetName}"!`);
} else {
alert('All elements of this retail catalog preset are already assigned.');
}
};
// Nilgiris Presets
const nilgirisDairy = [
{ name: 'Ooty Hills Creamery Butter 500g', sku: 'DY-OOT-BTR', cat: 'Groceries / Oils', price: 340, stock: 210, img: 'https://images.unsplash.com/photo-1589985270826-4b7bb135bc9d?auto=format&fit=crop&q=80&w=200' },
{ name: 'Nilgiris Mountain Farm Cheese 250g', sku: 'DY-NIL-CHS', cat: 'Groceries / Oils', price: 460, stock: 120, img: 'https://images.unsplash.com/photo-1486887396153-fa416525c108?auto=format&fit=crop&q=80&w=200' },
{ name: 'Aavin Premium Ghee Tin 1L', sku: 'DY-AAV-GHEE', cat: 'Groceries / Oils', price: 680, stock: 180, img: 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&q=80&w=200' }
];
// Coimbatore Crops
const cbeHeritage = [
{ name: 'Bhavani Premium Boiled Rice 10kg', sku: 'ST-BHV-RICE', cat: 'Staples / Rice', price: 740, stock: 350, img: 'https://images.unsplash.com/photo-1586201375761-83865001e31c?auto=format&fit=crop&q=80&w=200' },
{ name: 'Pollachi Clean Gram Dhal 2kg', sku: 'ST-POL-DHAL', cat: 'Staples / Rice', price: 185, stock: 240, img: 'https://images.unsplash.com/photo-1596040033229-a9821ebd058d?auto=format&fit=crop&q=80&w=200' },
{ name: 'Pure Wood Pressed Gingelly Oil 1L', sku: 'ST-OIL-WOOD', cat: 'Groceries / Oils', price: 395, stock: 190, img: 'https://images.unsplash.com/photo-1474979266404-7eaacbcd87c5?auto=format&fit=crop&q=80&w=200' }
];
return (
<div className="space-y-lg animate-in fade-in duration-500 font-sans relative">
@@ -422,7 +288,7 @@ export default function InventoryView({
<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-35">
<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
@@ -438,11 +304,11 @@ export default function InventoryView({
</div>
{/* Content Row */}
<div className="relative z-10 flex flex-col md:flex-row md:items-center justify-between gap-6">
<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 Catalog Command Center
Product Catalog
<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>
@@ -710,19 +576,22 @@ export default function InventoryView({
{/* Elegant Header Row */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-slate-100 pb-4 mt-8 select-none">
<div className="space-y-1 font-sans">
<div className="flex items-center gap-2">
<h3 className="font-bold text-sm text-slate-900 flex items-center gap-1.5">
<Server size={16} className="text-purple-650" /> Regional Hub Stocks
</h3>
<span className="inline-flex items-center gap-1 text-[9px] font-bold bg-emerald-50 text-emerald-600 border border-emerald-100/50 px-2 py-0.5 rounded-full">
<span className="w-1 h-1 rounded-full bg-emerald-500 animate-pulse" />
Live Sync
</span>
<div className="flex items-start gap-3 font-sans">
<div className="h-10 w-10 shrink-0 rounded-xl bg-gradient-to-br from-purple-500 to-indigo-500 text-white flex items-center justify-center shadow-sm">
<Server size={18} />
</div>
<div className="space-y-1">
<div className="flex items-center gap-2 flex-wrap">
<h3 className="font-bold text-base text-slate-900 tracking-tight">Regional Hub Stocks</h3>
<span className="inline-flex items-center gap-1 text-[9px] font-bold bg-emerald-50 text-emerald-600 border border-emerald-100/50 px-2 py-0.5 rounded-full">
<span className="w-1 h-1 rounded-full bg-emerald-500 animate-pulse" />
Live Sync
</span>
</div>
<p className="text-slate-400 text-[11px] font-medium">
Real-time inventory levels and capacity balance across {locations.length} regional outlets.
</p>
</div>
<p className="text-slate-400 text-[10px] font-medium">
Real-time inventory levels and capacity balance across {locations.length} regional outlets.
</p>
</div>
{/* Controls: Search + Filters */}
@@ -779,23 +648,29 @@ export default function InventoryView({
</div>
{/* Quick Metrics Strip */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 p-4 bg-slate-50/50 rounded-2xl border border-slate-100/80 font-sans text-xs">
<div className="flex flex-col gap-0.5">
<span className="text-[9px] text-slate-400 uppercase tracking-wider font-extrabold">Active Outlets</span>
<span className="font-bold text-slate-800 text-sm font-mono">{locations.length}</span>
</div>
<div className="flex flex-col gap-0.5">
<span className="text-[9px] text-slate-400 uppercase tracking-wider font-extrabold">Optimal Hubs</span>
<span className="font-bold text-emerald-600 text-sm font-mono">{locations.length - storeAlertsData.alertOutletsCount}</span>
</div>
<div className="flex flex-col gap-0.5">
<span className="text-[9px] text-slate-400 uppercase tracking-wider font-extrabold">Low Stock Items</span>
<span className="font-bold text-amber-600 text-sm font-mono">{storeAlertsData.lowStockCount}</span>
</div>
<div className="flex flex-col gap-0.5">
<span className="text-[9px] text-slate-400 uppercase tracking-wider font-extrabold">Critical Alerts</span>
<span className="font-bold text-rose-600 text-sm font-mono">{storeAlertsData.criticalCount}</span>
</div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 font-sans">
{[
{ label: 'Active Outlets', value: locations.length, icon: Server, chip: 'bg-purple-50 text-purple-650 ring-purple-100', value_cls: 'text-slate-900' },
{ label: 'Optimal Hubs', value: locations.length - storeAlertsData.alertOutletsCount, icon: CheckCircle, chip: 'bg-emerald-50 text-emerald-600 ring-emerald-100', value_cls: 'text-emerald-600' },
{ label: 'Low Stock Items', value: storeAlertsData.lowStockCount, icon: TrendingDown, chip: 'bg-amber-50 text-amber-600 ring-amber-100', value_cls: 'text-amber-600' },
{ label: 'Critical Alerts', value: storeAlertsData.criticalCount, icon: AlertTriangle, chip: 'bg-rose-50 text-rose-600 ring-rose-100', value_cls: 'text-rose-600' },
].map((m) => {
const MIcon = m.icon;
return (
<div
key={m.label}
className="bg-white border border-slate-200/70 rounded-2xl p-4 shadow-[0_1px_2px_rgba(16,24,40,0.04)] hover:shadow-[0_8px_22px_rgba(16,24,40,0.08)] hover:border-purple-200 transition-all duration-300 flex items-center gap-3"
>
<div className={`h-10 w-10 shrink-0 rounded-xl flex items-center justify-center ring-1 ${m.chip}`}>
<MIcon size={18} />
</div>
<div className="min-w-0">
<span className="block text-[9px] text-slate-400 uppercase tracking-wider font-extrabold truncate">{m.label}</span>
<span className={`block font-extrabold text-2xl leading-tight font-sans tracking-tight ${m.value_cls}`}>{m.value}</span>
</div>
</div>
);
})}
</div>
{(() => {
@@ -833,10 +708,14 @@ export default function InventoryView({
const meta = locations.find((l) => l.locationid === store.locationid);
const status = meta?.status ?? 'Active';
const statusDotColor = hasAlert
? criticalItemsCount > 0 ? 'bg-rose-500' : 'bg-amber-500'
const statusDotColor = hasAlert
? criticalItemsCount > 0 ? 'bg-rose-500' : 'bg-amber-500'
: 'bg-emerald-500';
const statusChip = hasAlert
? criticalItemsCount > 0 ? 'bg-rose-50 text-rose-600 ring-rose-100' : 'bg-amber-50 text-amber-600 ring-amber-100'
: 'bg-emerald-50 text-emerald-600 ring-emerald-100';
const optimalPct = totalItems > 0 ? (optimalCount / totalItems) * 100 : 0;
const lowPct = totalItems > 0 ? (lowCount / totalItems) * 100 : 0;
const criticalPct = totalItems > 0 ? (criticalItemsCount / totalItems) * 100 : 0;
@@ -848,72 +727,71 @@ export default function InventoryView({
});
return (
<div key={store.locationid} className="bg-white border border-slate-100 rounded-2xl shadow-sm hover:shadow-md transition-all duration-300 relative flex flex-col justify-between overflow-hidden min-h-[300px]">
{/* Loading Overlay */}
{loadingOutlets[store.locationid] && (
<div className="absolute inset-0 bg-white/95 backdrop-blur-sm z-40 flex flex-col items-center justify-center p-6 text-center animate-in fade-in duration-200">
<div className="space-y-3">
<div className="w-8 h-8 mx-auto relative">
<div className="absolute inset-0 rounded-full border-2 border-purple-500/20" />
<div className="absolute inset-0 rounded-full border-2 border-purple-600 border-t-transparent animate-spin" />
</div>
<div className="space-y-0.5 font-sans">
<h4 className="text-slate-850 font-bold text-[11px]">Replenishing Hub...</h4>
<p className="text-slate-400 text-[9px]">Dispatching supply batch to {store.locationname}</p>
</div>
<div key={store.locationid} className="bg-white border border-slate-200/70 rounded-2xl shadow-[0_1px_3px_rgba(16,24,40,0.05)] hover:shadow-[0_16px_36px_rgba(16,24,40,0.10)] hover:border-purple-200 hover:-translate-y-0.5 transition-all duration-300 relative flex flex-col justify-between overflow-hidden min-h-[300px]">
{/* Card Header */}
<div className="p-4 pb-3 flex justify-between items-start gap-2">
<div className="flex items-start gap-2.5 min-w-0">
<div className={`h-9 w-9 shrink-0 rounded-lg flex items-center justify-center ring-1 ${statusChip}`}>
<Server size={15} />
</div>
<div className="min-w-0">
<h4 className="font-bold text-slate-900 text-[13px] truncate leading-tight">
{store.locationname}
</h4>
<p className="text-[10px] text-slate-400 mt-0.5 font-medium">
{totalItems} items · {totalUnits.toLocaleString('en-IN')} units
</p>
</div>
</div>
)}
{/* Card Header (Clean & borderless) */}
<div className="p-4 pb-2 flex justify-between items-start gap-2">
<div className="min-w-0">
<h4 className="font-bold text-slate-900 text-xs truncate flex items-center gap-1.5">
<span className={`w-1.5 h-1.5 rounded-full ${statusDotColor}`} />
{store.locationname}
</h4>
<p className="text-[9px] text-slate-400 mt-0.5 font-medium">
{totalItems} items · {totalUnits.toLocaleString('en-IN')} units
</p>
</div>
<span className={`shrink-0 px-2 py-0.5 rounded-full text-[8px] font-black uppercase tracking-wider ${
<span className={`shrink-0 inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[8px] font-black uppercase tracking-wider ${
hasAlert
? criticalItemsCount > 0
? 'text-rose-600 bg-rose-50'
: 'text-amber-700 bg-amber-50'
: 'text-emerald-600 bg-emerald-50'
? criticalItemsCount > 0
? 'text-rose-600 bg-rose-50 border border-rose-100'
: 'text-amber-700 bg-amber-50 border border-amber-100'
: 'text-emerald-600 bg-emerald-50 border border-emerald-100'
}`}>
<span className={`w-1.5 h-1.5 rounded-full ${statusDotColor}`} />
{hasAlert ? criticalItemsCount > 0 ? 'Critical' : 'Low Stock' : 'Optimal'}
</span>
</div>
{/* Card Body */}
<div className="px-4 py-2 space-y-3 flex-1">
<div className="px-4 py-2 space-y-3.5 flex-1">
{/* Segmented Stock Health Distribution */}
<div className="w-full h-1 bg-slate-100 rounded-full overflow-hidden flex select-none">
{criticalPct > 0 && (
<div className="h-full bg-rose-500" style={{ width: `${criticalPct}%` }} />
)}
{lowPct > 0 && (
<div className="h-full bg-amber-500" style={{ width: `${lowPct}%` }} />
)}
{optimalPct > 0 && (
<div className="h-full bg-emerald-500" style={{ width: `${optimalPct}%` }} />
)}
<div className="space-y-1.5">
<div className="flex justify-between items-center text-[9px] font-bold text-slate-400 uppercase tracking-wider">
<span>Stock Health</span>
<span className="flex items-center gap-2 normal-case font-semibold">
{criticalItemsCount > 0 && <span className="text-rose-600">{criticalItemsCount} crit</span>}
{lowCount > 0 && <span className="text-amber-600">{lowCount} low</span>}
<span className="text-emerald-600">{optimalCount} ok</span>
</span>
</div>
<div className="w-full h-1.5 bg-slate-100 rounded-full overflow-hidden flex select-none">
{criticalPct > 0 && (
<div className="h-full bg-rose-500" style={{ width: `${criticalPct}%` }} />
)}
{lowPct > 0 && (
<div className="h-full bg-amber-500" style={{ width: `${lowPct}%` }} />
)}
{optimalPct > 0 && (
<div className="h-full bg-emerald-500" style={{ width: `${optimalPct}%` }} />
)}
</div>
</div>
{/* Capacity utilization indicator */}
<div className="space-y-1">
<div className="flex justify-between items-center text-[9px] font-medium text-slate-400">
<div className="space-y-1.5">
<div className="flex justify-between items-center text-[9px] font-bold text-slate-400 uppercase tracking-wider">
<span>Capacity Utilised</span>
<span className="font-mono text-slate-600">{Math.round(capacityPct)}%</span>
<span className="font-mono text-slate-600 normal-case font-bold">{Math.round(capacityPct)}%</span>
</div>
<div className="w-full bg-slate-100 h-1 rounded-full overflow-hidden">
<div
<div className="w-full bg-slate-100 h-1.5 rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-500 ${
capacityPct > 85 ? 'bg-rose-500' : 'bg-purple-650'
capacityPct > 85 ? 'bg-rose-500' : 'bg-gradient-to-r from-purple-500 to-indigo-500'
}`}
style={{ width: `${capacityPct}%` }}
/>
@@ -931,11 +809,10 @@ export default function InventoryView({
) : (
<div className="space-y-2 max-h-40 overflow-y-auto pr-0.5 scrollbar-thin">
{sortedItems.map((it, idx) => {
const rawRow = store.rows.find(r => `SKU-${String(r.productid ?? '') || String(r.productname ?? '')}` === it.sku);
const isLow = it.status !== 'Optimal';
return (
<div key={idx} className="flex justify-between items-center gap-3 py-0.5 group/row">
<div key={idx} className="flex justify-between items-center gap-3 py-0.5">
<div className="min-w-0 flex items-center gap-2">
<span className={`w-1 h-1 rounded-full shrink-0 ${
it.status === 'Critical' ? 'bg-rose-500 animate-pulse' : it.status === 'Low Stock' ? 'bg-amber-500 animate-pulse' : 'bg-emerald-500'
@@ -946,23 +823,12 @@ export default function InventoryView({
{it.name}
</span>
</div>
<div className="flex items-center gap-2 shrink-0">
<span className={`font-mono text-[10px] font-bold ${
it.status === 'Critical' ? 'text-rose-600' : it.status === 'Low Stock' ? 'text-amber-700' : 'text-slate-500'
}`}>
{it.stockLevel}
</span>
{isLow && (
<button
onClick={() => handleRestockSKU(store.locationid, rawRow, store.locationname)}
title="Replenish SKU"
className="p-0.5 rounded hover:bg-purple-50 text-slate-400 hover:text-purple-650 transition-colors border-none bg-transparent cursor-pointer opacity-0 group-hover/row:opacity-100 focus:opacity-100"
>
<RotateCw size={9} />
</button>
)}
</div>
<span className={`font-mono text-[10px] font-bold shrink-0 ${
it.status === 'Critical' ? 'text-rose-600' : it.status === 'Low Stock' ? 'text-amber-700' : 'text-slate-500'
}`}>
{it.stockLevel}
</span>
</div>
);
})}
@@ -972,20 +838,18 @@ export default function InventoryView({
</div>
{/* Card Footer actions */}
{/* Card Footer — read-only status (admins cannot edit hub stock) */}
<div className="px-4 py-3 border-t border-slate-100 bg-slate-50/50 flex items-center justify-between gap-2 shrink-0">
<span className="text-[9px] font-mono text-slate-400 uppercase tracking-wide">
{hasAlert ? `${criticalItemsCount + lowCount} items need attention` : 'All items optimal'}
</span>
{hasAlert ? (
<button
onClick={() => handleRestockOutlet(store.locationid, store.rows, store.locationname)}
className="bg-purple-650 hover:bg-purple-755 text-white px-3 py-1.5 rounded-lg text-[9px] font-bold uppercase tracking-wider cursor-pointer border-none transition-all flex items-center gap-1 shadow-sm active:scale-95"
>
<Zap size={10} />
<span>Restock Hub</span>
</button>
<span className={`text-[9px] font-black flex items-center gap-1 select-none pr-1 uppercase tracking-wide ${
criticalItemsCount > 0 ? 'text-rose-600' : 'text-amber-700'
}`}>
<AlertTriangle size={11} /> {criticalItemsCount > 0 ? 'Critical' : 'Low Stock'}
</span>
) : (
<span className="text-[9px] font-black text-emerald-600 flex items-center gap-1 select-none pr-1">
<CheckCircle size={11} /> Stocked
@@ -1013,45 +877,7 @@ export default function InventoryView({
<h3>Cooperative Catalog Presets</h3>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-sm">
{/* Preset 1 */}
<div className="border border-[#e2e8f0] rounded-2xl p-md space-y-md hover:border-purple-300 transition-colors bg-gradient-to-br from-indigo-50/20 to-purple-50/10 group">
<div className="space-y-1">
<span className="text-[8px] font-extrabold uppercase bg-purple-100 text-purple-700 px-2 py-0.5 rounded-md">Cooperative Dairy</span>
<h4 className="font-bold text-zinc-900 leading-tight pt-1">Nilgiris Dairy Fresh Pack</h4>
<p className="text-[10px] text-zinc-400 font-semibold">3 High-Margin Butter & Cheese SKUs</p>
</div>
<div className="flex justify-between items-center pt-2">
<span className="text-[10px] font-mono font-bold text-[#581c87]">CBE-COOP-04</span>
<button
onClick={() => handleImportPreset('Nilgiris Dairy Coop', nilgirisDairy)}
className="px-3 py-1.5 bg-[#581c87] hover:bg-purple-800 text-white font-bold rounded-lg text-[9px] uppercase cursor-pointer transition-transform group-hover:scale-105"
>
Import Batch
</button>
</div>
</div>
{/* Preset 2 */}
<div className="border border-[#e2e8f0] rounded-2xl p-md space-y-md hover:border-purple-300 transition-colors bg-gradient-to-br from-emerald-50/20 to-teal-50/10 group">
<div className="space-y-1">
<span className="text-[8px] font-extrabold uppercase bg-emerald-100 text-emerald-700 px-2 py-0.5 rounded-md">Agricultural Feed</span>
<h4 className="font-bold text-zinc-900 leading-tight pt-1">Coimbatore Heritage Grains</h4>
<p className="text-[10px] text-zinc-400 font-semibold">3 Premium Boiled Rice & Oils</p>
</div>
<div className="flex justify-between items-center pt-2">
<span className="text-[10px] font-mono font-bold text-emerald-600">TAMIL-AGRI-09</span>
<button
onClick={() => handleImportPreset('Coimbatore Heritage', cbeHeritage)}
className="px-3 py-1.5 bg-[#581c87] hover:bg-purple-800 text-white font-bold rounded-lg text-[9px] uppercase cursor-pointer transition-transform group-hover:scale-105"
>
Import Batch
</button>
</div>
</div>
</div>
<AwaitingApi label="Catalog presets" api="[R5]" compact />
</div>
{/* Custom CSV Parsing Box */}
@@ -1097,26 +923,7 @@ export default function InventoryView({
<span className="text-[9px] text-zinc-500 font-bold">COIMBATORE_ERP_V4</span>
</div>
<div className="space-y-2 max-h-48 overflow-y-auto pr-1">
{importLogs.map((log, idx) => (
<div key={idx} className="flex justify-between items-start gap-4 text-[10px] border-b border-zinc-900/50 pb-1.5">
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="text-purple-400 font-extrabold">{log.batchRef}</span>
<span className="text-zinc-650 text-[8px] font-semibold">{log.timestamp}</span>
</div>
<p className="text-zinc-400">
{log.type} via <span className="text-zinc-300 italic">{log.source}</span>
</p>
<p className="text-zinc-505 font-sans text-[9px] leading-tight">{log.result}</p>
</div>
<span className="px-1.5 py-0.5 bg-emerald-950/50 border border-emerald-900 text-emerald-450 font-extrabold uppercase text-[8px] rounded">
{log.status}
</span>
</div>
))}
</div>
<AwaitingApi label="Import / sync audit log" api="[R4]" compact className="bg-zinc-900/40 border-zinc-800" />
</div>
</div>
@@ -1130,115 +937,7 @@ export default function InventoryView({
<h3>Packaging Branding Studio</h3>
</div>
<div className="space-y-md text-xs">
{/* Studio Control 1 */}
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">Brand Title Caption</label>
<input
type="text"
value={brandStyle.themeName}
onChange={(e) => setBrandStyle({ ...brandStyle, themeName: 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] font-semibold text-zinc-700"
/>
</div>
{/* Studio Control 2 */}
<div className="grid grid-cols-2 gap-sm">
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">Primary color</label>
<div className="flex gap-sm items-center">
<input
type="color"
value={brandStyle.primaryColor}
onChange={(e) => setBrandStyle({ ...brandStyle, primaryColor: e.target.value })}
className="w-8 h-8 rounded border border-zinc-200 cursor-pointer bg-transparent"
/>
<span className="font-mono font-bold text-zinc-650">{brandStyle.primaryColor}</span>
</div>
</div>
<div className="space-y-1">
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">Accent text color</label>
<div className="flex gap-sm items-center">
<input
type="color"
value={brandStyle.secondaryColor}
onChange={(e) => setBrandStyle({ ...brandStyle, secondaryColor: e.target.value })}
className="w-8 h-8 rounded border border-zinc-200 cursor-pointer bg-transparent"
/>
<span className="font-mono font-bold text-zinc-650">{brandStyle.secondaryColor}</span>
</div>
</div>
</div>
{/* Studio Control 3 */}
<div className="space-y-1">
<label className="font-bold text-zinc-505 uppercase tracking-widest text-[9px]">Bag print footer tag</label>
<input
type="text"
value={brandStyle.bagLabel}
onChange={(e) => setBrandStyle({ ...brandStyle, bagLabel: 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] font-semibold text-zinc-700"
/>
</div>
{/* Studio Control 4 */}
<div className="flex items-center justify-between p-sm bg-slate-50/50 border border-slate-200 rounded-xl">
<div>
<h4 className="font-bold text-[#0f172a] text-xs">Sustainability Seal</h4>
<p className="text-[9px] text-zinc-400 font-semibold mt-0.5">Include Coimbatore eco certification stamp</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={brandStyle.isEcoVerified}
onChange={() => setBrandStyle({ ...brandStyle, isEcoVerified: !brandStyle.isEcoVerified })}
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>
{/* Interactive Dynamic Checkout Jute Bag Preview Canvas */}
<div className="border border-[#e2e8f0] rounded-2xl p-md bg-[#faf8f5] space-y-sm shadow-inner flex flex-col items-center">
<span className="text-[9px] font-sans font-bold text-zinc-405 uppercase tracking-widest block text-center border-b border-zinc-200/50 pb-1.5 w-full">
Package Bag Design Preview
</span>
<div className="relative mx-auto w-48 h-64 bg-[#eddcd2] border-2 border-[#d6b795] rounded-b-3xl rounded-t-xl shadow-2xl flex flex-col justify-between p-md mt-6 mb-2 overflow-hidden animate-in fade-in zoom-in-95 duration-500">
{/* Realistic Jute Texture Overlay simulation */}
<div className="absolute inset-0 bg-gradient-to-tr from-stone-900/5 to-amber-950/5 pointer-events-none" />
{/* Hanging handle simulation */}
<div className="absolute -top-3.5 left-1/2 -translate-x-1/2 w-20 h-6 border-3 border-b-0 border-[#c4a27d] rounded-t-full shadow-sm" />
<div className="text-center pt-sm space-y-1 relative z-10">
<span className="text-[11px] font-extrabold block tracking-tight uppercase" style={{ color: brandStyle.primaryColor }}>
{brandStyle.themeName || 'nearledaily Fresh'}
</span>
<div className="w-12 h-0.5 mx-auto rounded-full" style={{ backgroundColor: brandStyle.secondaryColor }} />
</div>
<div className="my-auto flex flex-col items-center text-center p-1 space-y-1.5 relative z-10">
<ShoppingBag className="w-12 h-12 stroke-1 drop-shadow-sm" style={{ color: brandStyle.primaryColor }} />
<span className="text-[9px] font-extrabold max-w-[130px] leading-tight block text-stone-750">
{brandStyle.bagLabel || 'Grown with Pride'}
</span>
</div>
<div className="flex justify-between items-center text-[7px] border-t border-stone-300 pt-1.5 relative z-10 font-bold text-stone-500">
<span>100% ORGANIC</span>
{brandStyle.isEcoVerified && (
<span className="text-emerald-800 font-extrabold bg-emerald-100/80 border border-emerald-200 px-1.5 py-0.5 rounded text-[6px]">
CBE-ECO
</span>
)}
</div>
</div>
</div>
</div>
<AwaitingApi label="Brand & packaging config" api="[R6]" />
</div>
</div>
@@ -1302,10 +1001,13 @@ export default function InventoryView({
onChange={(e) => setNewProduct({ ...newProduct, category: e.target.value })}
className="w-full border border-[#e2e8f0] rounded-lg p-2 bg-[#f8fafc] focus:bg-white outline-none"
>
<option value="Staples / Rice">Staples / Rice</option>
<option value="Groceries / Oils">Groceries / Oils</option>
<option value="Beverages / Coffee">Beverages / Coffee</option>
<option value="Fresh Produce / Veg">Fresh Produce / Veg</option>
{/* Keep the form's default selection valid even if it isn't in the live list. */}
{!productCategoryNames.includes(newProduct.category) && (
<option value={newProduct.category}>{newProduct.category}</option>
)}
{productCategoryNames.map((name) => (
<option key={name} value={name}>{name}</option>
))}
</select>
</div>
</div>