1550 lines
87 KiB
TypeScript
1550 lines
87 KiB
TypeScript
/**
|
|
* @license
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
ArrowLeft,
|
|
Calendar,
|
|
TrendingUp,
|
|
Layers,
|
|
Users,
|
|
Phone,
|
|
MapPin,
|
|
AlertTriangle,
|
|
Clock,
|
|
Activity,
|
|
CheckCircle2,
|
|
Package,
|
|
Search,
|
|
ShoppingCart,
|
|
Send,
|
|
Download,
|
|
X,
|
|
Battery,
|
|
ShieldCheck,
|
|
Globe,
|
|
UploadCloud,
|
|
FileText,
|
|
Mail,
|
|
UserCheck,
|
|
CreditCard,
|
|
History,
|
|
Building,
|
|
Award,
|
|
ShoppingBag
|
|
} from 'lucide-react';
|
|
import {
|
|
useFiestaStockStatement,
|
|
useFiestaTenantCustomers,
|
|
FIESTA_TENANT_ID
|
|
} from '../services/fiestaQueries';
|
|
import { str as fstr } from '../services/fiestaApi';
|
|
import {
|
|
initialInventory,
|
|
initialCustomerOrders,
|
|
operationalAlerts
|
|
} from '../data';
|
|
import ragulStoreCover from '../assets/images/store_front_view_1780299351800.png';
|
|
import OrdersDeliveriesView from './OrdersDeliveriesView';
|
|
|
|
interface StoreDetailViewProps {
|
|
store: {
|
|
locationid?: number;
|
|
name: string;
|
|
zone: string;
|
|
deliveries: number;
|
|
sales: string;
|
|
orders?: number;
|
|
staff: string;
|
|
color: string;
|
|
status: string;
|
|
};
|
|
onBack: () => void;
|
|
}
|
|
|
|
// ── Master Global Catalogue Items ──
|
|
const GLOBAL_CATALOGUE_ITEMS = [
|
|
{ sku: 'SALT-TATA-1KG', name: 'Tata Salt Premium Iodized 1kg', category: 'Staples / Salt', price: 28, image: 'https://images.unsplash.com/photo-1626132647523-66f5bf380027?auto=format&fit=crop&w=150&q=80' },
|
|
{ sku: 'SUN-OIL-1LIT', name: 'Gold Winner Sunflower Oil 1L', category: 'Groceries / Oils', price: 145, image: 'https://images.unsplash.com/photo-1474979266404-7eaacbcd87c5?auto=format&fit=crop&w=150&q=80' },
|
|
{ sku: 'BISCUIT-MAR-GD', name: 'Britannia Marie Gold Biscuit 250g', category: 'Snacks / Biscuits', price: 35, image: 'https://images.unsplash.com/photo-1558961363-fa8fdf82db35?auto=format&fit=crop&w=150&q=80' },
|
|
{ sku: 'SPICE-SAMBAR-MTR', name: 'MTR Sambar Powder 200g', category: 'Groceries / Spices', price: 85, image: 'https://images.unsplash.com/photo-1596040033229-a9821ebd058d?auto=format&fit=crop&w=150&q=80' },
|
|
{ sku: 'AAVIN-BUTTER-500', name: 'Aavin Salted Butter 500g', category: 'Dairy / Butter', price: 260, image: 'https://images.unsplash.com/photo-1589985270826-4b7bb135bc9d?auto=format&fit=crop&w=150&q=80' }
|
|
];
|
|
|
|
// Fallback cover images
|
|
const DETAIL_STORE_COVERS = [
|
|
'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&w=800&q=80',
|
|
'https://images.unsplash.com/photo-1578916171728-46686eac8d58?auto=format&fit=crop&w=800&q=80',
|
|
'https://images.unsplash.com/photo-1604719312566-8912e9227c6a?auto=format&fit=crop&w=800&q=80',
|
|
'https://images.unsplash.com/photo-1534723452862-4c874018d66d?auto=format&fit=crop&w=800&q=80',
|
|
'https://images.unsplash.com/photo-1582408929130-98a2c2640b8a?auto=format&fit=crop&w=800&q=80',
|
|
'https://images.unsplash.com/photo-1516594798947-e65505dbb29d?auto=format&fit=crop&w=800&q=80',
|
|
'https://images.unsplash.com/photo-1601599561263-60a4e4e083cd?auto=format&fit=crop&w=800&q=80',
|
|
'https://images.unsplash.com/photo-1441986300917-64674bd600d8?auto=format&fit=crop&w=800&q=80',
|
|
'https://images.unsplash.com/photo-1528698827591-e19ccd7bc23d?auto=format&fit=crop&w=800&q=80',
|
|
'https://images.unsplash.com/photo-1536697246787-1f7ae568d89a?auto=format&fit=crop&w=800&q=80',
|
|
'https://images.unsplash.com/photo-1506617498306-bd97b3663b65?auto=format&fit=crop&w=800&q=80',
|
|
'https://images.unsplash.com/photo-1579621970563-ebec7560ff3e?auto=format&fit=crop&w=800&q=80'
|
|
];
|
|
|
|
export default function StoreDetailView({ store, onBack }: StoreDetailViewProps) {
|
|
const [activeTab, setActiveTab] = useState<'overview' | 'inventory' | 'customers' | 'orders'>('overview');
|
|
|
|
const isRagul = store.name.toLowerCase().includes('ragul');
|
|
const getStoreCover = () => {
|
|
if (isRagul) return ragulStoreCover;
|
|
let hash = 0;
|
|
for (let j = 0; j < store.name.length; j++) {
|
|
hash = store.name.charCodeAt(j) + ((hash << 5) - hash);
|
|
}
|
|
const idx = Math.abs(hash) % DETAIL_STORE_COVERS.length;
|
|
return DETAIL_STORE_COVERS[idx];
|
|
};
|
|
const storeCoverImage = getStoreCover();
|
|
const [stockSearch, setStockSearch] = useState('');
|
|
const [customerSearch, setCustomerSearch] = useState('');
|
|
const [hoveredChartIndex, setHoveredChartIndex] = useState<number | null>(null);
|
|
|
|
// ── Toast Notification state ──────────────────────────────────────────────
|
|
const [toast, setToast] = useState<{ show: boolean; message: string; type: 'success' | 'info' | 'warning' }>({
|
|
show: false,
|
|
message: '',
|
|
type: 'success'
|
|
});
|
|
|
|
const showToast = (message: string, type: 'success' | 'info' | 'warning' = 'success') => {
|
|
setToast({ show: true, message, type });
|
|
setTimeout(() => {
|
|
setToast(prev => ({ ...prev, show: false }));
|
|
}, 4000);
|
|
};
|
|
|
|
// ── Replenishment Modal state ──────────────────────────────────────────────
|
|
const [replenishModal, setReplenishModal] = useState<{ show: boolean; item: any | null }>({
|
|
show: false,
|
|
item: null
|
|
});
|
|
const [replenishQty, setReplenishQty] = useState(100);
|
|
|
|
// ── Catalogue Local State & Modals ────────────────────────────────────────
|
|
const [localInventory, setLocalInventory] = useState<any[]>([]);
|
|
const [showImportModal, setShowImportModal] = useState(false);
|
|
const [importState, setImportState] = useState<'idle' | 'reading' | 'parsing' | 'saving' | 'done'>('idle');
|
|
const [showGlobalModal, setShowGlobalModal] = useState(false);
|
|
const [selectedGlobalSkus, setSelectedGlobalSkus] = useState<string[]>([]);
|
|
|
|
// ── Customer CRM Profile Drawer state ──────────────────────────────────────
|
|
const [selectedCustomer, setSelectedCustomer] = useState<any | null>(null);
|
|
|
|
// ── API Queries with live locationid ───────────────────────────────────────
|
|
const locationid = store.locationid || 1097;
|
|
const stockQ = useFiestaStockStatement({
|
|
tenantid: FIESTA_TENANT_ID,
|
|
locationid,
|
|
pagesize: 100
|
|
});
|
|
const customersQ = useFiestaTenantCustomers({
|
|
tenantid: FIESTA_TENANT_ID,
|
|
locationid,
|
|
pagesize: 100
|
|
});
|
|
|
|
// ── Seed / Fallback calculation helpers ────────────────────────────────────
|
|
const parseOrdersCount = (salesStr: string): number => {
|
|
const num = parseInt(salesStr.replace(/[^0-9]/g, ''), 10);
|
|
return isNaN(num) ? 45 : num;
|
|
};
|
|
|
|
const baseOrders = parseOrdersCount(store.sales);
|
|
const revenueToday = baseOrders > 100 ? Math.round(baseOrders * 320) : 48200;
|
|
|
|
// ── Interval slots with heights for chart representation ──────────────────
|
|
const intervalSlots = [
|
|
{ time: '06:00 AM - 10:00 AM', label: 'Morning Rush', orders: Math.round(store.deliveries * 0.35) || 14, sales: `₹${Math.round(revenueToday * 0.3).toLocaleString('en-IN')}`, height: '70%', status: 'PEAK' },
|
|
{ time: '10:00 AM - 02:00 PM', label: 'Mid-day Deliveries', orders: Math.round(store.deliveries * 0.25) || 10, sales: `₹${Math.round(revenueToday * 0.25).toLocaleString('en-IN')}`, height: '50%', status: 'NORMAL' },
|
|
{ time: '02:00 PM - 06:00 PM', label: 'Afternoon Dispatch', orders: Math.round(store.deliveries * 0.15) || 6, sales: `₹${Math.round(revenueToday * 0.15).toLocaleString('en-IN')}`, height: '30%', status: 'LOW' },
|
|
{ time: '06:00 PM - 10:00 PM', label: 'Evening Surge', orders: Math.round(store.deliveries * 0.2) || 8, sales: `₹${Math.round(revenueToday * 0.25).toLocaleString('en-IN')}`, height: '45%', status: 'HIGH' },
|
|
{ time: '10:00 PM - 06:00 AM', label: 'Night Prep', orders: Math.round(store.deliveries * 0.05) || 2, sales: `₹${Math.round(revenueToday * 0.05).toLocaleString('en-IN')}`, height: '15%', status: 'LOW' }
|
|
];
|
|
|
|
const activeSlotIndex = hoveredChartIndex !== null ? hoveredChartIndex : 0;
|
|
const activeSlot = intervalSlots[activeSlotIndex];
|
|
|
|
// Past 7 Days Log
|
|
const pastDaysLog = [
|
|
{ day: 'Wednesday (Today)', orders: store.deliveries || 40, sales: `₹${revenueToday.toLocaleString('en-IN')}`, rate: '98.2%', change: '+4.5%' },
|
|
{ day: 'Tuesday', orders: Math.round(store.deliveries * 0.9) || 36, sales: `₹${Math.round(revenueToday * 0.88).toLocaleString('en-IN')}`, rate: '100%', change: '+1.2%' },
|
|
{ day: 'Monday', orders: Math.round(store.deliveries * 0.85) || 34, sales: `₹${Math.round(revenueToday * 0.82).toLocaleString('en-IN')}`, rate: '96.8%', change: '-2.1%' },
|
|
{ day: 'Sunday', orders: Math.round(store.deliveries * 1.2) || 48, sales: `₹${Math.round(revenueToday * 1.15).toLocaleString('en-IN')}`, rate: '94.5%', change: '+12.4%' },
|
|
{ day: 'Saturday', orders: Math.round(store.deliveries * 1.15) || 46, sales: `₹${Math.round(revenueToday * 1.12).toLocaleString('en-IN')}`, rate: '99.1%', change: '+8.6%' },
|
|
{ day: 'Friday', orders: Math.round(store.deliveries * 0.95) || 38, sales: `₹${Math.round(revenueToday * 0.92).toLocaleString('en-IN')}`, rate: '97.4%', change: '+2.0%' },
|
|
{ day: 'Thursday', orders: Math.round(store.deliveries * 0.9) || 36, sales: `₹${Math.round(revenueToday * 0.89).toLocaleString('en-IN')}`, rate: '100%', change: '+0.5%' }
|
|
];
|
|
|
|
// Inventory mapping (derived + live merges)
|
|
const getMergedInventory = () => {
|
|
const rawStock = stockQ.data ?? [];
|
|
|
|
const resolveMetadata = (name: string) => {
|
|
const nameLower = name.toLowerCase();
|
|
let price = 60;
|
|
let image = 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&w=150&q=80';
|
|
|
|
if (nameLower.includes('rice')) {
|
|
price = 1400;
|
|
image = 'https://images.unsplash.com/photo-1586201375761-83865001e31c?auto=format&fit=crop&w=150&q=80';
|
|
} else if (nameLower.includes('oil')) {
|
|
price = 340;
|
|
image = 'https://images.unsplash.com/photo-1474979266404-7eaacbcd87c5?auto=format&fit=crop&w=150&q=80';
|
|
} else if (nameLower.includes('coffee')) {
|
|
price = 195;
|
|
image = 'https://images.unsplash.com/photo-1514432324607-a09d9b4aefdd?auto=format&fit=crop&w=150&q=80';
|
|
} else if (nameLower.includes('carrot')) {
|
|
price = 60;
|
|
image = 'https://images.unsplash.com/photo-1598170845058-32b9d6a5da37?auto=format&fit=crop&w=150&q=80';
|
|
} else if (nameLower.includes('ghee')) {
|
|
price = 320;
|
|
image = 'https://images.unsplash.com/photo-1589985270826-4b7bb135bc9d?auto=format&fit=crop&w=150&q=80';
|
|
} else if (nameLower.includes('butter')) {
|
|
price = 260;
|
|
image = 'https://images.unsplash.com/photo-1589985270826-4b7bb135bc9d?auto=format&fit=crop&w=150&q=80';
|
|
} else if (nameLower.includes('salt')) {
|
|
price = 28;
|
|
image = 'https://images.unsplash.com/photo-1626132647523-66f5bf380027?auto=format&fit=crop&w=150&q=80';
|
|
} else if (nameLower.includes('atta') || nameLower.includes('flour')) {
|
|
price = 440;
|
|
image = 'https://images.unsplash.com/photo-1574316071802-0d684efa7bf5?auto=format&fit=crop&w=150&q=80';
|
|
}
|
|
return { price, image };
|
|
};
|
|
|
|
if (rawStock.length > 0) {
|
|
return rawStock.map((item: any) => {
|
|
const name = fstr(item.productname) || fstr(item.name) || 'Product Item';
|
|
const meta = resolveMetadata(name);
|
|
return {
|
|
sku: fstr(item.sku) || fstr(item.productsku) || 'SKU-UNKNOWN',
|
|
name,
|
|
category: fstr(item.subcategoryname) || fstr(item.categoryname) || 'Groceries / Staples',
|
|
stockLevel: Number(item.physicalstock) || Number(item.stock) || 0,
|
|
maxCapacity: Number(item.maxcapacity) || 500,
|
|
status: (Number(item.physicalstock) || 0) < 50 ? 'Critical' : (Number(item.physicalstock) || 0) < 150 ? 'Low Stock' : 'Optimal',
|
|
price: meta.price,
|
|
image: meta.image
|
|
};
|
|
});
|
|
}
|
|
|
|
// High fidelity fallback seeded catalog if live API results are empty
|
|
return [
|
|
{ sku: 'RICE-PN-50', name: 'Premium Ponni Rice Bag 25kg', category: 'Staples / Rice', stockLevel: Math.round(baseOrders * 1.2) || 450, maxCapacity: 1000, status: 'Optimal', price: 1400, image: 'https://images.unsplash.com/photo-1586201375761-83865001e31c?auto=format&fit=crop&w=150&q=80' },
|
|
{ sku: 'ATTA-ASH-10', name: 'Aashirvaad Chakki Atta 10kg', category: 'Staples / Flour', stockLevel: 12, maxCapacity: 120, status: 'Critical', price: 440, image: 'https://images.unsplash.com/photo-1574316071802-0d684efa7bf5?auto=format&fit=crop&w=150&q=80' },
|
|
{ sku: 'OIL-IDH-05', name: 'Idhayam Sesame Oil Can 5L', category: 'Groceries / Oils', stockLevel: 32, maxCapacity: 150, status: 'Low Stock', price: 340, image: 'https://images.unsplash.com/photo-1474979266404-7eaacbcd87c5?auto=format&fit=crop&w=150&q=80' },
|
|
{ sku: 'COF-NAR-01', name: 'Narasus Filter Coffee 1kg Pack', category: 'Beverages / Coffee', stockLevel: Math.round(baseOrders * 0.5) || 120, maxCapacity: 300, status: 'Optimal', price: 195, image: 'https://images.unsplash.com/photo-1514432324607-a09d9b4aefdd?auto=format&fit=crop&w=150&q=80' },
|
|
{ sku: 'MILK-AAV-50', name: 'Aavin Premium Pouch Milk 500ml', category: 'Dairy / Milk', stockLevel: 18, maxCapacity: 200, status: 'Critical', price: 28, image: 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&w=150&q=80' },
|
|
{ sku: 'OOTY-CARROT-1KG', name: 'Ooty Fresh Quality Carrots 1kg', category: 'Fresh Produce / Veg', stockLevel: 45, maxCapacity: 100, status: 'Low Stock', price: 60, image: 'https://images.unsplash.com/photo-1598170845058-32b9d6a5da37?auto=format&fit=crop&w=150&q=80' },
|
|
{ sku: 'TURMERIC-200G', name: 'Organic Turmeric Powder 200g', category: 'Groceries / Spices', stockLevel: 280, maxCapacity: 300, status: 'Optimal', price: 90, image: 'https://images.unsplash.com/photo-1596040033229-a9821ebd058d?auto=format&fit=crop&w=150&q=80' },
|
|
{ sku: 'GHEE-500ML', name: 'Pure Cow Ghee 500ml', category: 'Dairy / Ghee', stockLevel: 75, maxCapacity: 250, status: 'Low Stock', price: 320, image: 'https://images.unsplash.com/photo-1589985270826-4b7bb135bc9d?auto=format&fit=crop&w=150&q=80' }
|
|
];
|
|
};
|
|
|
|
// Sync loaded stock to state
|
|
useEffect(() => {
|
|
setLocalInventory(getMergedInventory());
|
|
}, [stockQ.data, store.locationid]);
|
|
|
|
const inventoryList = localInventory.filter(item =>
|
|
item.name.toLowerCase().includes(stockSearch.toLowerCase()) ||
|
|
item.sku.toLowerCase().includes(stockSearch.toLowerCase()) ||
|
|
item.category.toLowerCase().includes(stockSearch.toLowerCase())
|
|
);
|
|
|
|
// Customer Directory mapping (derived + live merges)
|
|
const getMergedCustomers = () => {
|
|
const rawCustomers = customersQ.data ?? [];
|
|
if (rawCustomers.length > 0) {
|
|
return rawCustomers.map((c: any) => ({
|
|
name: fstr(c.fullname) || `${fstr(c.firstname)} ${fstr(c.lastname)}`.trim() || 'Customer',
|
|
phone: fstr(c.contactno) || '—',
|
|
address: fstr(c.address) || 'Coimbatore',
|
|
ordersCount: Number(c.orderscount) || Math.floor(Math.random() * 8) + 1,
|
|
totalSpent: `₹${(Number(c.totalspent) || Math.floor(Math.random() * 4000) + 500).toLocaleString('en-IN')}`,
|
|
lastOrder: '2 Days ago'
|
|
}));
|
|
}
|
|
|
|
// High fidelity fallback customer base
|
|
return [
|
|
{ name: 'Meenakshi Sundaram', phone: '+91 94432 18942', address: 'Plot 4, Lakshmipuram Ext, RS Puram, Coimbatore - 641002', ordersCount: 42, totalSpent: '₹34,820.00', lastOrder: 'Today, 14:24 PM' },
|
|
{ name: 'Senthil Kumar VSD', phone: '+91 98421 00234', address: 'Flat 2C, Whispering Palms, Avinashi Road, Peelamedu - 641004', ordersCount: 28, totalSpent: '₹22,910.00', lastOrder: 'Yesterday, 14:10 PM' },
|
|
{ name: 'Kavitha Ramaswamy', phone: '+91 90035 88921', address: 'No 15, Cross Cut Road, Gandhipuram, Coimbatore - 641012', ordersCount: 19, totalSpent: '₹14,240.00', lastOrder: '2 Days ago' },
|
|
{ name: 'Dr. Anand Selvapandian', phone: '+91 97890 22104', address: 'Villa 12, Sobha Elanza, Sathy Road, Saravanampatti - 641035', ordersCount: 12, totalSpent: '₹9,800.00', lastOrder: '3 Days ago' },
|
|
{ name: 'Rajesh Subramaniam', phone: '+91 94421 88902', address: '45, West Club Road, Race Course, Coimbatore - 641018', ordersCount: 64, totalSpent: '₹84,900.00', lastOrder: 'Today, 10:15 AM' },
|
|
{ name: 'Priya Krishnan', phone: '+91 90432 11094', address: '8C, Royal Arcade, Trichy Road, Singanallur - 641005', ordersCount: 7, totalSpent: '₹4,120.00', lastOrder: '1 Week ago' }
|
|
];
|
|
};
|
|
|
|
const customersList = getMergedCustomers().filter(c =>
|
|
c.name.toLowerCase().includes(customerSearch.toLowerCase()) ||
|
|
c.phone.includes(customerSearch) ||
|
|
c.address.toLowerCase().includes(customerSearch.toLowerCase())
|
|
);
|
|
|
|
// Store Alerts specific to the store
|
|
const storeAlerts = operationalAlerts.filter(alert =>
|
|
alert.title.toLowerCase().includes(store.name.split(' ')[0].toLowerCase()) ||
|
|
alert.details.toLowerCase().includes(store.name.split(' ')[0].toLowerCase())
|
|
);
|
|
|
|
// ── Riders fleet data list ────────────────────────────────────────────────
|
|
const activeRiders = [
|
|
{ name: 'Karthikeyan Radhakrishnan', initial: 'KR', status: 'Delivering', orders: 3, battery: 92, lastPing: '2m ago' },
|
|
{ name: 'Arun Kumar Chinnasamy', initial: 'AC', status: 'Delivering', orders: 2, battery: 48, lastPing: '10m ago' },
|
|
{ name: 'Suresh Balasubramaniam', initial: 'SB', status: 'Idle', orders: 0, battery: 84, lastPing: 'Just now' },
|
|
{ name: 'Manoj Kumar Gowda', initial: 'MG', status: 'Delivering', orders: 1, battery: 14, lastPing: '1m ago' }
|
|
];
|
|
|
|
// Actions simulation handles
|
|
const handleReplenishSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!replenishModal.item) return;
|
|
setLocalInventory(prev => prev.map(item => {
|
|
if (item.sku === replenishModal.item.sku) {
|
|
const newStock = Math.min(item.stockLevel + replenishQty, item.maxCapacity);
|
|
return {
|
|
...item,
|
|
stockLevel: newStock,
|
|
status: newStock < 50 ? 'Critical' : newStock < 150 ? 'Low Stock' : 'Optimal'
|
|
};
|
|
}
|
|
return item;
|
|
}));
|
|
showToast(`Replenishment batch ticket generated for ${replenishQty} units of ${replenishModal.item.name}.`, 'success');
|
|
setReplenishModal({ show: false, item: null });
|
|
};
|
|
|
|
// CSV Import simulation trigger
|
|
const handleStartCsvImport = () => {
|
|
setImportState('reading');
|
|
setTimeout(() => {
|
|
setImportState('parsing');
|
|
setTimeout(() => {
|
|
setImportState('saving');
|
|
setTimeout(() => {
|
|
const newItems = [
|
|
{ sku: 'NOODLE-MAG-12', name: 'Maggi 2-Min Masala Noodles 280g', category: 'Snacks / Noodles', stockLevel: 80, maxCapacity: 250, status: 'Optimal', price: 45, image: 'https://images.unsplash.com/photo-1569718212165-3a8278d5f624?auto=format&fit=crop&w=150&q=80' },
|
|
{ sku: 'DET-SURF-1KG', name: 'Surf Excel Easy Wash Detergent 1kg', category: 'Household / Detergent', stockLevel: 14, maxCapacity: 100, status: 'Critical', price: 165, image: 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&w=150&q=80' },
|
|
{ sku: 'SALT-TATA-1KG', name: 'Tata Salt Premium Iodized 1kg', category: 'Staples / Salt', stockLevel: 180, maxCapacity: 300, status: 'Optimal', price: 28, image: 'https://images.unsplash.com/photo-1626132647523-66f5bf380027?auto=format&fit=crop&w=150&q=80' }
|
|
];
|
|
|
|
// Add to localInventory state preventing duplicates
|
|
setLocalInventory(prev => {
|
|
const filtered = prev.filter(item => !newItems.some(ni => ni.sku === item.sku));
|
|
return [...filtered, ...newItems];
|
|
});
|
|
|
|
setImportState('done');
|
|
showToast(`CSV Inventory manifest sync complete. 3 items added to local stocks.`, 'success');
|
|
}, 800);
|
|
}, 700);
|
|
}, 700);
|
|
};
|
|
|
|
// Add items from Global Catalog
|
|
const handleAddGlobalCatalogue = () => {
|
|
if (selectedGlobalSkus.length === 0) {
|
|
showToast('Kindly select at least one catalogue item.', 'warning');
|
|
return;
|
|
}
|
|
|
|
const itemsToAdd = GLOBAL_CATALOGUE_ITEMS.filter(item => selectedGlobalSkus.includes(item.sku)).map(item => ({
|
|
...item,
|
|
stockLevel: Math.floor(Math.random() * 80) + 20,
|
|
maxCapacity: 200,
|
|
status: 'Optimal'
|
|
}));
|
|
|
|
setLocalInventory(prev => {
|
|
const filtered = prev.filter(item => !itemsToAdd.some(ni => ni.sku === item.sku));
|
|
return [...filtered, ...itemsToAdd];
|
|
});
|
|
|
|
showToast(`${itemsToAdd.length} products synced from Master Global Catalogue successfully!`, 'success');
|
|
setSelectedGlobalSkus([]);
|
|
setShowGlobalModal(false);
|
|
};
|
|
|
|
const handleExportLedger = () => {
|
|
showToast(`Generating secure PDF ledger audit reports for ${store.name}...`, 'info');
|
|
setTimeout(() => {
|
|
showToast(`Ledger spreadsheet decrypted and downloaded successfully.`, 'success');
|
|
}, 2000);
|
|
};
|
|
|
|
const handleStaffBroadcast = () => {
|
|
const text = prompt('Type notification message to broadcast to all cashiers and staff at this store:');
|
|
if (text) {
|
|
showToast(`Broadcast notification dispatched to all active terminals at ${store.name}.`, 'success');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-lg animate-in fade-in duration-500 relative font-sans text-zinc-900 pb-xl">
|
|
|
|
{/* ── Floating Alert Toast UI ── */}
|
|
{toast.show && (
|
|
<div className="fixed top-24 right-6 z-[300] bg-[#0f172a] text-white border border-[#334155] px-lg py-3 rounded-xl shadow-2xl flex items-center gap-md animate-in slide-in-from-top duration-300">
|
|
{toast.type === 'success' && <CheckCircle2 size={16} className="text-emerald-400" />}
|
|
{toast.type === 'info' && <Activity size={16} className="text-purple-400" />}
|
|
{toast.type === 'warning' && <AlertTriangle size={16} className="text-amber-400" />}
|
|
<span className="text-xs font-semibold">{toast.message}</span>
|
|
<button onClick={() => setToast(prev => ({ ...prev, show: false }))} className="text-zinc-500 hover:text-zinc-300 cursor-pointer">
|
|
<X size={14} />
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* ── Subheader Navigation Bar ── */}
|
|
<div className="flex items-center justify-between">
|
|
<button
|
|
onClick={onBack}
|
|
className="flex items-center gap-xs text-xs font-bold text-[#581c87] hover:text-[#4c1d95] bg-purple-50 hover:bg-purple-100/80 px-xl py-2 rounded-lg transition-all shadow-sm border border-purple-100 cursor-pointer"
|
|
>
|
|
<ArrowLeft size={14} />
|
|
<span>Back to Registry</span>
|
|
</button>
|
|
|
|
<div className="flex items-center gap-xs">
|
|
<span className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse" />
|
|
<span className="text-[10px] font-bold tracking-widest text-emerald-600 uppercase">System Sync Active</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── Immersive Analytics Banner (With Store Cover Image & Slate Gradient Overlay) ── */}
|
|
<div className="relative overflow-hidden rounded-2xl p-6 md:p-8 text-white shadow-xl border border-purple-500/20 mb-8 animate-in fade-in duration-300">
|
|
{/* Cover Image Background */}
|
|
<div className="absolute inset-0 z-0">
|
|
<img
|
|
src={storeCoverImage}
|
|
alt={store.name}
|
|
className="w-full h-full object-cover object-center"
|
|
/>
|
|
<div className="absolute inset-0 bg-gradient-to-r from-slate-950 via-slate-900/90 to-purple-950/80" />
|
|
</div>
|
|
|
|
{/* Background decorative glowing circles */}
|
|
<div className="absolute top-0 right-0 w-72 h-72 bg-purple-500/10 rounded-full blur-3xl -mr-20 -mt-20 pointer-events-none z-0" />
|
|
<div className="absolute bottom-0 left-0 w-56 h-56 bg-slate-500/5 rounded-full blur-2xl -ml-20 -mb-20 pointer-events-none z-0" />
|
|
|
|
<div className="relative flex flex-col md:flex-row md:items-center justify-between gap-6 z-10">
|
|
<div className="space-y-2">
|
|
<div className="inline-flex items-center gap-2 px-2.5 py-1 rounded-full bg-purple-500/20 border border-purple-400/30 text-purple-200 text-[10px] font-bold uppercase tracking-widest">
|
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
|
Outlet Node #{locationid} Operations
|
|
</div>
|
|
<h2 className="font-sans font-bold text-3xl tracking-tight bg-gradient-to-r from-white via-slate-100 to-purple-200 bg-clip-text text-transparent">
|
|
{store.name}
|
|
</h2>
|
|
<div className="flex flex-wrap gap-2 text-xs text-purple-200 mt-1">
|
|
<span className="flex items-center gap-1">
|
|
<MapPin size={13} className="text-purple-300" /> {store.zone}
|
|
</span>
|
|
<span className="flex items-center gap-1 ml-4">
|
|
<Users size={13} className="text-purple-300" /> Node Lead: <strong>{store.staff}</strong>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Metrics grid */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-4 gap-4 mt-8 pt-6 border-t border-slate-800/80 relative z-10">
|
|
<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">Node Status</span>
|
|
<div className="p-2 rounded-lg bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 group-hover:scale-110 transition-transform">
|
|
<Building className="w-4 h-4" />
|
|
</div>
|
|
</div>
|
|
<div className="mt-2">
|
|
<h3 className="text-2xl font-extrabold tracking-tight font-mono">{store.status}</h3>
|
|
<p className="text-[10px] text-emerald-400 font-semibold mt-1">● Online Operations</p>
|
|
</div>
|
|
</div>
|
|
|
|
<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">Today's Orders</span>
|
|
<div className="p-2 rounded-lg bg-purple-500/10 text-purple-400 border border-purple-500/20 group-hover:scale-110 transition-transform">
|
|
<ShoppingCart className="w-4 h-4" />
|
|
</div>
|
|
</div>
|
|
<div className="mt-2">
|
|
<h3 className="text-2xl font-extrabold tracking-tight font-mono">{Math.max(store.deliveries, store.orders ?? 0)}</h3>
|
|
<p className="text-[10px] text-slate-400 font-semibold mt-1">Incoming Volume</p>
|
|
</div>
|
|
</div>
|
|
|
|
<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">Today's Dispatches</span>
|
|
<div className="p-2 rounded-lg bg-purple-500/10 text-purple-400 border border-purple-500/20 group-hover:scale-110 transition-transform">
|
|
<TrendingUp className="w-4 h-4" />
|
|
</div>
|
|
</div>
|
|
<div className="mt-2">
|
|
<h3 className="text-2xl font-extrabold tracking-tight font-mono">{store.deliveries}</h3>
|
|
<p className="text-[10px] text-slate-400 font-semibold mt-1">Dispatched Deliveries</p>
|
|
</div>
|
|
</div>
|
|
|
|
<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">Fulfillment Rate</span>
|
|
<div className="p-2 rounded-lg bg-indigo-500/10 text-indigo-400 border border-indigo-500/20 group-hover:scale-110 transition-transform">
|
|
<Award className="w-4 h-4" />
|
|
</div>
|
|
</div>
|
|
<div className="mt-2">
|
|
<h3 className="text-2xl font-extrabold tracking-tight font-mono">
|
|
{Math.max(store.deliveries, store.orders ?? 0) > 0
|
|
? `${Math.min(100, Math.round((store.deliveries / Math.max(store.deliveries, store.orders ?? 0)) * 100))}%`
|
|
: '100%'}
|
|
</h3>
|
|
<p className="text-[10px] text-indigo-400 font-semibold mt-1">Outlet OTIF Rate</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── Visual Glass-look Tab Controls ── */}
|
|
<div className="flex gap-2 border-b border-[#e2e8f0] pb-[1px] select-none overflow-x-auto">
|
|
<button
|
|
onClick={() => setActiveTab('overview')}
|
|
className={`flex items-center gap-xs px-md pb-sm text-xs font-bold uppercase tracking-wider cursor-pointer border-b-2 transition-all ${
|
|
activeTab === 'overview'
|
|
? 'border-b-[#581c87] text-[#581c87]'
|
|
: 'border-b-transparent text-zinc-400 hover:text-zinc-600'
|
|
}`}
|
|
>
|
|
<Activity size={14} />
|
|
<span>Overview & Performance</span>
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('inventory')}
|
|
className={`flex items-center gap-xs px-md pb-sm text-xs font-bold uppercase tracking-wider cursor-pointer border-b-2 transition-all ${
|
|
activeTab === 'inventory'
|
|
? 'border-b-[#581c87] text-[#581c87]'
|
|
: 'border-b-transparent text-zinc-400 hover:text-zinc-600'
|
|
}`}
|
|
>
|
|
<Layers size={14} />
|
|
<span>Inventory & Catalogue ({inventoryList.length})</span>
|
|
{inventoryList.some(item => item.status === 'Critical') && (
|
|
<span className="px-1.5 py-0.5 rounded-full bg-rose-500 text-white text-[8px] font-black leading-none animate-pulse">!</span>
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('customers')}
|
|
className={`flex items-center gap-xs px-md pb-sm text-xs font-bold uppercase tracking-wider cursor-pointer border-b-2 transition-all ${
|
|
activeTab === 'customers'
|
|
? 'border-b-[#581c87] text-[#581c87]'
|
|
: 'border-b-transparent text-zinc-400 hover:text-zinc-600'
|
|
}`}
|
|
>
|
|
<Users size={14} />
|
|
<span>Customer CRM Base ({customersList.length})</span>
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('orders')}
|
|
className={`flex items-center gap-xs px-md pb-sm text-xs font-bold uppercase tracking-wider cursor-pointer border-b-2 transition-all ${
|
|
activeTab === 'orders'
|
|
? 'border-b-[#581c87] text-[#581c87]'
|
|
: 'border-b-transparent text-zinc-400 hover:text-zinc-600'
|
|
}`}
|
|
>
|
|
<ShoppingBag size={14} />
|
|
<span>Orders & Deliveries</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* ── TAB PAYLOAD AREA ── */}
|
|
{activeTab === 'overview' && (
|
|
<div className="space-y-lg animate-in fade-in duration-300">
|
|
|
|
{/* Top Metric Cards */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-gutter">
|
|
<div className="bg-white border border-[#eceef2] rounded-2xl p-md shadow-sm hover:shadow-md transition-all duration-200 hover:-translate-y-0.5 relative group overflow-hidden">
|
|
<div className="w-8 h-8 rounded-xl bg-purple-50 text-[#581c87] flex items-center justify-center mb-sm group-hover:scale-110 transition-transform">
|
|
<CheckCircle2 size={16} />
|
|
</div>
|
|
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest block">OTIF Fulfillment</span>
|
|
<p className="text-2xl font-black text-[#0f172a] mt-xs">98.2%</p>
|
|
<div className="text-[10px] text-emerald-600 font-bold mt-1 flex items-center gap-xs">
|
|
<span>▲ 0.4%</span>
|
|
<span className="text-zinc-400 font-medium">vs past week</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white border border-[#eceef2] rounded-2xl p-md shadow-sm hover:shadow-md transition-all duration-200 hover:-translate-y-0.5 relative group overflow-hidden">
|
|
<div className="w-8 h-8 rounded-xl bg-emerald-50 text-emerald-600 flex items-center justify-center mb-sm group-hover:scale-110 transition-transform">
|
|
<TrendingUp size={16} />
|
|
</div>
|
|
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest block">Est. Revenue</span>
|
|
<p className="text-2xl font-black text-[#0f172a] mt-xs">₹{revenueToday.toLocaleString('en-IN')}</p>
|
|
<div className="text-[10px] text-emerald-600 font-bold mt-1 flex items-center gap-xs">
|
|
<span>▲ 12.4%</span>
|
|
<span className="text-zinc-400 font-medium">growth threshold</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white border border-[#eceef2] rounded-2xl p-md shadow-sm hover:shadow-md transition-all duration-200 hover:-translate-y-0.5 relative group overflow-hidden">
|
|
<div className="w-8 h-8 rounded-xl bg-sky-50 text-sky-600 flex items-center justify-center mb-sm group-hover:scale-110 transition-transform">
|
|
<ShoppingCart size={16} />
|
|
</div>
|
|
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest block">Total Dispatches</span>
|
|
<p className="text-2xl font-black text-[#0f172a] mt-xs">{store.deliveries}</p>
|
|
<div className="text-[10px] text-zinc-400 font-bold mt-1">
|
|
<span>Daily cap quota: 200</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white border border-[#eceef2] rounded-2xl p-md shadow-sm hover:shadow-md transition-all duration-200 hover:-translate-y-0.5 relative group overflow-hidden">
|
|
<div className="w-8 h-8 rounded-xl bg-amber-50 text-amber-600 flex items-center justify-center mb-sm group-hover:scale-110 transition-transform">
|
|
<Users size={16} />
|
|
</div>
|
|
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest block">Active Fleet</span>
|
|
<p className="text-2xl font-black text-[#0f172a] mt-xs">4 Riders</p>
|
|
<div className="text-[10px] text-emerald-600 font-bold mt-1 flex items-center gap-xs">
|
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
|
<span>3 dispatches live</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-gutter">
|
|
|
|
{/* Interactive Timeline Pipeline Flow */}
|
|
<div className="lg:col-span-2 bg-white border border-[#eceef2] rounded-2xl p-lg shadow-sm flex flex-col justify-between hover:shadow-md transition-shadow relative">
|
|
<div>
|
|
<div className="flex justify-between items-start">
|
|
<div>
|
|
<h3 className="font-sans font-bold text-sm text-[#0f172a] flex items-center gap-xs">
|
|
<Clock size={15} className="text-[#581c87]" /> Dispatch Flow Pipeline
|
|
</h3>
|
|
<p className="text-zinc-450 text-[10px] font-sans mt-0.5">Audit orders & revenue progression by selecting nodes along the daily operational path.</p>
|
|
</div>
|
|
<span className="text-[9px] font-bold text-purple-600 bg-purple-50 px-2 py-0.5 rounded-lg border border-purple-100 uppercase tracking-wider">Time Flow</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* The Pipeline Line and Nodes */}
|
|
<div className="relative py-xl px-lg select-none my-xl">
|
|
|
|
{/* Flow track base line */}
|
|
<div className="w-full bg-[#f1f5f9] h-2 rounded-full relative shadow-inner">
|
|
{/* Glowing progress fill line */}
|
|
<div
|
|
className="absolute top-0 left-0 h-full bg-gradient-to-r from-purple-600 via-indigo-500 to-pink-500 rounded-full transition-all duration-700 shadow-[0_0_8px_rgba(99,102,241,0.5)]"
|
|
style={{ width: `${(activeSlotIndex / 4) * 100}%` }}
|
|
/>
|
|
|
|
{/* Operational Nodes */}
|
|
<div className="absolute inset-0 flex justify-between items-center overflow-visible">
|
|
{intervalSlots.map((slot, index) => {
|
|
const isActive = activeSlotIndex === index;
|
|
const isHovered = hoveredChartIndex === index;
|
|
|
|
// Status colors
|
|
let dotColor = 'bg-indigo-500';
|
|
let ringColor = 'border-indigo-100 hover:border-indigo-300';
|
|
let rippleColor = 'bg-indigo-400';
|
|
|
|
if (slot.status === 'PEAK') {
|
|
dotColor = 'bg-rose-500';
|
|
ringColor = isActive ? 'border-rose-300 bg-rose-50/50' : 'border-rose-100 hover:border-rose-300';
|
|
rippleColor = 'bg-rose-400';
|
|
} else if (slot.status === 'HIGH') {
|
|
dotColor = 'bg-amber-500';
|
|
ringColor = isActive ? 'border-amber-300 bg-amber-50/50' : 'border-amber-100 hover:border-amber-300';
|
|
rippleColor = 'bg-amber-400';
|
|
} else if (slot.status === 'LOW') {
|
|
dotColor = 'bg-emerald-500';
|
|
ringColor = isActive ? 'border-emerald-300 bg-emerald-50/50' : 'border-emerald-100 hover:border-emerald-300';
|
|
rippleColor = 'bg-emerald-400';
|
|
}
|
|
|
|
return (
|
|
<div
|
|
key={index}
|
|
onMouseEnter={() => setHoveredChartIndex(index)}
|
|
onMouseLeave={() => setHoveredChartIndex(null)}
|
|
onClick={() => setHoveredChartIndex(index)}
|
|
className="relative flex flex-col items-center cursor-pointer group z-10"
|
|
>
|
|
|
|
{/* Time label above node */}
|
|
<div className="absolute bottom-8 whitespace-nowrap text-center flex flex-col items-center">
|
|
<span className={`text-[10px] font-black tracking-tight transition-colors ${
|
|
isActive ? 'text-[#581c87]' : 'text-zinc-400 group-hover:text-zinc-600'
|
|
}`}>
|
|
{slot.time.split(' - ')[0]}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Node Circle */}
|
|
<div className={`w-8 h-8 rounded-full border-2 bg-white flex items-center justify-center transition-all duration-300 shadow-sm ${
|
|
isActive ? 'scale-125 border-purple-600 shadow-md' : 'border-zinc-200 hover:border-purple-300'
|
|
}`}>
|
|
|
|
{/* Pulse ripples if active */}
|
|
{isActive && (
|
|
<span className={`absolute inline-flex h-8 w-8 rounded-full ${rippleColor} opacity-25 animate-ping z-0`} />
|
|
)}
|
|
|
|
{/* Inner dot */}
|
|
<div className={`w-3.5 h-3.5 rounded-full transition-transform duration-300 ${dotColor} ${
|
|
isActive ? 'scale-110' : 'group-hover:scale-110'
|
|
}`} />
|
|
</div>
|
|
|
|
{/* Label and dispatches below node */}
|
|
<div className="absolute top-10 whitespace-nowrap text-center flex flex-col items-center">
|
|
<span className={`text-[10px] font-bold tracking-tight transition-colors ${
|
|
isActive ? 'text-[#0f172a]' : 'text-zinc-500 group-hover:text-zinc-700'
|
|
}`}>
|
|
{slot.label.split(' ')[0]}
|
|
</span>
|
|
<span className="text-[8px] font-bold text-zinc-400 uppercase tracking-wider mt-[1px]">
|
|
{slot.orders} orders
|
|
</span>
|
|
</div>
|
|
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{/* Interactive Glassmorphic Stats audit drawer below chart */}
|
|
<div className="mt-4 p-md bg-[#faf5ff] border border-[#f3e8ff] rounded-2xl flex flex-col sm:flex-row items-start sm:items-center justify-between gap-md transition-all duration-300 animate-in fade-in duration-200">
|
|
<div className="flex items-center gap-md">
|
|
<div className={`w-8 h-8 rounded-xl flex items-center justify-center shrink-0 shadow-sm border ${
|
|
activeSlot.status === 'PEAK' ? 'bg-rose-50 text-rose-600 border-rose-100' :
|
|
activeSlot.status === 'HIGH' ? 'bg-amber-50 text-amber-600 border-amber-100' :
|
|
activeSlot.status === 'NORMAL' ? 'bg-sky-50 text-sky-600 border-sky-100' :
|
|
'bg-emerald-50 text-emerald-600 border-emerald-100'
|
|
}`}>
|
|
<Clock size={16} />
|
|
</div>
|
|
<div>
|
|
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest block">Audit Interval</span>
|
|
<span className="font-extrabold text-sm text-[#0f172a]">{activeSlot.label}</span>
|
|
<span className="text-[10px] text-zinc-500 font-semibold block mt-0.5">{activeSlot.time}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap items-center gap-md sm:gap-xl select-none text-xs">
|
|
<div className="text-right">
|
|
<span className="text-[9px] font-bold text-zinc-400 uppercase tracking-widest block">Dispatches</span>
|
|
<span className="font-black text-sm text-[#0f172a]">{activeSlot.orders} dispatches</span>
|
|
</div>
|
|
<div className="text-right border-l border-purple-100 pl-md sm:pl-xl">
|
|
<span className="text-[9px] font-bold text-purple-400 uppercase tracking-widest block">Est. Revenue</span>
|
|
<span className="font-black text-sm text-[#581c87]">{activeSlot.sales}</span>
|
|
</div>
|
|
<div className="text-right border-l border-purple-100 pl-md sm:pl-xl">
|
|
<span className="text-[9px] font-bold text-zinc-400 uppercase tracking-widest block">Load level</span>
|
|
<span className={`px-2 py-0.5 rounded-lg text-[9px] font-black tracking-wider inline-block mt-0.5 ${
|
|
activeSlot.status === 'PEAK' ? 'bg-rose-100 text-rose-700' :
|
|
activeSlot.status === 'HIGH' ? 'bg-amber-100 text-amber-700' :
|
|
activeSlot.status === 'NORMAL' ? 'bg-sky-100 text-sky-700' :
|
|
'bg-emerald-100 text-emerald-700'
|
|
}`}>
|
|
{activeSlot.status}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Actions Console */}
|
|
<div className="bg-white border border-[#eceef2] rounded-2xl p-md shadow-sm space-y-md flex flex-col justify-between">
|
|
<div>
|
|
<h3 className="font-sans font-bold text-sm text-[#0f172a] flex items-center gap-xs">
|
|
<ShieldCheck size={15} className="text-[#581c87]" /> Node Operations Command
|
|
</h3>
|
|
<p className="text-zinc-450 text-[10px] font-sans mt-0.5">Automated actions for local outlet hubs.</p>
|
|
</div>
|
|
|
|
<div className="space-y-sm">
|
|
<button
|
|
onClick={() => setActiveTab('inventory')}
|
|
className="w-full flex items-center justify-between p-sm border border-[#e2e8f0] rounded-xl hover:border-purple-300 hover:bg-purple-50/20 text-left text-xs font-semibold text-zinc-700 hover:text-[#581c87] transition cursor-pointer"
|
|
>
|
|
<span className="flex items-center gap-sm">
|
|
<Layers size={14} className="text-[#581c87]" /> Replenish Critical Stock
|
|
</span>
|
|
<span className="px-1.5 py-0.5 rounded text-[8px] bg-rose-100 text-rose-700 font-black animate-pulse">ALERTS</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleExportLedger}
|
|
className="w-full flex items-center justify-between p-sm border border-[#e2e8f0] rounded-xl hover:border-purple-300 hover:bg-purple-50/20 text-left text-xs font-semibold text-zinc-700 hover:text-[#581c87] transition cursor-pointer"
|
|
>
|
|
<span className="flex items-center gap-sm">
|
|
<Download size={14} className="text-zinc-500" /> Export Compliance Ledger
|
|
</span>
|
|
<span className="text-[8px] font-bold text-zinc-400">PDF / CSV</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={handleStaffBroadcast}
|
|
className="w-full flex items-center justify-between p-sm border border-[#e2e8f0] rounded-xl hover:border-purple-300 hover:bg-purple-50/20 text-left text-xs font-semibold text-zinc-700 hover:text-[#581c87] transition cursor-pointer"
|
|
>
|
|
<span className="flex items-center gap-sm">
|
|
<Send size={14} className="text-zinc-500" /> Broadcast Terminal SMS
|
|
</span>
|
|
<span className="text-[8px] font-bold text-[#581c87] uppercase tracking-wider">SMS Blast</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-gutter">
|
|
|
|
{/* Past 7 days Table */}
|
|
<div className="lg:col-span-2 bg-white border border-[#eceef2] rounded-2xl p-md shadow-sm">
|
|
<h3 className="font-sans font-bold text-sm text-[#0f172a] mb-md flex items-center gap-xs">
|
|
<Calendar size={15} className="text-[#581c87]" /> Past 7 Days Ledger Log
|
|
</h3>
|
|
|
|
<div className="overflow-x-auto text-xs font-sans">
|
|
<table className="w-full text-left">
|
|
<thead className="bg-[#f8fafc] border-b border-[#e2e8f0] text-zinc-400 text-[10px] uppercase font-bold tracking-wider">
|
|
<tr>
|
|
<th className="px-md py-sm">Day Period</th>
|
|
<th className="px-md py-sm">Dispatches</th>
|
|
<th className="px-md py-sm">Revenue Volume</th>
|
|
<th className="px-md py-sm text-right">OTIF fulfillment</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-[#f1f5f9] font-medium text-zinc-700">
|
|
{pastDaysLog.map((dayLog, index) => (
|
|
<tr key={index} className="hover:bg-zinc-50/50 transition">
|
|
<td className="px-md py-md font-bold text-[#0f172a]">{dayLog.day}</td>
|
|
<td className="px-md py-md text-zinc-650">{dayLog.orders} dispatches</td>
|
|
<td className="px-md py-md text-[#581c87] font-black">{dayLog.sales}</td>
|
|
<td className="px-md py-md text-right font-mono text-emerald-600 font-bold">{dayLog.rate}</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Live Rider fleet list */}
|
|
<div className="bg-white border border-[#eceef2] rounded-2xl p-md shadow-sm flex flex-col justify-between">
|
|
<div>
|
|
<h3 className="font-sans font-bold text-sm text-[#0f172a] flex items-center gap-xs">
|
|
<ShoppingCart size={15} className="text-[#581c87]" /> Active Rider Fleet
|
|
</h3>
|
|
<p className="text-zinc-405 text-[10px] font-sans mt-0.5">Live status and battery tracking of assigned riders.</p>
|
|
</div>
|
|
|
|
<div className="divide-y divide-[#f1f5f9] text-xs font-sans mt-md">
|
|
{activeRiders.map((rider, index) => (
|
|
<div key={index} className="py-2 flex justify-between items-center group">
|
|
<div className="flex items-center gap-sm min-w-0">
|
|
<div className="w-7 h-7 rounded-full bg-purple-50 text-[#581c87] border border-purple-100 font-black text-[10px] flex items-center justify-center shrink-0">
|
|
{rider.initial}
|
|
</div>
|
|
<div className="min-w-0">
|
|
<p className="font-bold text-[#0f172a] truncate">{rider.name.split(' ')[0]} {rider.name.split(' ').slice(-1)[0]}</p>
|
|
<p className="text-[9px] text-zinc-400 flex items-center gap-1 font-semibold uppercase">
|
|
<span className={`w-1.5 h-1.5 rounded-full ${rider.status === 'Idle' ? 'bg-amber-400' : 'bg-emerald-500'}`} />
|
|
{rider.status} · {rider.orders} orders
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-sm text-right shrink-0">
|
|
<div className="flex items-center gap-[2px] font-mono text-[10px] text-zinc-400">
|
|
<Battery size={13} className={rider.battery < 20 ? 'text-rose-500' : 'text-zinc-400'} />
|
|
<span className={rider.battery < 20 ? 'text-rose-600 font-bold' : ''}>{rider.battery}%</span>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => showToast(`SMS alert broadcasted to rider ${rider.name.split(' ')[0]}.`, 'success')}
|
|
className="px-2 py-1 border border-zinc-200 rounded-lg hover:border-purple-300 text-[9px] font-bold hover:bg-purple-50/50 hover:text-[#581c87] cursor-pointer transition"
|
|
>
|
|
Ping
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'inventory' && (
|
|
<div className="space-y-lg animate-in fade-in duration-300">
|
|
|
|
{/* Inventory search, metrics & catalogue tools */}
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-md bg-white border border-[#eceef2] p-md rounded-2xl shadow-sm">
|
|
<div className="relative w-full max-w-sm">
|
|
<Search size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-450" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search local stocks catalogue..."
|
|
value={stockSearch}
|
|
onChange={(e) => setStockSearch(e.target.value)}
|
|
className="w-full pl-9 pr-4 py-2.5 border border-[#e2e8f0] rounded-xl text-xs outline-none bg-[#f8fafc] focus:bg-white focus:ring-1 focus:ring-[#581c87] transition-all"
|
|
/>
|
|
</div>
|
|
|
|
{/* Actions for Store Catalogue Management */}
|
|
<div className="flex flex-wrap items-center gap-sm text-xs shrink-0 select-none">
|
|
<button
|
|
onClick={() => { setImportState('idle'); setShowImportModal(true); }}
|
|
className="px-3 py-2 bg-purple-50 text-[#581c87] hover:bg-purple-100/80 border border-purple-100 rounded-xl font-bold flex items-center gap-xs cursor-pointer transition shadow-sm"
|
|
>
|
|
<UploadCloud size={14} />
|
|
<span>Import Manual (CSV)</span>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => { setSelectedGlobalSkus([]); setShowGlobalModal(true); }}
|
|
className="px-3 py-2 bg-[#0f172a] text-white hover:bg-zinc-800 rounded-xl font-bold flex items-center gap-xs cursor-pointer transition shadow-sm"
|
|
>
|
|
<Globe size={14} />
|
|
<span>Global Catalogue Master</span>
|
|
</button>
|
|
|
|
<span className="h-6 w-[1px] bg-zinc-200 mx-xs hidden md:block" />
|
|
|
|
<span className="px-3 py-2 bg-rose-50 border border-rose-100 rounded-lg text-rose-700 font-bold">
|
|
{inventoryList.filter(item => item.status === 'Critical').length} Critical
|
|
</span>
|
|
<span className="px-3 py-2 bg-amber-50 border border-amber-100 rounded-lg text-amber-700 font-bold">
|
|
{inventoryList.filter(item => item.status === 'Low Stock').length} Low
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stocks statement Table */}
|
|
<div className="bg-white border border-[#eceef2] rounded-2xl overflow-hidden shadow-sm">
|
|
<div className="p-md border-b border-[#eceef2] bg-[#f8fafc] flex justify-between items-center">
|
|
<h4 className="font-sans font-bold text-sm text-[#0f172a]">
|
|
Product Stock Levels & Catalog
|
|
</h4>
|
|
<span className="text-[10px] font-bold text-[#581c87] bg-purple-50 px-2 py-0.5 rounded border border-purple-100 uppercase tracking-wide">Live list</span>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto text-xs font-sans">
|
|
<table className="w-full text-left">
|
|
<thead className="bg-[#f8fafc] border-b border-[#e2e8f0] text-zinc-450 text-[10px] uppercase font-bold tracking-wider">
|
|
<tr>
|
|
<th className="px-md py-sm">Product Item</th>
|
|
<th className="px-md py-sm">SKU Ref</th>
|
|
<th className="px-md py-sm">Category</th>
|
|
<th className="px-md py-sm">Est. Price</th>
|
|
<th className="px-md py-sm">Capacity Load</th>
|
|
<th className="px-md py-sm">Status</th>
|
|
<th className="px-md py-sm text-right">Replenish</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-[#f1f5f9] font-medium text-zinc-700">
|
|
{inventoryList.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={7} className="text-center py-10 text-zinc-400 font-medium">
|
|
No product stocks found matching the search keyword.
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
inventoryList.map((item, index) => {
|
|
const capacityPct = Math.round((item.stockLevel / item.maxCapacity) * 100);
|
|
|
|
// Pretty category badge mapping
|
|
const isStaples = item.category.toLowerCase().includes('staple') || item.category.toLowerCase().includes('rice');
|
|
const isBeverages = item.category.toLowerCase().includes('bev');
|
|
const isProduce = item.category.toLowerCase().includes('produce') || item.category.toLowerCase().includes('fresh') || item.category.toLowerCase().includes('carrot');
|
|
const isDairy = item.category.toLowerCase().includes('dairy');
|
|
|
|
return (
|
|
<tr key={index} className="hover:bg-[#f8fafc]/60 transition-colors">
|
|
<td className="px-md py-md">
|
|
<div className="flex items-center gap-sm">
|
|
<img
|
|
src={item.image}
|
|
alt={item.name}
|
|
className="w-10 h-10 object-cover rounded-lg border border-zinc-200 shadow-sm shrink-0"
|
|
/>
|
|
<span className="font-bold text-[#0f172a]">{item.name}</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-md py-md font-mono text-[10px] text-zinc-400 font-bold uppercase">{item.sku}</td>
|
|
<td className="px-md py-md">
|
|
<span className={`px-2 py-0.5 rounded text-[8px] font-bold uppercase tracking-wider ${
|
|
isStaples ? 'bg-amber-50 text-amber-700 border border-amber-100' :
|
|
isBeverages ? 'bg-rose-50 text-rose-700 border border-rose-100' :
|
|
isProduce ? 'bg-emerald-50 text-emerald-700 border border-emerald-100' :
|
|
isDairy ? 'bg-sky-50 text-sky-700 border border-sky-100' :
|
|
'bg-zinc-150 text-zinc-650'
|
|
}`}>
|
|
{item.category.split(' / ').slice(-1)[0]}
|
|
</span>
|
|
</td>
|
|
<td className="px-md py-md font-bold text-zinc-800">
|
|
₹{item.price.toLocaleString('en-IN')}
|
|
</td>
|
|
<td className="px-md py-md w-44">
|
|
<div className="flex items-center gap-sm">
|
|
<span className="w-8 shrink-0 font-mono text-right font-bold text-[10px]">{item.stockLevel} / {item.maxCapacity}</span>
|
|
<div className="w-20 bg-[#f1f5f9] h-2 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full ${
|
|
item.status === 'Critical' ? 'bg-rose-500 animate-pulse' :
|
|
item.status === 'Low Stock' ? 'bg-amber-500' :
|
|
'bg-emerald-500'
|
|
}`}
|
|
style={{ width: `${Math.min(capacityPct, 100)}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-md py-md">
|
|
<span className={`px-2 py-0.5 rounded text-[9px] font-bold uppercase tracking-wider ${
|
|
item.status === 'Optimal' ? 'bg-emerald-50 text-emerald-700 border border-emerald-100' :
|
|
item.status === 'Low Stock' ? 'bg-amber-50 text-amber-700 border border-amber-100' :
|
|
'bg-rose-50 text-rose-700 border border-rose-100'
|
|
}`}>
|
|
● {item.status}
|
|
</span>
|
|
</td>
|
|
<td className="px-md py-md text-right">
|
|
<button
|
|
onClick={() => setReplenishModal({ show: true, item })}
|
|
className={`px-3 py-1 rounded-lg text-[10px] font-bold hover:shadow-sm transition cursor-pointer ${
|
|
item.status === 'Critical'
|
|
? 'bg-rose-500 text-white hover:bg-rose-600'
|
|
: 'border border-zinc-200 text-zinc-700 hover:border-[#581c87] hover:text-[#581c87]'
|
|
}`}
|
|
>
|
|
Replenish
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'customers' && (
|
|
<div className="space-y-lg animate-in fade-in duration-300">
|
|
|
|
{/* Customer directory search and metrics */}
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-md bg-white border border-[#eceef2] p-md rounded-2xl shadow-sm">
|
|
<div className="relative w-full max-w-sm">
|
|
<Search size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" />
|
|
<input
|
|
type="text"
|
|
placeholder="Search CRM profile roster..."
|
|
value={customerSearch}
|
|
onChange={(e) => setCustomerSearch(e.target.value)}
|
|
className="w-full pl-9 pr-4 py-2.5 border border-[#e2e8f0] rounded-xl text-xs outline-none bg-[#f8fafc] focus:bg-white focus:ring-1 focus:ring-[#581c87] transition-all"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-sm text-xs shrink-0 select-none font-bold">
|
|
<span className="px-3 py-1.5 bg-purple-50 text-[#581c87] border border-purple-100 rounded-lg">
|
|
Retention Rate: 88.4%
|
|
</span>
|
|
<span className="px-3 py-1.5 bg-sky-50 text-sky-700 border border-sky-100 rounded-lg">
|
|
AOV: ₹1,580
|
|
</span>
|
|
<span className="px-3 py-1.5 bg-emerald-50 text-emerald-700 border border-emerald-100 rounded-lg">
|
|
CSAT Index: 4.9/5
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Customer list directory */}
|
|
<div className="bg-white border border-[#eceef2] rounded-2xl overflow-hidden shadow-sm">
|
|
<div className="p-md border-b border-[#eceef2] bg-[#f8fafc] flex justify-between items-center">
|
|
<h4 className="font-sans font-bold text-sm text-[#0f172a]">
|
|
Active Customer Directory
|
|
</h4>
|
|
<span className="text-[10px] font-bold text-[#581c87] bg-purple-50 px-2 py-0.5 rounded border border-purple-100 uppercase tracking-wide">Customer registry</span>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto text-xs font-sans">
|
|
<table className="w-full text-left">
|
|
<thead className="bg-[#f8fafc] border-b border-[#e2e8f0] text-zinc-450 text-[10px] uppercase font-bold tracking-wider">
|
|
<tr>
|
|
<th className="px-md py-sm">Customer Profile</th>
|
|
<th className="px-md py-sm">Contact Details</th>
|
|
<th className="px-md py-sm">Delivery Address</th>
|
|
<th className="px-md py-sm">Total Dispatches</th>
|
|
<th className="px-md py-sm">Gross Volume Spent</th>
|
|
<th className="px-md py-sm text-right">Audit CRM Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-[#f1f5f9] font-medium text-zinc-700">
|
|
{customersList.length === 0 ? (
|
|
<tr>
|
|
<td colSpan={6} className="text-center py-10 text-zinc-400 font-medium">
|
|
No customer accounts found matching search keyword.
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
customersList.map((c, idx) => {
|
|
const initials = c.name.split(' ').map((n: string) => n[0]).join('');
|
|
const gradients = [
|
|
'from-purple-500 to-indigo-500 text-white',
|
|
'from-rose-500 to-pink-500 text-white',
|
|
'from-sky-500 to-indigo-500 text-white',
|
|
'from-emerald-500 to-teal-500 text-white',
|
|
'from-amber-500 to-orange-500 text-white'
|
|
];
|
|
const avatarGrad = gradients[idx % gradients.length];
|
|
|
|
return (
|
|
<tr key={idx} className="hover:bg-[#f8fafc]/60 transition-colors">
|
|
<td className="px-md py-md">
|
|
<div className="flex items-center gap-xs">
|
|
<div className={`w-8 h-8 rounded-full bg-gradient-to-br ${avatarGrad} flex items-center justify-center font-black text-[10px] shadow-sm shrink-0`}>
|
|
{initials}
|
|
</div>
|
|
<span className="font-bold text-[#0f172a]">{c.name}</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-md py-md font-mono text-zinc-500 font-semibold">{c.phone}</td>
|
|
<td className="px-md py-md max-w-xs truncate text-zinc-500 font-medium" title={c.address}>
|
|
{c.address}
|
|
</td>
|
|
<td className="px-md py-md text-zinc-700 font-bold">{c.ordersCount} orders</td>
|
|
<td className="px-md py-md text-[#581c87] font-black">{c.totalSpent}</td>
|
|
<td className="px-md py-md text-right space-x-sm shrink-0">
|
|
<button
|
|
onClick={() => showToast(`Voucher promo code successfully dispatched to ${c.phone}.`, 'success')}
|
|
className="px-2.5 py-1 border border-zinc-200 hover:border-purple-300 rounded-lg font-bold text-[10px] text-zinc-650 hover:bg-purple-50/50 hover:text-[#581c87] cursor-pointer transition"
|
|
>
|
|
Promo SMS
|
|
</button>
|
|
<button
|
|
onClick={() => setSelectedCustomer(c)}
|
|
className="px-2.5 py-1 bg-[#0f172a] hover:bg-zinc-800 text-white rounded-lg font-bold text-[10px] cursor-pointer transition shadow-sm"
|
|
>
|
|
View Profile
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'orders' && (
|
|
<OrdersDeliveriesView
|
|
searchQuery=""
|
|
isCoimbatoreView={false}
|
|
locationid={locationid}
|
|
/>
|
|
)}
|
|
|
|
{/* ── Replenishment Modal Dialog Overlay ── */}
|
|
{replenishModal.show && replenishModal.item && (
|
|
<div
|
|
className="fixed inset-0 bg-[#0f172a]/40 backdrop-blur-sm z-[200] flex items-center justify-center p-md animate-in fade-in duration-200"
|
|
onClick={(e) => { if (e.target === e.currentTarget) setReplenishModal({ show: false, item: null }); }}
|
|
>
|
|
<div className="bg-white border border-[#e2e8f0] rounded-2xl w-full max-w-[24rem] max-h-[90vh] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200 text-xs font-sans">
|
|
<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">
|
|
<ShoppingCart size={15} className="text-[#581c87]" />
|
|
Replenish Inventory Stack
|
|
</h4>
|
|
<button
|
|
onClick={() => setReplenishModal({ show: false, item: null })}
|
|
className="p-1 hover:bg-zinc-200 rounded-full text-zinc-400 cursor-pointer transition-colors"
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleReplenishSubmit} className="flex-1 flex flex-col min-h-0">
|
|
<div className="p-md space-y-md overflow-y-auto flex-1">
|
|
<div className="p-sm bg-purple-50/50 border border-purple-100 rounded-xl space-y-xs">
|
|
<span className="text-[9px] font-black text-purple-600 uppercase tracking-wider">Replenishing Item</span>
|
|
<p className="font-bold text-sm text-[#0f172a]">{replenishModal.item.name}</p>
|
|
<p className="text-[10px] text-zinc-400 font-bold uppercase tracking-wider">SKU: {replenishModal.item.sku}</p>
|
|
</div>
|
|
|
|
<div className="space-y-sm">
|
|
<div className="space-y-1">
|
|
<label className="font-bold text-zinc-400 uppercase tracking-widest text-[9px]">Destination Store</label>
|
|
<input
|
|
type="text"
|
|
value={store.name}
|
|
className="w-full border border-[#e2e8f0] rounded-xl p-sm bg-[#f8fafc] text-zinc-500 font-semibold outline-none border-dashed"
|
|
disabled
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="font-bold text-zinc-500 uppercase tracking-widest text-[9px]">Replenish Quantity (Units)</label>
|
|
<input
|
|
type="number"
|
|
value={replenishQty}
|
|
onChange={(e) => setReplenishQty(Number(e.target.value))}
|
|
className="w-full border border-[#e2e8f0] rounded-xl p-sm bg-[#f8fafc] focus:bg-white outline-none focus:ring-1 focus:ring-[#581c87] font-bold"
|
|
min={1}
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-md border-t border-[#f1f5f9] flex justify-end gap-sm bg-[#f8fafc] shrink-0">
|
|
<button
|
|
type="button"
|
|
onClick={() => setReplenishModal({ show: false, item: null })}
|
|
className="px-4 py-2 border border-[#e2e8f0] rounded-xl 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-xl font-bold hover:bg-purple-800 cursor-pointer shadow-sm"
|
|
>
|
|
Confirm Batch Dispatch
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ── Manual CSV Import simulated Modal ── */}
|
|
{showImportModal && (
|
|
<div
|
|
className="fixed inset-0 bg-[#0f172a]/40 backdrop-blur-sm z-[200] flex items-center justify-center p-md animate-in fade-in duration-200"
|
|
onClick={(e) => { if (e.target === e.currentTarget && importState !== 'reading' && importState !== 'parsing' && importState !== 'saving') setShowImportModal(false); }}
|
|
>
|
|
<div className="bg-white border border-[#e2e8f0] rounded-2xl w-full max-w-[26rem] max-h-[90vh] flex flex-col shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200 text-xs font-sans">
|
|
<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">
|
|
<UploadCloud size={15} className="text-[#581c87]" />
|
|
Import Catalogue Manifest
|
|
</h4>
|
|
<button
|
|
onClick={() => setShowImportModal(false)}
|
|
disabled={['reading', 'parsing', 'saving'].includes(importState)}
|
|
className="p-1 hover:bg-zinc-200 rounded-full text-zinc-400 cursor-pointer transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-md space-y-md overflow-y-auto flex-1">
|
|
<p className="text-zinc-500 leading-relaxed font-medium">
|
|
Upload or drop your CSV stock ledger files below to commission new items into the <strong>{store.name}</strong> local registry database.
|
|
</p>
|
|
|
|
{importState === 'idle' && (
|
|
<div
|
|
onClick={handleStartCsvImport}
|
|
className="border-2 border-dashed border-[#e2e8f0] hover:border-purple-300 rounded-2xl p-xl text-center cursor-pointer transition-colors bg-[#f8fafc] hover:bg-purple-50/10 space-y-sm group"
|
|
>
|
|
<UploadCloud size={28} className="mx-auto text-zinc-400 group-hover:text-[#581c87] transition-colors" />
|
|
<div>
|
|
<p className="font-bold text-[#0f172a]">Click here to import file</p>
|
|
<p className="text-[10px] text-zinc-400 font-semibold uppercase tracking-wider mt-0.5">Mock file: coimbatore_manifest_v2.csv</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{['reading', 'parsing', 'saving'].includes(importState) && (
|
|
<div className="border border-[#e2e8f0] rounded-xl p-md bg-[#f8fafc] space-y-md text-center">
|
|
<div className="relative w-8 h-8 mx-auto">
|
|
<span className="absolute inset-0 rounded-full border-2 border-purple-100" />
|
|
<span className="absolute inset-0 rounded-full border-2 border-t-purple-600 animate-spin" />
|
|
</div>
|
|
<div>
|
|
<p className="font-bold text-[#0f172a] uppercase tracking-wide">
|
|
{importState === 'reading' && 'Reading uploaded CSV sheets...'}
|
|
{importState === 'parsing' && 'Scanning item SKU catalog mapping...'}
|
|
{importState === 'saving' && 'Syncing manifest entries with local inventory...'}
|
|
</p>
|
|
<p className="text-[10px] text-zinc-400 font-semibold mt-1">Kindly keep this window open while processing dispatches.</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{importState === 'done' && (
|
|
<div className="border border-emerald-100 rounded-xl p-md bg-emerald-50/50 space-y-md text-center animate-in zoom-in-95">
|
|
<CheckCircle2 size={32} className="mx-auto text-emerald-600" />
|
|
<div>
|
|
<p className="font-black text-emerald-800 uppercase tracking-wide text-xs">Ledger Imported Successfully</p>
|
|
<p className="text-[10px] text-emerald-700 font-bold mt-1">3 new SKU codes commissioned successfully into local registry.</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="p-md border-t border-[#f1f5f9] flex justify-end bg-[#f8fafc] shrink-0">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowImportModal(false)}
|
|
disabled={['reading', 'parsing', 'saving'].includes(importState)}
|
|
className="px-4 py-2 border border-[#e2e8f0] rounded-xl font-bold text-zinc-650 hover:bg-zinc-50 cursor-pointer disabled:opacity-50"
|
|
>
|
|
{importState === 'done' ? 'Close Window' : 'Cancel'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ── Choose from Global Catalogue Modal ── */}
|
|
{showGlobalModal && (
|
|
<div
|
|
className="fixed inset-0 bg-[#0f172a]/40 backdrop-blur-sm z-[200] flex items-center justify-center p-md animate-in fade-in duration-200"
|
|
onClick={(e) => { if (e.target === e.currentTarget) setShowGlobalModal(false); }}
|
|
>
|
|
<div className="bg-white border border-[#e2e8f0] rounded-2xl 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">
|
|
<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">
|
|
<Globe size={15} className="text-[#581c87]" />
|
|
Select Products from Master Catalogue
|
|
</h4>
|
|
<button
|
|
onClick={() => setShowGlobalModal(false)}
|
|
className="p-1 hover:bg-zinc-200 rounded-full text-zinc-400 cursor-pointer transition-colors"
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="p-md space-y-md overflow-y-auto flex-1">
|
|
<p className="text-zinc-500 leading-relaxed font-medium">
|
|
Choose master items from the national database to stock and commission locally at <strong>{store.name}</strong>.
|
|
</p>
|
|
|
|
<div className="space-y-sm divide-y divide-[#f1f5f9]">
|
|
{GLOBAL_CATALOGUE_ITEMS.map((item) => {
|
|
const isChecked = selectedGlobalSkus.includes(item.sku);
|
|
return (
|
|
<div
|
|
key={item.sku}
|
|
onClick={() => {
|
|
setSelectedGlobalSkus(prev =>
|
|
isChecked ? prev.filter(s => s !== item.sku) : [...prev, item.sku]
|
|
);
|
|
}}
|
|
className="py-2.5 flex items-center justify-between gap-sm cursor-pointer select-none hover:bg-zinc-50/50 rounded-lg px-1 transition-colors"
|
|
>
|
|
<div className="flex items-center gap-sm min-w-0">
|
|
<input
|
|
type="checkbox"
|
|
checked={isChecked}
|
|
onChange={() => {}} // handled by row click
|
|
className="w-4 h-4 rounded text-[#581c87] border-[#e2e8f0] focus:ring-purple-500"
|
|
/>
|
|
<img
|
|
src={item.image}
|
|
alt={item.name}
|
|
className="w-9 h-9 object-cover rounded-lg border border-zinc-200 shrink-0"
|
|
/>
|
|
<div className="min-w-0">
|
|
<p className="font-bold text-[#0f172a] truncate">{item.name}</p>
|
|
<p className="text-[9px] text-zinc-450 font-bold uppercase tracking-wider">{item.category} · SKU: {item.sku}</p>
|
|
</div>
|
|
</div>
|
|
<span className="font-bold text-zinc-800 shrink-0">₹{item.price}</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-md border-t border-[#f1f5f9] flex justify-end gap-sm bg-[#f8fafc] shrink-0">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowGlobalModal(false)}
|
|
className="px-4 py-2 border border-[#e2e8f0] rounded-xl font-bold text-zinc-500 hover:bg-zinc-50 cursor-pointer"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleAddGlobalCatalogue}
|
|
className="px-4 py-2 bg-[#581c87] text-white rounded-xl font-bold hover:bg-purple-800 cursor-pointer shadow-sm"
|
|
>
|
|
Add Selected to Store
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ── Customer CRM Profile Side Drawer Overlay ── */}
|
|
{selectedCustomer && (
|
|
<div
|
|
className="fixed inset-0 bg-[#0f172a]/40 backdrop-blur-sm z-[200] flex justify-end p-0 animate-in fade-in duration-200"
|
|
onClick={(e) => { if (e.target === e.currentTarget) setSelectedCustomer(null); }}
|
|
>
|
|
<div className="bg-white border-l border-[#e2e8f0] w-full max-w-[28rem] h-full flex flex-col shadow-2xl overflow-hidden animate-in slide-in-from-right duration-300 text-xs font-sans">
|
|
|
|
{/* Header info */}
|
|
<div className="p-lg border-b border-[#e2e8f0] bg-[#f8fafc] flex justify-between items-start shrink-0">
|
|
<div className="flex items-center gap-md">
|
|
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-purple-600 to-indigo-600 text-white flex items-center justify-center font-black text-sm shadow-md">
|
|
{selectedCustomer.name.split(' ').map((n: string) => n[0]).join('')}
|
|
</div>
|
|
<div>
|
|
<h4 className="font-sans font-black text-base text-[#0f172a]">{selectedCustomer.name}</h4>
|
|
<span className="inline-flex items-center gap-xs px-2 py-0.5 rounded text-[8px] bg-purple-100 text-purple-700 font-black tracking-wider uppercase mt-1">
|
|
<UserCheck size={9} /> High Value Account
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => setSelectedCustomer(null)}
|
|
className="p-1 hover:bg-zinc-200 rounded-full text-zinc-400 cursor-pointer transition-colors"
|
|
>
|
|
<X size={18} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Profile Drawer Details Container */}
|
|
<div className="p-lg overflow-y-auto flex-1 space-y-lg">
|
|
|
|
{/* Contact info list card */}
|
|
<div className="space-y-sm bg-white border border-[#eceef2] rounded-2xl p-md shadow-sm">
|
|
<span className="text-[9px] font-black text-purple-600 uppercase tracking-wider block">Profile Registry Details</span>
|
|
|
|
<div className="space-y-xs text-zinc-650">
|
|
<div className="flex items-center gap-sm py-1 border-b border-[#f1f5f9]">
|
|
<Phone size={13} className="text-zinc-400 shrink-0" />
|
|
<span className="font-mono font-semibold">{selectedCustomer.phone}</span>
|
|
</div>
|
|
<div className="flex items-center gap-sm py-1 border-b border-[#f1f5f9]">
|
|
<Mail size={13} className="text-zinc-400 shrink-0" />
|
|
<span>{selectedCustomer.name.toLowerCase().replace(/[^a-z]/g, '')}@gmail.com</span>
|
|
</div>
|
|
<div className="flex items-start gap-sm py-1">
|
|
<MapPin size={13} className="text-zinc-400 shrink-0 mt-0.5" />
|
|
<span>{selectedCustomer.address}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Metric grid info overlay */}
|
|
<div className="grid grid-cols-3 gap-sm select-none">
|
|
<div className="bg-[#faf5ff] border border-[#f3e8ff] p-sm rounded-xl text-center">
|
|
<span className="text-[8px] font-black text-purple-400 uppercase tracking-widest block">Dispatches</span>
|
|
<span className="font-black text-sm text-[#581c87] mt-xs block">{selectedCustomer.ordersCount} dispatches</span>
|
|
</div>
|
|
<div className="bg-sky-50/50 border border-sky-100 p-sm rounded-xl text-center">
|
|
<span className="text-[8px] font-black text-sky-500 uppercase tracking-widest block">Gross spend</span>
|
|
<span className="font-black text-sm text-sky-700 mt-xs block">{selectedCustomer.totalSpent}</span>
|
|
</div>
|
|
<div className="bg-emerald-50/50 border border-emerald-100 p-sm rounded-xl text-center">
|
|
<span className="text-[8px] font-black text-emerald-500 uppercase tracking-widest block">CSAT Score</span>
|
|
<span className="font-black text-sm text-emerald-700 mt-xs block">5.0 / 5.0</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Simulated Customer Order History ledger */}
|
|
<div className="space-y-sm">
|
|
<h5 className="font-sans font-bold text-xs text-[#0f172a] flex items-center gap-xs">
|
|
<History size={14} className="text-[#581c87]" /> Past Interactions & Orders
|
|
</h5>
|
|
|
|
<div className="divide-y divide-[#f1f5f9] select-none text-[11px] font-semibold text-zinc-650">
|
|
<div className="py-2.5 flex justify-between items-center">
|
|
<div className="space-y-0.5">
|
|
<span className="text-[#0f172a]">DM-ORD-2091</span>
|
|
<p className="text-[9px] text-zinc-400 font-medium">{selectedCustomer.lastOrder}</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<span className="font-mono text-[#581c87] font-bold">{selectedCustomer.totalSpent}</span>
|
|
<span className="px-1.5 py-0.2 bg-emerald-50 text-emerald-600 rounded text-[7px] font-black tracking-wider block mt-0.5 uppercase">Delivered</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="py-2.5 flex justify-between items-center opacity-70">
|
|
<div className="space-y-0.5">
|
|
<span className="text-[#0f172a]">DM-ORD-1982</span>
|
|
<p className="text-[9px] text-zinc-400 font-medium">1 Month ago</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<span className="font-mono text-zinc-700 font-bold">₹1,240.00</span>
|
|
<span className="px-1.5 py-0.2 bg-emerald-50 text-emerald-600 rounded text-[7px] font-black tracking-wider block mt-0.5 uppercase">Delivered</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="py-2.5 flex justify-between items-center opacity-60">
|
|
<div className="space-y-0.5">
|
|
<span className="text-[#0f172a]">DM-ORD-1721</span>
|
|
<p className="text-[9px] text-zinc-400 font-medium">2 Months ago</p>
|
|
</div>
|
|
<div className="text-right">
|
|
<span className="font-mono text-zinc-700 font-bold">₹2,840.00</span>
|
|
<span className="px-1.5 py-0.2 bg-emerald-50 text-emerald-600 rounded text-[7px] font-black tracking-wider block mt-0.5 uppercase">Delivered</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Quick Actions in Side Drawer */}
|
|
<div className="p-lg bg-[#f8fafc] border-t border-[#e2e8f0] grid grid-cols-2 gap-sm shrink-0">
|
|
<button
|
|
onClick={() => {
|
|
showToast(`Promo voucher code SMS broadcasted to customer.`, 'success');
|
|
setSelectedCustomer(null);
|
|
}}
|
|
className="w-full py-2.5 bg-[#581c87] hover:bg-purple-800 text-white rounded-xl font-bold text-center cursor-pointer transition shadow-sm"
|
|
>
|
|
Send Promo SMS
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => {
|
|
showToast(`Refund credit index check initiated.`, 'info');
|
|
setSelectedCustomer(null);
|
|
}}
|
|
className="w-full py-2.5 border border-[#e2e8f0] bg-white hover:bg-zinc-50 rounded-xl font-bold text-center text-zinc-700 cursor-pointer transition"
|
|
>
|
|
Issue Store Credit
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
);
|
|
}
|