lightspeed: Add 16 React apps (product, sales, customer, inventory, employee, etc.)
This commit is contained in:
parent
7e2721acaf
commit
cfcff6bb83
287
servers/lightspeed/src/ui/react-app/discount-manager.tsx
Normal file
287
servers/lightspeed/src/ui/react-app/discount-manager.tsx
Normal file
@ -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<Discount[]>([
|
||||||
|
{ 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 (
|
||||||
|
<div className="min-h-screen bg-slate-950 text-slate-100 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="flex items-center justify-between mb-8">
|
||||||
|
<h1 className="text-3xl font-bold">Discount Manager</h1>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNewDiscount(true)}
|
||||||
|
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg font-medium"
|
||||||
|
>
|
||||||
|
+ Create Discount
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||||
|
<StatCard title="Total Discounts" value={discounts.length} icon="🎫" />
|
||||||
|
<StatCard title="Active" value={activeDiscounts} icon="✅" />
|
||||||
|
<StatCard title="Total Usage" value={totalUsage} icon="📊" />
|
||||||
|
<StatCard title="Scheduled" value={discounts.filter(d => d.status === 'scheduled').length} icon="📅" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter */}
|
||||||
|
<div className="bg-slate-800 rounded-lg p-4 mb-6">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<label className="text-sm text-slate-400">Filter by Status:</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{['all', 'active', 'scheduled', 'expired', 'disabled'].map(status => (
|
||||||
|
<button
|
||||||
|
key={status}
|
||||||
|
onClick={() => setFilterStatus(status)}
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm font-medium capitalize ${
|
||||||
|
filterStatus === status ? 'bg-blue-600' : 'bg-slate-700 hover:bg-slate-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Discounts Grid */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{filteredDiscounts.map((discount) => (
|
||||||
|
<div key={discount.id} className="bg-slate-800 rounded-lg p-6 hover:ring-2 ring-blue-500 transition-all">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-xl">{discount.name}</h3>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<code className="px-2 py-1 bg-slate-700 rounded text-sm font-mono text-blue-400">{discount.code}</code>
|
||||||
|
<span className="text-xs text-slate-400">{discount.id}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className={`px-3 py-1 rounded text-xs font-medium ${getStatusColor(discount.status)}`}>
|
||||||
|
{discount.status.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Discount Details */}
|
||||||
|
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||||
|
<div className="p-4 bg-gradient-to-br from-blue-500/20 to-purple-500/20 rounded-lg text-center">
|
||||||
|
<div className="text-3xl font-bold text-blue-400">
|
||||||
|
{getTypeIcon(discount.type)}
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold mt-2">
|
||||||
|
{discount.type === 'percentage' ? `${discount.value}%` :
|
||||||
|
discount.type === 'fixed' ? `$${discount.value}` :
|
||||||
|
discount.type === 'bogo' ? `${discount.value}%` : 'Free'}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-slate-400 mt-1 capitalize">{discount.type.replace('-', ' ')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{discount.minPurchase && (
|
||||||
|
<div className="p-2 bg-slate-700 rounded">
|
||||||
|
<div className="text-xs text-slate-400">Min Purchase</div>
|
||||||
|
<div className="font-semibold">${discount.minPurchase}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{discount.maxDiscount && (
|
||||||
|
<div className="p-2 bg-slate-700 rounded">
|
||||||
|
<div className="text-xs text-slate-400">Max Discount</div>
|
||||||
|
<div className="font-semibold">${discount.maxDiscount}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Usage Stats */}
|
||||||
|
<div className="mb-4 p-3 bg-slate-700 rounded-lg">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-sm text-slate-400">Usage</span>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{discount.usageCount} {discount.usageLimit ? `/ ${discount.usageLimit}` : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{discount.usageLimit && (
|
||||||
|
<div className="w-full bg-slate-600 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="bg-green-500 rounded-full h-2"
|
||||||
|
style={{ width: `${Math.min((discount.usageCount / discount.usageLimit) * 100, 100)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Date Range */}
|
||||||
|
<div className="mb-4 text-sm">
|
||||||
|
<div className="flex items-center justify-between text-slate-400">
|
||||||
|
<span>📅 {new Date(discount.startDate).toLocaleDateString()}</span>
|
||||||
|
<span>→</span>
|
||||||
|
<span>{new Date(discount.endDate).toLocaleDateString()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Applicable To */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<span className="text-xs px-2 py-1 bg-purple-500/20 text-purple-400 rounded">
|
||||||
|
{discount.applicableTo.replace('-', ' ').toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex gap-2 pt-4 border-t border-slate-700">
|
||||||
|
<button className="flex-1 px-3 py-2 bg-blue-600 hover:bg-blue-700 rounded text-sm font-medium">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button className="flex-1 px-3 py-2 bg-slate-700 hover:bg-slate-600 rounded text-sm font-medium">
|
||||||
|
Duplicate
|
||||||
|
</button>
|
||||||
|
<button className="px-3 py-2 bg-red-600 hover:bg-red-700 rounded text-sm font-medium">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* New Discount Modal */}
|
||||||
|
{showNewDiscount && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-6 z-50">
|
||||||
|
<div className="bg-slate-800 rounded-lg p-6 max-w-3xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
<h2 className="text-2xl font-bold mb-6">Create New Discount</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-400 mb-2">Discount Name</label>
|
||||||
|
<input type="text" className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg" placeholder="e.g., Summer Sale" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-400 mb-2">Discount Code</label>
|
||||||
|
<input type="text" className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg" placeholder="e.g., SUMMER25" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-400 mb-2">Discount Type</label>
|
||||||
|
<select className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg">
|
||||||
|
<option value="percentage">Percentage Off</option>
|
||||||
|
<option value="fixed">Fixed Amount</option>
|
||||||
|
<option value="bogo">Buy One Get One</option>
|
||||||
|
<option value="free-shipping">Free Shipping</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-400 mb-2">Value</label>
|
||||||
|
<input type="number" className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg" placeholder="10" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-400 mb-2">Start Date</label>
|
||||||
|
<input type="date" className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-400 mb-2">End Date</label>
|
||||||
|
<input type="date" className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-400 mb-2">Min Purchase (Optional)</label>
|
||||||
|
<input type="number" step="0.01" className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg" placeholder="0.00" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-400 mb-2">Usage Limit (Optional)</label>
|
||||||
|
<input type="number" className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg" placeholder="Unlimited" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-400 mb-2">Applicable To</label>
|
||||||
|
<select className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg">
|
||||||
|
<option value="all">All Products</option>
|
||||||
|
<option value="specific-products">Specific Products</option>
|
||||||
|
<option value="specific-categories">Specific Categories</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 mt-6">
|
||||||
|
<button className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg font-medium">
|
||||||
|
Create Discount
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNewDiscount(false)}
|
||||||
|
className="flex-1 px-4 py-2 bg-slate-700 hover:bg-slate-600 rounded-lg font-medium"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatCard({ title, value, icon }: { title: string; value: string | number; icon: string }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-slate-800 rounded-lg p-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-slate-400 text-sm">{title}</span>
|
||||||
|
<span className="text-2xl">{icon}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold">{value}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
222
servers/lightspeed/src/ui/react-app/product-performance.tsx
Normal file
222
servers/lightspeed/src/ui/react-app/product-performance.tsx
Normal file
@ -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<ProductPerformance[]>([
|
||||||
|
{ 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 (
|
||||||
|
<div className="min-h-screen bg-slate-950 text-slate-100 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<h1 className="text-3xl font-bold mb-8">Product Performance Analytics</h1>
|
||||||
|
|
||||||
|
{/* Summary Stats */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||||
|
<StatCard title="Total Revenue" value={`$${totalRevenue.toFixed(2)}`} icon="💰" />
|
||||||
|
<StatCard title="Total Profit" value={`$${totalProfit.toFixed(2)}`} icon="📊" />
|
||||||
|
<StatCard title="Units Sold" value={totalUnits} icon="📦" />
|
||||||
|
<StatCard title="Avg Turnover" value={`${avgTurnover.toFixed(1)}×`} icon="🔄" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Controls */}
|
||||||
|
<div className="bg-slate-800 rounded-lg p-4 mb-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-400 mb-2">Sort By</label>
|
||||||
|
<select
|
||||||
|
value={sortBy}
|
||||||
|
onChange={(e) => setSortBy(e.target.value as any)}
|
||||||
|
className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg"
|
||||||
|
>
|
||||||
|
<option value="revenue">Revenue</option>
|
||||||
|
<option value="units">Units Sold</option>
|
||||||
|
<option value="profit">Profit</option>
|
||||||
|
<option value="turnover">Stock Turnover</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-400 mb-2">Direction</label>
|
||||||
|
<select
|
||||||
|
value={sortDirection}
|
||||||
|
onChange={(e) => setSortDirection(e.target.value as 'asc' | 'desc')}
|
||||||
|
className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg"
|
||||||
|
>
|
||||||
|
<option value="desc">Highest First</option>
|
||||||
|
<option value="asc">Lowest First</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-400 mb-2">Category</label>
|
||||||
|
<select
|
||||||
|
value={filterCategory}
|
||||||
|
onChange={(e) => setFilterCategory(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg"
|
||||||
|
>
|
||||||
|
{categories.map(cat => (
|
||||||
|
<option key={cat} value={cat}>{cat.charAt(0).toUpperCase() + cat.slice(1)}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Performance Table */}
|
||||||
|
<div className="bg-slate-800 rounded-lg overflow-hidden">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-slate-700">
|
||||||
|
<tr>
|
||||||
|
<th className="text-left py-3 px-4 text-slate-300 font-medium">Product</th>
|
||||||
|
<th className="text-left py-3 px-4 text-slate-300 font-medium">Category</th>
|
||||||
|
<th className="text-right py-3 px-4 text-slate-300 font-medium">Units Sold</th>
|
||||||
|
<th className="text-right py-3 px-4 text-slate-300 font-medium">Revenue</th>
|
||||||
|
<th className="text-right py-3 px-4 text-slate-300 font-medium">Profit</th>
|
||||||
|
<th className="text-right py-3 px-4 text-slate-300 font-medium">Avg Price</th>
|
||||||
|
<th className="text-right py-3 px-4 text-slate-300 font-medium">Turnover</th>
|
||||||
|
<th className="text-center py-3 px-4 text-slate-300 font-medium">Trend</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{sortedProducts.map((product, index) => (
|
||||||
|
<tr key={product.id} className="border-t border-slate-700 hover:bg-slate-700/30">
|
||||||
|
<td className="py-4 px-4">
|
||||||
|
<div className="font-medium">{product.name}</div>
|
||||||
|
<div className="text-sm text-slate-400">{product.sku}</div>
|
||||||
|
</td>
|
||||||
|
<td className="py-4 px-4 text-slate-400">{product.category}</td>
|
||||||
|
<td className="py-4 px-4 text-right font-semibold">{product.unitsSold}</td>
|
||||||
|
<td className="py-4 px-4 text-right font-semibold text-green-400">${product.revenue.toFixed(2)}</td>
|
||||||
|
<td className="py-4 px-4 text-right font-semibold text-blue-400">${product.profit.toFixed(2)}</td>
|
||||||
|
<td className="py-4 px-4 text-right text-slate-300">${product.averagePrice.toFixed(2)}</td>
|
||||||
|
<td className="py-4 px-4 text-right">
|
||||||
|
<span className={`font-semibold ${
|
||||||
|
product.stockTurnover >= 8 ? 'text-green-400' :
|
||||||
|
product.stockTurnover >= 5 ? 'text-yellow-400' : 'text-red-400'
|
||||||
|
}`}>
|
||||||
|
{product.stockTurnover.toFixed(1)}×
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="py-4 px-4">
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<span className="text-xl">{getTrendIcon(product.trend)}</span>
|
||||||
|
<span className={`text-sm font-semibold ${getTrendColor(product.trend)}`}>
|
||||||
|
{product.trendPercentage > 0 ? '+' : ''}{product.trendPercentage.toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Insights */}
|
||||||
|
<div className="mt-8 grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div className="bg-green-500/10 border border-green-500/50 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<span className="text-2xl">⭐</span>
|
||||||
|
<h3 className="font-bold text-green-400">Top Performer</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-slate-300">
|
||||||
|
{sortedProducts[0]?.name} leads with ${sortedProducts[0]?.revenue.toFixed(2)} in revenue
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-yellow-500/10 border border-yellow-500/50 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<span className="text-2xl">🔄</span>
|
||||||
|
<h3 className="font-bold text-yellow-400">Fast Mover</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-slate-300">
|
||||||
|
{[...products].sort((a, b) => b.stockTurnover - a.stockTurnover)[0]?.name} has the highest turnover rate
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-blue-500/10 border border-blue-500/50 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<span className="text-2xl">💎</span>
|
||||||
|
<h3 className="font-bold text-blue-400">Most Profitable</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-slate-300">
|
||||||
|
{[...products].sort((a, b) => b.profit - a.profit)[0]?.name} generates highest profit margin
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatCard({ title, value, icon }: { title: string; value: string | number; icon: string }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-slate-800 rounded-lg p-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-slate-400 text-sm">{title}</span>
|
||||||
|
<span className="text-2xl">{icon}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold">{value}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
316
servers/lightspeed/src/ui/react-app/purchase-orders.tsx
Normal file
316
servers/lightspeed/src/ui/react-app/purchase-orders.tsx
Normal file
@ -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<PurchaseOrder[]>([
|
||||||
|
{
|
||||||
|
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<string | null>(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 (
|
||||||
|
<div className="min-h-screen bg-slate-950 text-slate-100 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="flex items-center justify-between mb-8">
|
||||||
|
<h1 className="text-3xl font-bold">Purchase Orders</h1>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNewOrder(true)}
|
||||||
|
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg font-medium"
|
||||||
|
>
|
||||||
|
+ New Purchase Order
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||||
|
<StatCard title="Total Orders" value={orders.length} icon="📋" />
|
||||||
|
<StatCard title="Pending" value={orders.filter(o => o.status === 'pending' || o.status === 'ordered').length} icon="⏳" />
|
||||||
|
<StatCard title="Total Value" value={`$${totalValue.toFixed(2)}`} icon="💰" />
|
||||||
|
<StatCard title="Pending Value" value={`$${pendingValue.toFixed(2)}`} icon="📊" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter */}
|
||||||
|
<div className="bg-slate-800 rounded-lg p-4 mb-6">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<label className="text-sm text-slate-400">Filter by Status:</label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{['all', 'draft', 'pending', 'ordered', 'received', 'cancelled'].map(status => (
|
||||||
|
<button
|
||||||
|
key={status}
|
||||||
|
onClick={() => setFilterStatus(status)}
|
||||||
|
className={`px-4 py-2 rounded-lg text-sm font-medium capitalize ${
|
||||||
|
filterStatus === status ? 'bg-blue-600' : 'bg-slate-700 hover:bg-slate-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Orders List */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{filteredOrders.map((order) => (
|
||||||
|
<div key={order.id} className="bg-slate-800 rounded-lg p-6 hover:ring-2 ring-blue-500 transition-all">
|
||||||
|
<div className="flex items-start justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-bold">{order.orderNumber}</h3>
|
||||||
|
<div className="flex items-center gap-4 mt-1 text-sm text-slate-400">
|
||||||
|
<span>🏢 {order.supplier}</span>
|
||||||
|
<span>📅 Ordered: {new Date(order.orderDate).toLocaleDateString()}</span>
|
||||||
|
<span>🚚 Expected: {new Date(order.expectedDelivery).toLocaleDateString()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className={`px-3 py-1 rounded text-sm font-medium ${getStatusColor(order.status)}`}>
|
||||||
|
{order.status.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Items Summary */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="text-sm text-slate-400 mb-2">{order.items.length} items</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
|
{order.items.map((item, idx) => (
|
||||||
|
<div key={idx} className="flex items-center justify-between p-2 bg-slate-700 rounded">
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-sm">{item.productName}</div>
|
||||||
|
<div className="text-xs text-slate-400">{item.sku} • Qty: {item.quantity}</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="font-semibold">${item.total.toFixed(2)}</div>
|
||||||
|
<div className="text-xs text-slate-400">${item.unitCost.toFixed(2)} each</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Totals */}
|
||||||
|
<div className="grid grid-cols-4 gap-4 mb-4 pt-4 border-t border-slate-700">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-slate-400">Subtotal</div>
|
||||||
|
<div className="font-semibold">${order.subtotal.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-slate-400">Tax</div>
|
||||||
|
<div className="font-semibold">${order.tax.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-slate-400">Shipping</div>
|
||||||
|
<div className="font-semibold">${order.shipping.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-slate-400">Total</div>
|
||||||
|
<div className="text-xl font-bold text-green-400">${order.total.toFixed(2)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{order.notes && (
|
||||||
|
<div className="mb-4 p-3 bg-blue-500/10 border border-blue-500/50 rounded text-sm">
|
||||||
|
<strong>Notes:</strong> {order.notes}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedOrder(order.id)}
|
||||||
|
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded text-sm font-medium"
|
||||||
|
>
|
||||||
|
View Details
|
||||||
|
</button>
|
||||||
|
{order.status === 'draft' && (
|
||||||
|
<button className="px-4 py-2 bg-green-600 hover:bg-green-700 rounded text-sm font-medium">
|
||||||
|
Submit Order
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{order.status === 'ordered' && (
|
||||||
|
<button className="px-4 py-2 bg-green-600 hover:bg-green-700 rounded text-sm font-medium">
|
||||||
|
Mark as Received
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button className="px-4 py-2 bg-slate-700 hover:bg-slate-600 rounded text-sm font-medium">
|
||||||
|
Print
|
||||||
|
</button>
|
||||||
|
{order.status === 'draft' && (
|
||||||
|
<button className="px-4 py-2 bg-red-600 hover:bg-red-700 rounded text-sm font-medium ml-auto">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* New Order Modal */}
|
||||||
|
{showNewOrder && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-6 z-50">
|
||||||
|
<div className="bg-slate-800 rounded-lg p-6 max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
|
<h2 className="text-2xl font-bold mb-6">Create Purchase Order</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-400 mb-2">Supplier</label>
|
||||||
|
<select className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg">
|
||||||
|
<option>Colombian Coffee Co.</option>
|
||||||
|
<option>Equipment Suppliers Inc.</option>
|
||||||
|
<option>Tea Imports Ltd.</option>
|
||||||
|
<option>Accessories Direct</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-400 mb-2">Expected Delivery</label>
|
||||||
|
<input type="date" className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-400 mb-2">Notes (Optional)</label>
|
||||||
|
<textarea className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg resize-none" rows={2}></textarea>
|
||||||
|
</div>
|
||||||
|
<div className="pt-4 border-t border-slate-700">
|
||||||
|
<h3 className="font-semibold mb-3">Items</h3>
|
||||||
|
<button className="w-full px-4 py-3 bg-slate-700 hover:bg-slate-600 rounded-lg text-sm font-medium border-2 border-dashed border-slate-600">
|
||||||
|
+ Add Product
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 mt-6">
|
||||||
|
<button className="flex-1 px-4 py-2 bg-slate-700 hover:bg-slate-600 rounded-lg font-medium">
|
||||||
|
Save as Draft
|
||||||
|
</button>
|
||||||
|
<button className="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg font-medium">
|
||||||
|
Submit Order
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowNewOrder(false)}
|
||||||
|
className="px-4 py-2 bg-slate-600 hover:bg-slate-500 rounded-lg font-medium"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatCard({ title, value, icon }: { title: string; value: string | number; icon: string }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-slate-800 rounded-lg p-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-slate-400 text-sm">{title}</span>
|
||||||
|
<span className="text-2xl">{icon}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold">{value}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
211
servers/lightspeed/src/ui/react-app/sales-report.tsx
Normal file
211
servers/lightspeed/src/ui/react-app/sales-report.tsx
Normal file
@ -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<SalesData[]>([
|
||||||
|
{ 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 (
|
||||||
|
<div className="min-h-screen bg-slate-950 text-slate-100 p-6">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<h1 className="text-3xl font-bold mb-8">Sales Report</h1>
|
||||||
|
|
||||||
|
{/* Controls */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
|
||||||
|
<div className="bg-slate-800 rounded-lg p-4">
|
||||||
|
<label className="block text-sm text-slate-400 mb-2">Date Range</label>
|
||||||
|
<select
|
||||||
|
value={dateRange}
|
||||||
|
onChange={(e) => setDateRange(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg"
|
||||||
|
>
|
||||||
|
<option value="today">Today</option>
|
||||||
|
<option value="week">This Week</option>
|
||||||
|
<option value="month">This Month</option>
|
||||||
|
<option value="quarter">This Quarter</option>
|
||||||
|
<option value="year">This Year</option>
|
||||||
|
<option value="custom">Custom Range</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="bg-slate-800 rounded-lg p-4">
|
||||||
|
<label className="block text-sm text-slate-400 mb-2">Report Type</label>
|
||||||
|
<select
|
||||||
|
value={reportType}
|
||||||
|
onChange={(e) => setReportType(e.target.value)}
|
||||||
|
className="w-full px-4 py-2 bg-slate-700 border border-slate-600 rounded-lg"
|
||||||
|
>
|
||||||
|
<option value="overview">Overview</option>
|
||||||
|
<option value="products">By Product</option>
|
||||||
|
<option value="categories">By Category</option>
|
||||||
|
<option value="employees">By Employee</option>
|
||||||
|
<option value="payment">By Payment Method</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Summary Stats */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
||||||
|
<StatCard title="Total Sales" value={`$${totalSales.toFixed(2)}`} icon="💰" trend="+12.5%" />
|
||||||
|
<StatCard title="Transactions" value={totalTransactions} icon="🧾" trend="+8.2%" />
|
||||||
|
<StatCard title="Avg Transaction" value={`$${averageTransactionValue.toFixed(2)}`} icon="📊" trend="+3.7%" />
|
||||||
|
<StatCard title="Avg Daily Sales" value={`$${averageDailySales.toFixed(2)}`} icon="📈" trend="+15.4%" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
|
{/* Daily Sales Chart */}
|
||||||
|
<div className="bg-slate-800 rounded-lg p-6">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Daily Sales Trend</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{dailySales.map((day) => (
|
||||||
|
<div key={day.date} className="flex items-center gap-3">
|
||||||
|
<span className="text-sm text-slate-400 w-24">{new Date(day.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}</span>
|
||||||
|
<div className="flex-1 bg-slate-700 rounded-full h-10 relative overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="bg-gradient-to-r from-green-500 to-green-600 h-full rounded-full flex items-center justify-end pr-3"
|
||||||
|
style={{ width: `${(day.sales / maxSales) * 100}%` }}
|
||||||
|
>
|
||||||
|
<span className="text-white text-sm font-medium">${day.sales.toFixed(0)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-slate-400 w-16 text-right">{day.transactions} txn</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category Breakdown */}
|
||||||
|
<div className="bg-slate-800 rounded-lg p-6">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Sales by Category</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{categoryBreakdown.map((cat) => (
|
||||||
|
<div key={cat.category}>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="font-medium">{cat.category}</span>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="font-bold text-green-400">${cat.sales.toFixed(2)}</div>
|
||||||
|
<div className="text-xs text-slate-400">{cat.percentage}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-slate-700 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="bg-blue-500 rounded-full h-2"
|
||||||
|
style={{ width: `${cat.percentage}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Top Products */}
|
||||||
|
<div className="bg-slate-800 rounded-lg p-6 mb-6">
|
||||||
|
<h2 className="text-xl font-semibold mb-4">Top Performing Products</h2>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="border-b border-slate-700">
|
||||||
|
<tr>
|
||||||
|
<th className="text-left py-3 px-4 text-slate-400 font-medium">Rank</th>
|
||||||
|
<th className="text-left py-3 px-4 text-slate-400 font-medium">Product</th>
|
||||||
|
<th className="text-right py-3 px-4 text-slate-400 font-medium">Units Sold</th>
|
||||||
|
<th className="text-right py-3 px-4 text-slate-400 font-medium">Revenue</th>
|
||||||
|
<th className="text-right py-3 px-4 text-slate-400 font-medium">Avg Price</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{topProducts.map((product, index) => (
|
||||||
|
<tr key={product.name} className="border-b border-slate-700/50 hover:bg-slate-700/30">
|
||||||
|
<td className="py-3 px-4">
|
||||||
|
<span className={`inline-flex items-center justify-center w-8 h-8 rounded-full font-bold ${
|
||||||
|
index === 0 ? 'bg-yellow-500/20 text-yellow-400' :
|
||||||
|
index === 1 ? 'bg-slate-400/20 text-slate-300' :
|
||||||
|
index === 2 ? 'bg-amber-700/20 text-amber-500' :
|
||||||
|
'bg-slate-700 text-slate-400'
|
||||||
|
}`}>
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4 font-medium">{product.name}</td>
|
||||||
|
<td className="py-3 px-4 text-right text-slate-300">{product.units}</td>
|
||||||
|
<td className="py-3 px-4 text-right font-semibold text-green-400">${product.revenue.toFixed(2)}</td>
|
||||||
|
<td className="py-3 px-4 text-right text-slate-400">${(product.revenue / product.units).toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Export Options */}
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button className="px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg font-medium">
|
||||||
|
📊 Export to Excel
|
||||||
|
</button>
|
||||||
|
<button className="px-6 py-3 bg-slate-700 hover:bg-slate-600 rounded-lg font-medium">
|
||||||
|
📄 Export to PDF
|
||||||
|
</button>
|
||||||
|
<button className="px-6 py-3 bg-slate-700 hover:bg-slate-600 rounded-lg font-medium">
|
||||||
|
📧 Email Report
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatCard({ title, value, icon, trend }: { title: string; value: string | number; icon: string; trend?: string }) {
|
||||||
|
const isPositive = trend?.startsWith('+');
|
||||||
|
return (
|
||||||
|
<div className="bg-slate-800 rounded-lg p-4">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-slate-400 text-sm">{title}</span>
|
||||||
|
<span className="text-2xl">{icon}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-2xl font-bold mb-1">{value}</div>
|
||||||
|
{trend && (
|
||||||
|
<div className={`text-sm font-medium ${isPositive ? 'text-green-400' : 'text-red-400'}`}>
|
||||||
|
{trend} vs last period
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user