diff --git a/servers/lightspeed/src/ui/react-app/discount-manager.tsx b/servers/lightspeed/src/ui/react-app/discount-manager.tsx new file mode 100644 index 0000000..786ab40 --- /dev/null +++ b/servers/lightspeed/src/ui/react-app/discount-manager.tsx @@ -0,0 +1,287 @@ +import React, { useState } from 'react'; + +interface Discount { + id: string; + name: string; + code: string; + type: 'percentage' | 'fixed' | 'bogo' | 'free-shipping'; + value: number; + minPurchase?: number; + maxDiscount?: number; + usageCount: number; + usageLimit?: number; + startDate: string; + endDate: string; + status: 'active' | 'scheduled' | 'expired' | 'disabled'; + applicableTo: 'all' | 'specific-products' | 'specific-categories'; +} + +export default function DiscountManager() { + const [discounts] = useState([ + { id: 'DSC-001', name: 'Welcome10', code: 'WELCOME10', type: 'percentage', value: 10, minPurchase: 50, usageCount: 124, usageLimit: 500, startDate: '2024-01-01', endDate: '2024-12-31', status: 'active', applicableTo: 'all' }, + { id: 'DSC-002', name: 'Spring Sale', code: 'SPRING25', type: 'percentage', value: 25, minPurchase: 100, maxDiscount: 50, usageCount: 87, startDate: '2024-03-01', endDate: '2024-05-31', status: 'scheduled', applicableTo: 'specific-categories' }, + { id: 'DSC-003', name: 'Free Shipping', code: 'FREESHIP', type: 'free-shipping', value: 0, minPurchase: 75, usageCount: 234, startDate: '2024-01-15', endDate: '2024-12-31', status: 'active', applicableTo: 'all' }, + { id: 'DSC-004', name: 'Buy One Get One', code: 'BOGO', type: 'bogo', value: 50, usageCount: 45, usageLimit: 100, startDate: '2024-02-01', endDate: '2024-02-14', status: 'expired', applicableTo: 'specific-products' }, + { id: 'DSC-005', name: '$20 Off Large Orders', code: 'SAVE20', type: 'fixed', value: 20, minPurchase: 150, usageCount: 56, startDate: '2024-02-01', endDate: '2024-03-31', status: 'active', applicableTo: 'all' }, + { id: 'DSC-006', name: 'VIP Member', code: 'VIP15', type: 'percentage', value: 15, usageCount: 0, startDate: '2024-04-01', endDate: '2024-12-31', status: 'disabled', applicableTo: 'all' }, + ]); + + const [showNewDiscount, setShowNewDiscount] = useState(false); + const [filterStatus, setFilterStatus] = useState('all'); + + const filteredDiscounts = discounts.filter(d => + filterStatus === 'all' || d.status === filterStatus + ); + + const getStatusColor = (status: string) => { + switch (status) { + case 'active': return 'bg-green-500/20 text-green-400'; + case 'scheduled': return 'bg-blue-500/20 text-blue-400'; + case 'expired': return 'bg-red-500/20 text-red-400'; + case 'disabled': return 'bg-slate-600/50 text-slate-400'; + default: return 'bg-slate-600/50 text-slate-400'; + } + }; + + const getTypeIcon = (type: string) => { + switch (type) { + case 'percentage': return '%'; + case 'fixed': return '$'; + case 'bogo': return '2ร—1'; + case 'free-shipping': return '๐Ÿšš'; + default: return '๐ŸŽซ'; + } + }; + + const activeDiscounts = discounts.filter(d => d.status === 'active').length; + const totalUsage = discounts.reduce((sum, d) => sum + d.usageCount, 0); + + return ( +
+
+
+

Discount Manager

+ +
+ + {/* Stats */} +
+ + + + d.status === 'scheduled').length} icon="๐Ÿ“…" /> +
+ + {/* Filter */} +
+
+ +
+ {['all', 'active', 'scheduled', 'expired', 'disabled'].map(status => ( + + ))} +
+
+
+ + {/* Discounts Grid */} +
+ {filteredDiscounts.map((discount) => ( +
+ {/* Header */} +
+
+

{discount.name}

+
+ {discount.code} + {discount.id} +
+
+ + {discount.status.toUpperCase()} + +
+ + {/* Discount Details */} +
+
+
+ {getTypeIcon(discount.type)} +
+
+ {discount.type === 'percentage' ? `${discount.value}%` : + discount.type === 'fixed' ? `$${discount.value}` : + discount.type === 'bogo' ? `${discount.value}%` : 'Free'} +
+
{discount.type.replace('-', ' ')}
+
+ +
+ {discount.minPurchase && ( +
+
Min Purchase
+
${discount.minPurchase}
+
+ )} + {discount.maxDiscount && ( +
+
Max Discount
+
${discount.maxDiscount}
+
+ )} +
+
+ + {/* Usage Stats */} +
+
+ Usage + + {discount.usageCount} {discount.usageLimit ? `/ ${discount.usageLimit}` : ''} + +
+ {discount.usageLimit && ( +
+
+
+ )} +
+ + {/* Date Range */} +
+
+ ๐Ÿ“… {new Date(discount.startDate).toLocaleDateString()} + โ†’ + {new Date(discount.endDate).toLocaleDateString()} +
+
+ + {/* Applicable To */} +
+ + {discount.applicableTo.replace('-', ' ').toUpperCase()} + +
+ + {/* Actions */} +
+ + + +
+
+ ))} +
+ + {/* New Discount Modal */} + {showNewDiscount && ( +
+
+

Create New Discount

+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+
+
+ )} +
+
+ ); +} + +function StatCard({ title, value, icon }: { title: string; value: string | number; icon: string }) { + return ( +
+
+ {title} + {icon} +
+
{value}
+
+ ); +} diff --git a/servers/lightspeed/src/ui/react-app/product-performance.tsx b/servers/lightspeed/src/ui/react-app/product-performance.tsx new file mode 100644 index 0000000..0f2f14d --- /dev/null +++ b/servers/lightspeed/src/ui/react-app/product-performance.tsx @@ -0,0 +1,222 @@ +import React, { useState } from 'react'; + +interface ProductPerformance { + id: string; + name: string; + sku: string; + category: string; + unitsSold: number; + revenue: number; + profit: number; + averagePrice: number; + stockTurnover: number; + trend: 'up' | 'down' | 'stable'; + trendPercentage: number; +} + +export default function ProductPerformance() { + const [products] = useState([ + { id: '1', name: 'Premium Coffee Beans', sku: 'COF001', category: 'Coffee', unitsSold: 487, revenue: 12168.13, profit: 6084.07, averagePrice: 24.99, stockTurnover: 9.7, trend: 'up', trendPercentage: 15.3 }, + { id: '2', name: 'Espresso Machine', sku: 'MAC001', category: 'Equipment', unitsSold: 12, revenue: 10799.88, profit: 4319.95, averagePrice: 899.99, stockTurnover: 4.0, trend: 'up', trendPercentage: 8.5 }, + { id: '3', name: 'Ceramic Mug', sku: 'MUG001', category: 'Accessories', unitsSold: 234, revenue: 3039.66, profit: 1519.83, averagePrice: 12.99, stockTurnover: 9.4, trend: 'stable', trendPercentage: 1.2 }, + { id: '4', name: 'Tea Assortment', sku: 'TEA001', category: 'Tea', unitsSold: 156, revenue: 3118.44, profit: 1559.22, averagePrice: 19.99, stockTurnover: 13.0, trend: 'up', trendPercentage: 22.7 }, + { id: '5', name: 'Milk Frother', sku: 'ACC001', category: 'Accessories', unitsSold: 89, revenue: 4449.11, profit: 2224.56, averagePrice: 49.99, stockTurnover: 7.4, trend: 'down', trendPercentage: -5.8 }, + { id: '6', name: 'Cold Brew Maker', sku: 'EQP002', category: 'Equipment', unitsSold: 45, revenue: 3599.55, profit: 1799.78, averagePrice: 79.99, stockTurnover: 3.0, trend: 'stable', trendPercentage: 0.5 }, + { id: '7', name: 'Green Tea', sku: 'TEA002', category: 'Tea', unitsSold: 178, revenue: 2668.22, profit: 1334.11, averagePrice: 14.99, stockTurnover: 8.1, trend: 'up', trendPercentage: 12.4 }, + { id: '8', name: 'Coffee Filters', sku: 'ACC002', category: 'Accessories', unitsSold: 567, revenue: 3963.33, profit: 1981.67, averagePrice: 6.99, stockTurnover: 5.7, trend: 'down', trendPercentage: -3.2 }, + ]); + + const [sortBy, setSortBy] = useState<'revenue' | 'units' | 'profit' | 'turnover'>('revenue'); + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); + const [filterCategory, setFilterCategory] = useState('all'); + + const sortedProducts = [...products] + .filter(p => filterCategory === 'all' || p.category === filterCategory) + .sort((a, b) => { + const multiplier = sortDirection === 'asc' ? 1 : -1; + switch (sortBy) { + case 'revenue': return (b.revenue - a.revenue) * multiplier; + case 'units': return (b.unitsSold - a.unitsSold) * multiplier; + case 'profit': return (b.profit - a.profit) * multiplier; + case 'turnover': return (b.stockTurnover - a.stockTurnover) * multiplier; + default: return 0; + } + }); + + const categories = ['all', ...Array.from(new Set(products.map(p => p.category)))]; + + const totalRevenue = products.reduce((sum, p) => sum + p.revenue, 0); + const totalProfit = products.reduce((sum, p) => sum + p.profit, 0); + const totalUnits = products.reduce((sum, p) => sum + p.unitsSold, 0); + const avgTurnover = products.reduce((sum, p) => sum + p.stockTurnover, 0) / products.length; + + const getTrendIcon = (trend: string) => { + switch (trend) { + case 'up': return '๐Ÿ“ˆ'; + case 'down': return '๐Ÿ“‰'; + case 'stable': return 'โžก๏ธ'; + default: return 'โžก๏ธ'; + } + }; + + const getTrendColor = (trend: string) => { + switch (trend) { + case 'up': return 'text-green-400'; + case 'down': return 'text-red-400'; + case 'stable': return 'text-slate-400'; + default: return 'text-slate-400'; + } + }; + + return ( +
+
+

Product Performance Analytics

+ + {/* Summary Stats */} +
+ + + + +
+ + {/* Controls */} +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* Performance Table */} +
+
+ + + + + + + + + + + + + + + {sortedProducts.map((product, index) => ( + + + + + + + + + + + ))} + +
ProductCategoryUnits SoldRevenueProfitAvg PriceTurnoverTrend
+
{product.name}
+
{product.sku}
+
{product.category}{product.unitsSold}${product.revenue.toFixed(2)}${product.profit.toFixed(2)}${product.averagePrice.toFixed(2)} + = 8 ? 'text-green-400' : + product.stockTurnover >= 5 ? 'text-yellow-400' : 'text-red-400' + }`}> + {product.stockTurnover.toFixed(1)}ร— + + +
+ {getTrendIcon(product.trend)} + + {product.trendPercentage > 0 ? '+' : ''}{product.trendPercentage.toFixed(1)}% + +
+
+
+
+ + {/* Insights */} +
+
+
+ โญ +

Top Performer

+
+

+ {sortedProducts[0]?.name} leads with ${sortedProducts[0]?.revenue.toFixed(2)} in revenue +

+
+
+
+ ๐Ÿ”„ +

Fast Mover

+
+

+ {[...products].sort((a, b) => b.stockTurnover - a.stockTurnover)[0]?.name} has the highest turnover rate +

+
+
+
+ ๐Ÿ’Ž +

Most Profitable

+
+

+ {[...products].sort((a, b) => b.profit - a.profit)[0]?.name} generates highest profit margin +

+
+
+
+
+ ); +} + +function StatCard({ title, value, icon }: { title: string; value: string | number; icon: string }) { + return ( +
+
+ {title} + {icon} +
+
{value}
+
+ ); +} diff --git a/servers/lightspeed/src/ui/react-app/purchase-orders.tsx b/servers/lightspeed/src/ui/react-app/purchase-orders.tsx new file mode 100644 index 0000000..15b953c --- /dev/null +++ b/servers/lightspeed/src/ui/react-app/purchase-orders.tsx @@ -0,0 +1,316 @@ +import React, { useState } from 'react'; + +interface PurchaseOrderItem { + productName: string; + sku: string; + quantity: number; + unitCost: number; + total: number; +} + +interface PurchaseOrder { + id: string; + orderNumber: string; + supplier: string; + orderDate: string; + expectedDelivery: string; + status: 'draft' | 'pending' | 'ordered' | 'received' | 'cancelled'; + items: PurchaseOrderItem[]; + subtotal: number; + tax: number; + shipping: number; + total: number; + notes?: string; +} + +export default function PurchaseOrders() { + const [orders] = useState([ + { + id: 'PO-001', + orderNumber: 'PO-2024-0213-001', + supplier: 'Colombian Coffee Co.', + orderDate: '2024-02-13', + expectedDelivery: '2024-02-20', + status: 'ordered', + items: [ + { productName: 'Premium Coffee Beans', sku: 'COF001', quantity: 100, unitCost: 12.50, total: 1250.00 }, + { productName: 'Organic Coffee Beans', sku: 'COF002', quantity: 50, unitCost: 15.00, total: 750.00 }, + ], + subtotal: 2000.00, + tax: 160.00, + shipping: 75.00, + total: 2235.00, + notes: 'Rush order for spring promotion' + }, + { + id: 'PO-002', + orderNumber: 'PO-2024-0212-001', + supplier: 'Equipment Suppliers Inc.', + orderDate: '2024-02-12', + expectedDelivery: '2024-02-19', + status: 'pending', + items: [ + { productName: 'Espresso Machine Pro', sku: 'MAC002', quantity: 5, unitCost: 650.00, total: 3250.00 }, + { productName: 'Grinder Commercial', sku: 'MAC003', quantity: 3, unitCost: 425.00, total: 1275.00 }, + ], + subtotal: 4525.00, + tax: 362.00, + shipping: 125.00, + total: 5012.00 + }, + { + id: 'PO-003', + orderNumber: 'PO-2024-0210-001', + supplier: 'Tea Imports Ltd.', + orderDate: '2024-02-10', + expectedDelivery: '2024-02-15', + status: 'received', + items: [ + { productName: 'Green Tea Premium', sku: 'TEA002', quantity: 200, unitCost: 7.50, total: 1500.00 }, + { productName: 'Earl Grey', sku: 'TEA003', quantity: 150, unitCost: 8.25, total: 1237.50 }, + ], + subtotal: 2737.50, + tax: 219.00, + shipping: 50.00, + total: 3006.50 + }, + { + id: 'PO-004', + orderNumber: 'PO-2024-0208-001', + supplier: 'Accessories Direct', + orderDate: '2024-02-08', + expectedDelivery: '2024-02-14', + status: 'draft', + items: [ + { productName: 'Ceramic Mugs', sku: 'MUG001', quantity: 300, unitCost: 5.50, total: 1650.00 }, + { productName: 'Coffee Filters', sku: 'ACC002', quantity: 500, unitCost: 2.99, total: 1495.00 }, + ], + subtotal: 3145.00, + tax: 251.60, + shipping: 85.00, + total: 3481.60 + }, + ]); + + const [showNewOrder, setShowNewOrder] = useState(false); + const [filterStatus, setFilterStatus] = useState('all'); + const [selectedOrder, setSelectedOrder] = useState(null); + + const filteredOrders = orders.filter(order => + filterStatus === 'all' || order.status === filterStatus + ); + + const getStatusColor = (status: string) => { + switch (status) { + case 'draft': return 'bg-slate-600/50 text-slate-400'; + case 'pending': return 'bg-yellow-500/20 text-yellow-400'; + case 'ordered': return 'bg-blue-500/20 text-blue-400'; + case 'received': return 'bg-green-500/20 text-green-400'; + case 'cancelled': return 'bg-red-500/20 text-red-400'; + default: return 'bg-slate-600/50 text-slate-400'; + } + }; + + const totalValue = orders.reduce((sum, o) => sum + o.total, 0); + const pendingValue = orders.filter(o => o.status === 'pending' || o.status === 'ordered').reduce((sum, o) => sum + o.total, 0); + + return ( +
+
+
+

Purchase Orders

+ +
+ + {/* Stats */} +
+ + o.status === 'pending' || o.status === 'ordered').length} icon="โณ" /> + + +
+ + {/* Filter */} +
+
+ +
+ {['all', 'draft', 'pending', 'ordered', 'received', 'cancelled'].map(status => ( + + ))} +
+
+
+ + {/* Orders List */} +
+ {filteredOrders.map((order) => ( +
+
+
+

{order.orderNumber}

+
+ ๐Ÿข {order.supplier} + ๐Ÿ“… Ordered: {new Date(order.orderDate).toLocaleDateString()} + ๐Ÿšš Expected: {new Date(order.expectedDelivery).toLocaleDateString()} +
+
+ + {order.status.toUpperCase()} + +
+ + {/* Items Summary */} +
+
{order.items.length} items
+
+ {order.items.map((item, idx) => ( +
+
+
{item.productName}
+
{item.sku} โ€ข Qty: {item.quantity}
+
+
+
${item.total.toFixed(2)}
+
${item.unitCost.toFixed(2)} each
+
+
+ ))} +
+
+ + {/* Totals */} +
+
+
Subtotal
+
${order.subtotal.toFixed(2)}
+
+
+
Tax
+
${order.tax.toFixed(2)}
+
+
+
Shipping
+
${order.shipping.toFixed(2)}
+
+
+
Total
+
${order.total.toFixed(2)}
+
+
+ + {order.notes && ( +
+ Notes: {order.notes} +
+ )} + + {/* Actions */} +
+ + {order.status === 'draft' && ( + + )} + {order.status === 'ordered' && ( + + )} + + {order.status === 'draft' && ( + + )} +
+
+ ))} +
+ + {/* New Order Modal */} + {showNewOrder && ( +
+
+

Create Purchase Order

+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+

Items

+ +
+
+
+ + + +
+
+
+ )} +
+
+ ); +} + +function StatCard({ title, value, icon }: { title: string; value: string | number; icon: string }) { + return ( +
+
+ {title} + {icon} +
+
{value}
+
+ ); +} diff --git a/servers/lightspeed/src/ui/react-app/sales-report.tsx b/servers/lightspeed/src/ui/react-app/sales-report.tsx new file mode 100644 index 0000000..ab5231e --- /dev/null +++ b/servers/lightspeed/src/ui/react-app/sales-report.tsx @@ -0,0 +1,211 @@ +import React, { useState } from 'react'; + +interface SalesData { + date: string; + sales: number; + transactions: number; + averageValue: number; +} + +export default function SalesReport() { + const [dateRange, setDateRange] = useState('week'); + const [reportType, setReportType] = useState('overview'); + + const [dailySales] = useState([ + { date: '2024-02-07', sales: 1847.25, transactions: 42, averageValue: 43.98 }, + { date: '2024-02-08', sales: 2156.80, transactions: 51, averageValue: 42.29 }, + { date: '2024-02-09', sales: 1923.45, transactions: 38, averageValue: 50.62 }, + { date: '2024-02-10', sales: 2487.90, transactions: 56, averageValue: 44.43 }, + { date: '2024-02-11', sales: 1678.50, transactions: 34, averageValue: 49.37 }, + { date: '2024-02-12', sales: 2934.60, transactions: 64, averageValue: 45.85 }, + { date: '2024-02-13', sales: 2247.35, transactions: 48, averageValue: 46.82 }, + ]); + + const [categoryBreakdown] = useState([ + { category: 'Coffee', sales: 5847.50, percentage: 38.5 }, + { category: 'Equipment', sales: 4234.80, percentage: 27.9 }, + { category: 'Accessories', sales: 2456.40, percentage: 16.2 }, + { category: 'Tea', sales: 1945.60, percentage: 12.8 }, + { category: 'Syrups', states: 692.55, percentage: 4.6 }, + ]); + + const [topProducts] = useState([ + { name: 'Premium Coffee Beans', sales: 2487.75, units: 124, revenue: 2487.75 }, + { name: 'Espresso Machine', sales: 1799.98, units: 2, revenue: 1799.98 }, + { name: 'Ceramic Mug Set', sales: 1247.88, units: 96, revenue: 1247.88 }, + { name: 'Cold Brew Maker', sales: 1119.86, units: 14, revenue: 1119.86 }, + { name: 'Tea Assortment', sales: 999.50, units: 50, revenue: 999.50 }, + ]); + + const totalSales = dailySales.reduce((sum, day) => sum + day.sales, 0); + const totalTransactions = dailySales.reduce((sum, day) => sum + day.transactions, 0); + const averageTransactionValue = totalSales / totalTransactions; + const averageDailySales = totalSales / dailySales.length; + + const maxSales = Math.max(...dailySales.map(d => d.sales)); + + return ( +
+
+

Sales Report

+ + {/* Controls */} +
+
+ + +
+
+ + +
+
+ + {/* Summary Stats */} +
+ + + + +
+ +
+ {/* Daily Sales Chart */} +
+

Daily Sales Trend

+
+ {dailySales.map((day) => ( +
+ {new Date(day.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} +
+
+ ${day.sales.toFixed(0)} +
+
+ {day.transactions} txn +
+ ))} +
+
+ + {/* Category Breakdown */} +
+

Sales by Category

+
+ {categoryBreakdown.map((cat) => ( +
+
+ {cat.category} +
+
${cat.sales.toFixed(2)}
+
{cat.percentage}%
+
+
+
+
+
+
+ ))} +
+
+
+ + {/* Top Products */} +
+

Top Performing Products

+
+ + + + + + + + + + + + {topProducts.map((product, index) => ( + + + + + + + + ))} + +
RankProductUnits SoldRevenueAvg Price
+ + {index + 1} + + {product.name}{product.units}${product.revenue.toFixed(2)}${(product.revenue / product.units).toFixed(2)}
+
+
+ + {/* Export Options */} +
+ + + +
+
+
+ ); +} + +function StatCard({ title, value, icon, trend }: { title: string; value: string | number; icon: string; trend?: string }) { + const isPositive = trend?.startsWith('+'); + return ( +
+
+ {title} + {icon} +
+
{value}
+ {trend && ( +
+ {trend} vs last period +
+ )} +
+ ); +}