feat: relocate orders and deliveries to store console & polish store cover images
This commit is contained in:
901
src/components/InventoryView.tsx
Normal file
901
src/components/InventoryView.tsx
Normal file
@@ -0,0 +1,901 @@
|
||||
/**
|
||||
* @license
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Layers,
|
||||
Search,
|
||||
Plus,
|
||||
RefreshCw,
|
||||
AlertTriangle,
|
||||
TrendingUp,
|
||||
Sparkles,
|
||||
Check,
|
||||
Package,
|
||||
ChevronRight,
|
||||
TrendingDown,
|
||||
Trash2,
|
||||
PackageCheck,
|
||||
Zap,
|
||||
Tag,
|
||||
UploadCloud,
|
||||
FileSpreadsheet,
|
||||
Palette,
|
||||
ShoppingBag,
|
||||
Info,
|
||||
X
|
||||
} from 'lucide-react';
|
||||
import { ProductMatrixItem, InventoryItem, ImportLog } from '../types';
|
||||
import { initialImportLogs } from '../data';
|
||||
import { useFiestaStockStatement, useFiestaTenantLocations } from '../services/fiestaQueries';
|
||||
import { FIESTA_TENANT_ID, FIESTA_PRIMARY_LOCATION_ID, str as fstr } from '../services/fiestaApi';
|
||||
import { stockRowToProduct, stockRowToInventory } from '../services/fiestaMappers';
|
||||
|
||||
interface InventoryViewProps {
|
||||
searchQuery: string;
|
||||
isCoimbatoreView: boolean;
|
||||
}
|
||||
|
||||
export default function InventoryView({
|
||||
searchQuery,
|
||||
isCoimbatoreView
|
||||
}: InventoryViewProps) {
|
||||
// ── Live stock data (Fiesta) ─────────────────────────────────────────────
|
||||
// The catalog grid and the hub-balance ledger are both derived from the live
|
||||
// stock statement for the tenant's primary outlet. We seed local state from
|
||||
// it once it loads so the existing add / CSV / replenish interactions keep
|
||||
// mutating in-session without losing the live baseline.
|
||||
const locationsQ = useFiestaTenantLocations(FIESTA_TENANT_ID);
|
||||
const primaryLocation =
|
||||
(locationsQ.data ?? []).find((l) => Number(l.locationid) === FIESTA_PRIMARY_LOCATION_ID) ||
|
||||
(locationsQ.data ?? [])[0];
|
||||
const locationId = primaryLocation ? Number(primaryLocation.locationid) : FIESTA_PRIMARY_LOCATION_ID;
|
||||
const locationName = fstr(primaryLocation?.locationname) || 'Primary Outlet';
|
||||
|
||||
const stockQ = useFiestaStockStatement({
|
||||
tenantid: FIESTA_TENANT_ID,
|
||||
locationid: locationId,
|
||||
keyword: '',
|
||||
pageno: 1,
|
||||
pagesize: 100,
|
||||
});
|
||||
|
||||
const [products, setProducts] = useState<ProductMatrixItem[]>([]);
|
||||
const [inventory, setInventory] = useState<InventoryItem[]>([]);
|
||||
const [importLogs, setImportLogs] = useState<ImportLog[]>(initialImportLogs);
|
||||
|
||||
useEffect(() => {
|
||||
if (stockQ.data) {
|
||||
setProducts(stockQ.data.map(stockRowToProduct));
|
||||
setInventory(stockQ.data.map((r) => stockRowToInventory(r, locationName)));
|
||||
}
|
||||
// locationName is derived from the same query chain; safe to depend on data.
|
||||
}, [stockQ.data, locationName]);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'catalog' | 'import_branding'>('catalog');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>('ALL');
|
||||
const [showAddProductModal, setShowAddProductModal] = useState(false);
|
||||
const [replenishmentList, setReplenishmentList] = useState<string[]>([]);
|
||||
|
||||
// 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: '',
|
||||
sku: '',
|
||||
category: 'Staples / Rice',
|
||||
price: 150,
|
||||
initialStock: 250,
|
||||
image: 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&q=80&w=200'
|
||||
});
|
||||
|
||||
// Categories derived from the live catalog (falls back to ALL only).
|
||||
const categorySet = new Set<string>();
|
||||
products.forEach((p) => categorySet.add(p.category));
|
||||
const categories: string[] = ['ALL', ...Array.from(categorySet)];
|
||||
|
||||
// Handle SKU quantity change
|
||||
const handleUpdateStock = (sku: string, delta: number) => {
|
||||
setInventory(prev => prev.map(item => {
|
||||
if (item.sku === sku) {
|
||||
const newLevel = Math.max(0, item.stockLevel + delta);
|
||||
const status = newLevel < 25 ? 'Critical' : newLevel < 120 ? 'Low Stock' : 'Optimal';
|
||||
return { ...item, stockLevel: newLevel, status };
|
||||
}
|
||||
return item;
|
||||
}));
|
||||
};
|
||||
|
||||
// Trigger quick reorder recommendation
|
||||
const handleReplenishSku = (sku: string) => {
|
||||
if (replenishmentList.includes(sku)) return;
|
||||
setReplenishmentList(prev => [...prev, sku]);
|
||||
handleUpdateStock(sku, 500); // Add 500 units to stock
|
||||
setTimeout(() => {
|
||||
alert(`Auto-Replenish complete! 500 units ordered and allocated directly to corresponding hub for SKU ${sku}`);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// Filter criteria
|
||||
const filteredProducts = products.filter(p => {
|
||||
const matchesSearch = p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.sku.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
p.category.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesCat = selectedCategory === 'ALL' || p.category.startsWith(selectedCategory.split(' / ')[0]);
|
||||
return matchesSearch && matchesCat;
|
||||
});
|
||||
|
||||
const handleAddNewProduct = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!newProduct.name || !newProduct.sku) {
|
||||
alert('Kindly supply correct product specifications and catalog SKU code.');
|
||||
return;
|
||||
}
|
||||
|
||||
const createdProd: ProductMatrixItem = {
|
||||
id: String(products.length + 1),
|
||||
name: newProduct.name,
|
||||
sku: newProduct.sku,
|
||||
unitsSold: 0,
|
||||
revenue: 0,
|
||||
stockStatus: 'Healthy',
|
||||
trend: 'flat',
|
||||
image: newProduct.image,
|
||||
category: newProduct.category,
|
||||
exposure: 'All Outlets',
|
||||
verified: true
|
||||
};
|
||||
|
||||
const createdInv: InventoryItem = {
|
||||
sku: newProduct.sku,
|
||||
name: newProduct.name,
|
||||
warehouse: 'RS Puram Hub (CBE-01)',
|
||||
stockLevel: newProduct.initialStock,
|
||||
maxCapacity: 1000,
|
||||
status: 'Optimal',
|
||||
region: 'CBE-NORTH'
|
||||
};
|
||||
|
||||
setProducts([createdProd, ...products]);
|
||||
setInventory([createdInv, ...inventory]);
|
||||
setShowAddProductModal(false);
|
||||
alert(`Fresh product "${createdProd.name}" incorporated into Master Grocery Catalog and standard ledger!`);
|
||||
|
||||
setNewProduct({
|
||||
name: '',
|
||||
sku: '',
|
||||
category: 'Staples / Rice',
|
||||
price: 150,
|
||||
initialStock: 250,
|
||||
image: 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&q=80&w=200'
|
||||
});
|
||||
};
|
||||
|
||||
// Custom Raw CSV import
|
||||
const handleCSVImport = () => {
|
||||
const lines = csvText.split('\n').map(l => l.trim()).filter(l => l.length > 0 && !l.startsWith('Name'));
|
||||
if (lines.length === 0) {
|
||||
alert('CSV sequence contains no importable entries.');
|
||||
return;
|
||||
}
|
||||
|
||||
let parsedCount = 0;
|
||||
const newProds: ProductMatrixItem[] = [];
|
||||
const newInvs: InventoryItem[] = [];
|
||||
|
||||
lines.forEach(line => {
|
||||
const parts = line.split(',').map(p => p.trim());
|
||||
if (parts.length >= 2) {
|
||||
const name = parts[0];
|
||||
const sku = parts[1];
|
||||
const category = parts[2] || 'Staples / Rice';
|
||||
const price = Number(parts[3]) || 120;
|
||||
const initialStock = Number(parts[4]) || 150;
|
||||
|
||||
if (!products.some(p => p.sku === sku)) {
|
||||
newProds.push({
|
||||
id: String(products.length + newProds.length + 1),
|
||||
name,
|
||||
sku,
|
||||
unitsSold: 0,
|
||||
revenue: 0,
|
||||
stockStatus: 'Healthy',
|
||||
trend: 'flat',
|
||||
image: 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&q=80&w=200',
|
||||
category,
|
||||
exposure: 'All Outlets',
|
||||
verified: true
|
||||
});
|
||||
|
||||
newInvs.push({
|
||||
sku,
|
||||
name,
|
||||
warehouse: 'RS Puram Hub (CBE-01)',
|
||||
stockLevel: initialStock,
|
||||
maxCapacity: 1000,
|
||||
status: 'Optimal',
|
||||
region: 'CBE-NORTH'
|
||||
});
|
||||
parsedCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (parsedCount > 0) {
|
||||
setProducts(prev => [...newProds, ...prev]);
|
||||
setInventory(prev => [...newInvs, ...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[] = [];
|
||||
const newInvs: InventoryItem[] = [];
|
||||
|
||||
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
|
||||
});
|
||||
|
||||
newInvs.push({
|
||||
sku: itm.sku,
|
||||
name: itm.name,
|
||||
warehouse: 'Peelamedu Sort Center',
|
||||
stockLevel: itm.stock,
|
||||
maxCapacity: 800,
|
||||
status: 'Optimal',
|
||||
region: 'CBE-EAST'
|
||||
});
|
||||
imported++;
|
||||
}
|
||||
});
|
||||
|
||||
if (imported > 0) {
|
||||
setProducts(prev => [...newProds, ...prev]);
|
||||
setInventory(prev => [...newInvs, ...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">
|
||||
|
||||
{/* Dynamic Navigation Toolbar header */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-md border-b border-[#e2e8f0] pb-md">
|
||||
<div>
|
||||
<h1 className="font-sans font-bold text-2xl tracking-tight text-[#0f172a] flex items-center gap-xs">
|
||||
<Layers className="text-[#581c87]" size={24} />
|
||||
Coimbatore Grocery Assortment & Catalogue Studio
|
||||
</h1>
|
||||
<p className="text-zinc-500 font-sans text-xs mt-1">
|
||||
Build regional catalogues, update localized stock balances, parse batch imports, and style brand bag templates.
|
||||
</p>
|
||||
<div className="mt-1.5">
|
||||
{stockQ.isLoading ? (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-zinc-400 uppercase tracking-wide">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-zinc-300 animate-pulse" /> Loading live stock…
|
||||
</span>
|
||||
) : stockQ.isError ? (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-rose-600 uppercase tracking-wide">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-rose-500" /> Live data unavailable
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-semibold text-emerald-600 uppercase tracking-wide">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" /> Live · {locationName} · {products.length} SKUs
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-sm">
|
||||
<button
|
||||
onClick={() => setActiveTab('catalog')}
|
||||
className={`px-4 py-2 rounded-lg text-xs font-bold transition-all cursor-pointer ${
|
||||
activeTab === 'catalog'
|
||||
? 'bg-[#581c87] text-white shadow-sm'
|
||||
: 'bg-white hover:bg-zinc-50 text-zinc-700 border border-[#e2e8f0]'
|
||||
}`}
|
||||
>
|
||||
🌾 Catalog Grid & Ledger
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveTab('import_branding')}
|
||||
className={`px-4 py-2 rounded-lg text-xs font-bold transition-all cursor-pointer ${
|
||||
activeTab === 'import_branding'
|
||||
? 'bg-[#581c87] text-white shadow-sm'
|
||||
: 'bg-white hover:bg-zinc-50 text-zinc-700 border border-[#e2e8f0]'
|
||||
}`}
|
||||
>
|
||||
📥 Import & Brand Studio
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeTab === 'catalog' ? (
|
||||
<>
|
||||
{/* Quick Category Tab Filter Row */}
|
||||
<div className="flex flex-wrap gap-2 py-1 items-center justify-between">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
onClick={() => setSelectedCategory(cat)}
|
||||
className={`px-4 py-2 rounded-lg font-sans text-xs font-semibold tracking-wide transition-all border outline-none cursor-pointer ${
|
||||
selectedCategory === cat
|
||||
? 'bg-[#581c87] text-white border-[#581c87] shadow-sm'
|
||||
: 'bg-white text-zinc-700 border-[#e2e8f0] hover:bg-zinc-50'
|
||||
}`}
|
||||
>
|
||||
{cat === 'ALL' ? '🌾 All Catalog Items' : cat.replace('Groceries / ', '').replace('Staples / ', '').replace('Beverages / ', '').replace('Fresh Produce / ', '')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setShowAddProductModal(true)}
|
||||
className="bg-[#581c87] text-white px-xl py-2 rounded-lg text-xs font-bold uppercase tracking-wider flex items-center justify-center gap-xs cursor-pointer hover:bg-purple-800 transition shadow-sm"
|
||||
>
|
||||
<Plus size={14} />
|
||||
Add Manual SKU
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Multi-Pane Layout: Left Catalog Grid, Right Stock balances */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-gutter text-xs font-sans">
|
||||
|
||||
{/* Left Grid: Grocery Catalogue Items Showcase */}
|
||||
<div className="lg:col-span-2 space-y-md">
|
||||
<div className="bg-[#f8fafc]/50 border border-[#e2e8f0] p-md rounded-xl">
|
||||
<h3 className="font-sans font-bold text-sm text-[#0f172a] mb-xs">Active Assortment Items</h3>
|
||||
<p className="text-zinc-500 font-normal mb-md leading-relaxed text-[11px]">Primary catalog schema synchronized on customer booking apps. Total: {filteredProducts.length} items</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-sm">
|
||||
{filteredProducts.map((prod) => {
|
||||
return (
|
||||
<div key={prod.id} className="bg-white border border-[#e2e8f0] rounded-xl overflow-hidden p-md flex gap-md shadow-sm hover:shadow-md transition-shadow relative">
|
||||
<div className="w-16 h-16 rounded-xl border border-zinc-100 shrink-0 overflow-hidden bg-zinc-50">
|
||||
<img
|
||||
src={prod.image}
|
||||
alt={prod.name}
|
||||
referrerPolicy="no-referrer"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 className="font-bold text-zinc-900 leading-tight text-xs">{prod.name}</h4>
|
||||
<span className="text-[10px] text-zinc-400 font-bold tracking-tight">{prod.sku}</span>
|
||||
</div>
|
||||
<span className="px-1.5 py-0.5 rounded text-[9px] font-bold uppercase bg-purple-50 text-purple-700">
|
||||
{prod.category.split(' / ')[0]}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center pt-2">
|
||||
<div>
|
||||
<span className="text-[9px] text-zinc-400 block uppercase tracking-wider font-bold">Sold (Units)</span>
|
||||
<span className="font-bold text-zinc-800 font-mono">{prod.unitsSold.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-[9px] text-zinc-400 block uppercase tracking-wider font-bold">Total revenue</span>
|
||||
<span className="font-bold text-emerald-600 font-mono">₹{prod.revenue.toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Pane: Stock level adjustment ledgers */}
|
||||
<div className="space-y-md">
|
||||
<div className="bg-white border border-[#e2e8f0] rounded-xl p-md shadow-sm space-y-md">
|
||||
<div>
|
||||
<h3 className="font-sans font-bold text-sm text-[#0f172a]">Hub Balances Ledger</h3>
|
||||
<p className="text-zinc-500 font-normal leading-relaxed text-[11px] mt-0.5">Physical checkout balances across localized Coimbatore warehouse locations.</p>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-[#f1f5f9] select-none">
|
||||
{inventory.map((item, idx) => {
|
||||
const percentage = (item.stockLevel / item.maxCapacity) * 100;
|
||||
return (
|
||||
<div key={idx} className="py-md space-y-xs">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<p className="font-bold text-[#0f172a]">{item.name}</p>
|
||||
<p className="text-[10px] text-zinc-400 mt-1 font-medium">{item.warehouse}</p>
|
||||
<div className="flex gap-px pt-1 items-center">
|
||||
<span className="bg-[#f1f5f9] px-1 py-0.5 rounded text-[8px] font-bold text-zinc-500 font-mono tracking-tight mr-1">{item.region}</span>
|
||||
<span className={`px-1.5 py-0.5 rounded text-[8px] font-bold tracking-wide uppercase ${
|
||||
item.status === 'Critical' ? 'bg-rose-50 text-rose-600 border border-rose-100 animate-pulse' : item.status === 'Low Stock' ? 'bg-amber-50 text-amber-600' : 'bg-emerald-50 text-emerald-600'
|
||||
}`}>
|
||||
● {item.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right space-y-1">
|
||||
<span className="font-mono font-bold text-[#0f172a] block">{item.stockLevel.toLocaleString()} units</span>
|
||||
|
||||
<div className="flex gap-1 justify-end">
|
||||
<button
|
||||
className="bg-zinc-100 hover:bg-zinc-200 p-1 px-2 rounded font-bold cursor-pointer text-[10px]"
|
||||
onClick={() => handleUpdateStock(item.sku, -5)}
|
||||
title="Decrement 5 units"
|
||||
>
|
||||
-5
|
||||
</button>
|
||||
<button
|
||||
className="bg-zinc-100 hover:bg-zinc-200 p-1 px-2 rounded font-bold cursor-pointer text-[10px]"
|
||||
onClick={() => handleUpdateStock(item.sku, 5)}
|
||||
title="Increment 5 units"
|
||||
>
|
||||
+5
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gauge percentage */}
|
||||
<div className="pt-1.5 space-y-1">
|
||||
<div className="w-full bg-[#eceef0] h-1.5 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-300 ${
|
||||
item.status === 'Critical' ? 'bg-rose-500' : item.status === 'Low Stock' ? 'bg-amber-500' : 'bg-[#581c87]'
|
||||
}`}
|
||||
style={{ width: `${Math.min(percentage, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-[9px] text-zinc-400 font-bold">
|
||||
<span>Verification Level: {Math.round(percentage)}%</span>
|
||||
{item.status !== 'Optimal' && (
|
||||
<button
|
||||
onClick={() => handleReplenishSku(item.sku)}
|
||||
className="text-[#581c87] hover:underline flex items-center gap-px font-bold cursor-pointer"
|
||||
>
|
||||
<Zap size={11} className="text-amber-500 animate-bounce" />
|
||||
Auto-Replenish
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-gutter text-xs font-sans">
|
||||
|
||||
{/* Left Column: Catalogue Import & Batch Console */}
|
||||
<div className="space-y-lg">
|
||||
|
||||
{/* Fast Imports presets Card */}
|
||||
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl shadow-sm space-y-md">
|
||||
<div className="flex items-center gap-xs text-[#0f172a] font-bold text-sm">
|
||||
<Sparkles className="text-amber-500" size={18} />
|
||||
<h3>Tamil Nadu Region Catalog Presets</h3>
|
||||
</div>
|
||||
<p className="text-zinc-500 leading-relaxed text-[11px]">
|
||||
Instantly import bulk verified grocers, spices and diary products catalogs from local Coimbatore farms & cooperatives.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-sm">
|
||||
|
||||
{/* Preset 1 */}
|
||||
<div className="border border-[#e2e8f0] rounded-xl p-sm space-y-md hover:border-purple-300 transition-colors bg-[#f8fafc]/30">
|
||||
<div>
|
||||
<h4 className="font-bold text-zinc-900 leading-tight">Nilgiris Dairy Fresh Pack</h4>
|
||||
<p className="text-[10px] text-zinc-400 mt-0.5">3 High-Margin Butter & Cheese SKU</p>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[10px] font-mono font-bold text-[#581c87]">CBE-COOP-04</span>
|
||||
<button
|
||||
onClick={() => handleImportPreset('Nilgiris Dairy Coop', nilgirisDairy)}
|
||||
className="px-2 py-1 bg-[#581c87] hover:bg-purple-800 text-white font-bold rounded text-[9px] uppercase cursor-pointer"
|
||||
>
|
||||
Import Batch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preset 2 */}
|
||||
<div className="border border-[#e2e8f0] rounded-xl p-sm space-y-md hover:border-purple-300 transition-colors bg-[#f8fafc]/30">
|
||||
<div>
|
||||
<h4 className="font-bold text-zinc-900 leading-tight">Coimbatore Heritage Grains</h4>
|
||||
<p className="text-[10px] text-zinc-400 mt-0.5">3 Premium Boiled Rice & Oils</p>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[10px] font-mono font-bold text-emerald-600">TAMIL-AGRI-09</span>
|
||||
<button
|
||||
onClick={() => handleImportPreset('Coimbatore Heritage', cbeHeritage)}
|
||||
className="px-2 py-1 bg-[#581c87] hover:bg-purple-800 text-white font-bold rounded text-[9px] uppercase cursor-pointer"
|
||||
>
|
||||
Import Batch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Custom CSV Parsing Box */}
|
||||
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl shadow-sm space-y-md">
|
||||
<div className="flex items-center gap-xs text-[#0f172a] font-bold text-sm">
|
||||
<FileSpreadsheet className="text-[#581c87]" size={18} />
|
||||
<h3>Manual CSV Direct-Entry Console</h3>
|
||||
</div>
|
||||
<p className="text-zinc-500 text-[11px]">
|
||||
Paste comma-separated rows here (Name, SKU, Category, Price, InitialStock) to bulk register catalog elements.
|
||||
</p>
|
||||
|
||||
<div className="space-y-sm">
|
||||
<textarea
|
||||
value={csvText}
|
||||
onChange={(e) => setCsvText(e.target.value)}
|
||||
className="w-full h-28 p-sm font-mono text-[11px] border border-[#e2e8f0] rounded-lg bg-[#f8fafc] outline-none focus:bg-white focus:ring-1 focus:ring-[#581c87] leading-relaxed"
|
||||
/>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[10px] text-zinc-400 font-medium">Header line is skipped automatically.</span>
|
||||
<button
|
||||
onClick={handleCSVImport}
|
||||
className="bg-[#581c87] text-white px-xl py-2 rounded-lg text-xs font-bold uppercase tracking-wider cursor-pointer hover:bg-purple-800 transition"
|
||||
>
|
||||
Parse CSV Data & Sync
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Realtime Import Logs list */}
|
||||
<div className="bg-[#f8fafc]/50 border border-[#e2e8f0] p-md rounded-xl">
|
||||
<h3 className="font-sans font-bold text-sm text-[#0f172a] mb-xs">Live Channel Import Logs & Audit</h3>
|
||||
<p className="text-zinc-505 mb-md text-[11px]">Recent logistics synchronization log sequences executed by central Coimbatore ERP.</p>
|
||||
|
||||
<div className="space-y-sm">
|
||||
{importLogs.map((log, idx) => (
|
||||
<div key={idx} className="bg-white p-sm border border-[#e2e8f0] rounded-lg flex items-center justify-between text-xs font-sans">
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex items-center gap-sm">
|
||||
<span className="font-mono font-bold text-[#581c87]">{log.batchRef}</span>
|
||||
<span className="text-zinc-400 text-[10px] font-medium">{log.timestamp}</span>
|
||||
</div>
|
||||
<p className="font-bold text-zinc-800">{log.type} • <em className="text-zinc-400 font-normal">{log.source}</em></p>
|
||||
</div>
|
||||
|
||||
<span className="px-1.5 py-0.5 bg-emerald-50 border border-emerald-100 text-emerald-600 font-bold uppercase text-[9px] rounded">
|
||||
{log.status}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Right Column: Beautiful Interactive Brand Design Studio */}
|
||||
<div className="space-y-lg">
|
||||
|
||||
<div className="bg-white border border-[#e2e8f0] p-md rounded-xl shadow-sm space-y-md">
|
||||
<div className="flex items-center gap-xs text-[#0f172a] font-bold text-sm">
|
||||
<Palette className="text-[#581c87]" size={18} />
|
||||
<h3>Operational Branding & Package Studio</h3>
|
||||
</div>
|
||||
<p className="text-zinc-500 leading-relaxed text-[11px]">
|
||||
Grocery apps and parcel delivery bags use custom generated corporate brand designs. Style bag backgrounds, badges, and titles live.
|
||||
</p>
|
||||
|
||||
<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 THEME 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]"
|
||||
/>
|
||||
</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 BACKPLANE 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"
|
||||
/>
|
||||
<span className="font-mono font-bold text-zinc-700">{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"
|
||||
/>
|
||||
<span className="font-mono font-bold text-zinc-700">{brandStyle.secondaryColor}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Studio Control 3 */}
|
||||
<div className="space-y-1">
|
||||
<label className="font-bold text-zinc-500 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]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Studio Control 4 */}
|
||||
<div className="flex items-center justify-between p-sm bg-[#f8fafc] border border-zinc-200/50 rounded-lg">
|
||||
<div>
|
||||
<h4 className="font-bold text-zinc-900 text-xs">Acknowledge Eco-Certified Badge</h4>
|
||||
<p className="text-[10px] text-zinc-400 mt-0.5">Prints stamp acknowledging sustainable jute bag usage.</p>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={brandStyle.isEcoVerified}
|
||||
onChange={() => setBrandStyle({ ...brandStyle, isEcoVerified: !brandStyle.isEcoVerified })}
|
||||
className="w-4 h-4 text-emerald-600 border-[#e2e8f0] rounded focus:ring-0 outline-none cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Interactive Dynamic Checkout Jute Bag Preview Canvas */}
|
||||
<div className="border border-[#e2e8f0] rounded-xl p-md bg-zinc-50 space-y-sm">
|
||||
<span className="text-[9px] font-sans font-bold text-zinc-400 uppercase tracking-widest block text-center border-b border-zinc-200 pb-1">
|
||||
Live Packaged Grocery Bag Design Preview
|
||||
</span>
|
||||
|
||||
<div className="relative mx-auto w-48 h-64 bg-[#efe5d9] border-2 border-[#d2b48c] rounded-b-2xl rounded-t-lg shadow-inner flex flex-col justify-between p-sm">
|
||||
|
||||
{/* Hanging handle simulation */}
|
||||
<div className="absolute -top-3 left-1/2 -translate-x-1/2 w-20 h-5 border-2 border-b-0 border-[#d2b48c] rounded-t-full" />
|
||||
|
||||
<div className="text-center pt-md space-y-1">
|
||||
<span className="text-[10px] font-bold block tracking-tight uppercase" style={{ color: brandStyle.primaryColor }}>
|
||||
{brandStyle.themeName || 'nearledaily Fresh'}
|
||||
</span>
|
||||
<div className="w-12 h-0.5 mx-auto bg-amber-500" style={{ backgroundColor: brandStyle.secondaryColor }} />
|
||||
</div>
|
||||
|
||||
<div className="my-auto flex flex-col items-center text-center p-1 space-y-1">
|
||||
<ShoppingBag className="w-10 h-10 stroke-1" style={{ color: brandStyle.primaryColor }} />
|
||||
<span className="text-[9px] font-medium max-w-[130px] leading-tight block text-zinc-700">
|
||||
{brandStyle.bagLabel || 'Grown with Pride'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center text-[8px] border-t border-zinc-300 pt-1">
|
||||
<span className="font-bold text-zinc-500">100% COMPOSTABLE</span>
|
||||
{brandStyle.isEcoVerified && (
|
||||
<span className="text-emerald-700 font-bold bg-emerald-100 px-1 py-0.5 rounded text-[7px]">
|
||||
CBE-ECO
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-[10px] text-zinc-405 font-medium">Standard printed thermal stamps scale according to the preview.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CREATE NEW PRODUCT MODAL PORTAL */}
|
||||
{showAddProductModal && (
|
||||
<div
|
||||
className="fixed inset-0 bg-[#0f172a]/40 backdrop-blur-sm z-[200] flex items-center justify-center p-md"
|
||||
onClick={(e) => { if (e.target === e.currentTarget) setShowAddProductModal(false); }}
|
||||
>
|
||||
<div className="bg-white border border-[#e2e8f0] rounded-xl w-full max-w-[28rem] max-h-[90vh] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200 text-xs font-sans cursor-default">
|
||||
<div className="p-md border-b border-[#e2e8f0] bg-[#f8fafc] flex justify-between items-center shrink-0">
|
||||
<h4 className="font-bold text-[#0f172a] flex items-center gap-xs">
|
||||
<Package size={15} className="text-[#581c87]" />
|
||||
Introduce New Grocery Catalog SKU
|
||||
</h4>
|
||||
<button
|
||||
onClick={() => setShowAddProductModal(false)}
|
||||
className="p-1 hover:bg-zinc-200 rounded-full text-zinc-400 cursor-pointer transition-colors"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleAddNewProduct} className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||
<div className="p-md space-y-md overflow-y-auto flex-1">
|
||||
<div className="space-y-sm">
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">PRODUCT BRAND NAME (*)</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. Aavin Pure Cow Ghee"
|
||||
value={newProduct.name}
|
||||
onChange={(e) => setNewProduct({ ...newProduct, name: e.target.value })}
|
||||
className="w-full border border-[#e2e8f0] rounded-lg p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87]"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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]">SKU CODE IDENTIFIER (*)</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="e.g. GHEE-AAV-500"
|
||||
value={newProduct.sku}
|
||||
onChange={(e) => setNewProduct({ ...newProduct, sku: 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-mono"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">CATEGORY SEGMENT</label>
|
||||
<select
|
||||
value={newProduct.category}
|
||||
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>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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]">ESTIMATED price (₹)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={newProduct.price}
|
||||
onChange={(e) => setNewProduct({ ...newProduct, price: Number(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]"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">INITIAL ALLOCATED BALANCES</label>
|
||||
<input
|
||||
type="number"
|
||||
value={newProduct.initialStock}
|
||||
onChange={(e) => setNewProduct({ ...newProduct, initialStock: Number(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]"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">PRODUCT IMAGE PATH OR LINK</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newProduct.image}
|
||||
onChange={(e) => setNewProduct({ ...newProduct, image: 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]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-md border-t border-[#f1f5f9] flex justify-end gap-sm bg-[#f8fafc] shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAddProductModal(false)}
|
||||
className="px-4 py-2 border border-[#e2e8f0] rounded-lg font-semibold text-zinc-500 hover:bg-zinc-50 cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 bg-[#581c87] text-white rounded-lg font-bold hover:bg-purple-800 cursor-pointer shadow-sm"
|
||||
>
|
||||
Commit Product Design SKU
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user