Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Business Tracker - One Beaver 1300 Enhanced</title>
- <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
- <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
- <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
- <script src="https://cdn.tailwindcss.com"></script>
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.js"></script>
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
- </head>
- <body>
- <div id="root"></div>
- <script type="text/babel">
- const { useState, useMemo, useEffect, useRef } = React;
- // Initial materials data
- const INITIAL_MATERIALS = [
- { id: '1001', item: 'Pastel Sheets', type: 'Paper', category: 'Core Materials', supplier: 'Staples', itemSize: '8.5" x 11"', grade: '75 gm2', packCount: 500, units: 'Sheets', packCost: 18.99, packCostAfterTax: 21.269, cpp: 0.043, source: '-' },
- { id: '2001', item: 'Cream Cardstock Sheet', type: 'Cardstock', category: 'Core Materials', supplier: 'Staples', itemSize: '8.5" x 11"', grade: '176 gm2', packCount: 250, units: 'Sheets', packCost: 27.29, packCostAfterTax: 30.565, cpp: 0.122, source: '-' },
- { id: '2002', item: 'Cream Cardstock Sheets', type: 'Cardstock', category: 'Core Materials', supplier: 'Michaels', itemSize: '8.5" x 11"', grade: '176 gm2', packCount: 50, units: 'Sheets', packCost: 11.99, packCostAfterTax: 13.429, cpp: 0.269, source: '-' },
- { id: '2003', item: 'Cream Cardstock Sheets', type: 'Cardstock', category: 'Premium Materials', supplier: 'Michaels', itemSize: '8.5" x 11"', grade: '300 gm2', packCount: 100, units: 'Sheets', packCost: 21.99, packCostAfterTax: 24.629, cpp: 0.246, source: '-' },
- { id: '3001', item: 'Lineco PVA Glue', type: 'Glue', category: 'Core Materials', supplier: 'Opus', itemSize: '32', grade: 'Neutral PH', packCount: 32, units: 'Ounces', packCost: 52.49, packCostAfterTax: 58.789, cpp: 1.837, source: '-' },
- { id: '3002', item: 'Glue Strips', type: 'Glue', category: 'Core Materials', supplier: 'Amazon', itemSize: '.5" x 11"', grade: 'Hot Melt', packCount: 100, units: 'Strips', packCost: 19.99, packCostAfterTax: 22.289, cpp: 0.224, source: '-' },
- { id: '4001', item: 'Light Brown PUV Material', type: 'Cover', category: 'Premium Materials', supplier: 'Michaels', itemSize: '1m x 1.37m', grade: 'PUV', packCount: 2123, units: 'Square Inch', packCost: 16.00, packCostAfterTax: 17.920, cpp: 0.008, source: '-' },
- { id: '5002', item: 'Brown Ribbon', type: 'Ribbon', category: 'Decorative', supplier: 'Michaels', itemSize: '9mm', grade: '-', packCount: 108, units: 'Inches', packCost: 6.99, packCostAfterTax: 7.829, cpp: 0.072, source: '-' },
- { id: '6001', item: 'Lineco Thread', type: 'Thread', category: 'Core Materials', supplier: 'Amazon', itemSize: '35/3', grade: 'Unwaxed', packCount: 3600, units: 'Inches', packCost: 30.36, packCostAfterTax: 34.003, cpp: 0.009, source: '-' },
- { id: '7001', item: 'Mull Cloth', type: 'Cloth', category: 'Core Materials', supplier: 'Amazon', itemSize: '20" x 60"', grade: '-', packCount: 1200, units: 'Square Inches', packCost: 32.00, packCostAfterTax: 35.840, cpp: 0.030, source: '-' }
- ];
- // Initial product materials data
- const INITIAL_PRODUCT_MATERIALS = {
- 'PB-BB-BVR1M-1300-0825': {
- name: 'One Beaver - 1300 - Mini',
- category: 'Premium',
- size: '3.5 x 5.5',
- sellingPrice: 8.00,
- totalCost: 1.813,
- profit: 6.187,
- profitMargin: 77.3,
- timeToMake: 1.5,
- toMake1k: 162,
- materials: [
- { id: '1001', name: 'Pastel Sheets', quantity: 15.00, cpp: 0.043, materialCost: 0.638, type: 'Paper' },
- { id: '2001', name: 'Cream Cardstock Sheet', quantity: 1.00, cpp: 0.122, materialCost: 0.122, type: 'Cardstock' },
- { id: '2003', name: 'Cream Cardstock Sheets', quantity: 0.50, cpp: 0.246, materialCost: 0.123, type: 'Cardstock' },
- { id: '3001', name: 'Lineco PVA Glue', quantity: 0.30, cpp: 1.837, materialCost: 0.551, type: 'Glue' },
- { id: '3002', name: 'Glue Strips', quantity: 0.25, cpp: 0.224, materialCost: 0.056, type: 'Glue' },
- { id: '4001', name: 'Light Brown PUV Material', quantity: 38.50, cpp: 0.008, materialCost: 0.323, type: 'Cover' }
- ]
- },
- 'PB-BB-BVR1K-1300-0825': {
- name: 'One Beaver - 1300 - King',
- category: 'Premium',
- size: '8.5 x 11',
- sellingPrice: 25.00,
- totalCost: 11.478,
- profit: 13.522,
- profitMargin: 54.1,
- timeToMake: 3.5,
- toMake1k: 74,
- materials: [
- { id: '1001', name: 'Pastel Sheets', quantity: 100.00, cpp: 0.043, materialCost: 4.250, type: 'Paper' },
- { id: '2001', name: 'Cream Cardstock Sheet', quantity: 4.00, cpp: 0.122, materialCost: 0.489, type: 'Cardstock' },
- { id: '2002', name: 'Cream Cardstock Sheets', quantity: 4.00, cpp: 0.269, materialCost: 1.074, type: 'Cardstock' },
- { id: '2003', name: 'Cream Cardstock Sheets', quantity: 2.50, cpp: 0.246, materialCost: 0.616, type: 'Cardstock' },
- { id: '3001', name: 'Lineco PVA Glue', quantity: 2.00, cpp: 1.837, materialCost: 3.674, type: 'Glue' },
- { id: '3002', name: 'Glue Strips', quantity: 1.00, cpp: 0.224, materialCost: 0.224, type: 'Glue' },
- { id: '4001', name: 'Light Brown PUV Material', quantity: 137.00, cpp: 0.008, materialCost: 1.151, type: 'Cover' }
- ]
- },
- 'PB-BB-BVR1-1300-0825': {
- name: 'One Beaver - 1300',
- category: 'Premium',
- size: '7 x 10',
- sellingPrice: 20.00,
- totalCost: 8.377,
- profit: 11.623,
- profitMargin: 58.1,
- timeToMake: 2.5,
- toMake1k: 86,
- materials: [
- { id: '1001', name: 'Pastel Sheets', quantity: 80.00, cpp: 0.043, materialCost: 3.400, type: 'Paper' },
- { id: '2001', name: 'Cream Cardstock Sheet', quantity: 3.00, cpp: 0.122, materialCost: 0.367, type: 'Cardstock' },
- { id: '2002', name: 'Cream Cardstock Sheets', quantity: 3.00, cpp: 0.269, materialCost: 0.806, type: 'Cardstock' },
- { id: '2003', name: 'Cream Cardstock Sheets', quantity: 2.50, cpp: 0.246, materialCost: 0.616, type: 'Cardstock' },
- { id: '3001', name: 'Lineco PVA Glue', quantity: 1.12, cpp: 1.837, materialCost: 2.058, type: 'Glue' },
- { id: '3002', name: 'Glue Strips', quantity: 1.00, cpp: 0.224, materialCost: 0.224, type: 'Glue' },
- { id: '4001', name: 'Light Brown PUV Material', quantity: 108.00, cpp: 0.008, materialCost: 0.907, type: 'Cover' }
- ]
- },
- 'PB-BB-BVR1C-1300-0825': {
- name: 'One Beaver - 1300 - Companion',
- category: 'Premium',
- size: '4.25 x 7.25',
- sellingPrice: 13.00,
- totalCost: 4.609,
- profit: 8.391,
- profitMargin: 64.5,
- timeToMake: 2.0,
- toMake1k: 119,
- materials: [
- { id: '1001', name: 'Pastel Sheets', quantity: 40.00, cpp: 0.043, materialCost: 1.700, type: 'Paper' },
- { id: '2001', name: 'Cream Cardstock Sheet', quantity: 1.50, cpp: 0.122, materialCost: 0.183, type: 'Cardstock' },
- { id: '2002', name: 'Cream Cardstock Sheets', quantity: 2.00, cpp: 0.269, materialCost: 0.537, type: 'Cardstock' },
- { id: '2003', name: 'Cream Cardstock Sheets', quantity: 1.00, cpp: 0.246, materialCost: 0.246, type: 'Cardstock' },
- { id: '3001', name: 'Lineco PVA Glue', quantity: 0.60, cpp: 1.837, materialCost: 1.102, type: 'Glue' },
- { id: '3002', name: 'Glue Strips', quantity: 0.75, cpp: 0.224, materialCost: 0.168, type: 'Glue' },
- { id: '4001', name: 'Light Brown PUV Material', quantity: 80.00, cpp: 0.008, materialCost: 0.672, type: 'Cover' }
- ]
- },
- 'PB-BB-BVR1MB-1300-0825': {
- name: 'One Beaver - 1300 - Mini - Basic',
- category: 'Basic',
- size: '3.5 x 5.5',
- sellingPrice: 5.00,
- totalCost: 1.673,
- profit: 3.327,
- profitMargin: 66.5,
- timeToMake: 1.0,
- toMake1k: 301,
- materials: [
- { id: '1001', name: 'Pastel Sheets', quantity: 15.00, cpp: 0.043, materialCost: 0.638, type: 'Paper' },
- { id: '2001', name: 'Cream Cardstock Sheet', quantity: 0.50, cpp: 0.122, materialCost: 0.061, type: 'Cardstock' },
- { id: '3001', name: 'Lineco PVA Glue', quantity: 0.50, cpp: 1.837, materialCost: 0.919, type: 'Glue' },
- { id: '3002', name: 'Glue Strips', quantity: 0.25, cpp: 0.224, materialCost: 0.056, type: 'Glue' }
- ]
- },
- 'PB-BB-BVR1CB-1300-0825': {
- name: 'One Beaver - 1300 - Companion - Basic',
- category: 'Basic',
- size: '4.25 x 7.25',
- sellingPrice: 9.00,
- totalCost: 2.943,
- profit: 6.057,
- profitMargin: 67.3,
- timeToMake: 1.5,
- toMake1k: 165,
- materials: [
- { id: '1001', name: 'Pastel Sheets', quantity: 30.00, cpp: 0.043, materialCost: 1.275, type: 'Paper' },
- { id: '2001', name: 'Cream Cardstock Sheet', quantity: 1.00, cpp: 0.122, materialCost: 0.122, type: 'Cardstock' },
- { id: '3001', name: 'Lineco PVA Glue', quantity: 0.75, cpp: 1.837, materialCost: 1.378, type: 'Glue' },
- { id: '3002', name: 'Glue Strips', quantity: 0.75, cpp: 0.224, materialCost: 0.168, type: 'Glue' }
- ]
- },
- 'PB-BB-BVR1B-1300-0825': {
- name: 'One Beaver - 1300 - Basic',
- category: 'Basic',
- size: '7 x 10',
- sellingPrice: 15.00,
- totalCost: 4.917,
- profit: 10.083,
- profitMargin: 67.2,
- timeToMake: 2.0,
- toMake1k: 99,
- materials: [
- { id: '1001', name: 'Pastel Sheets', quantity: 60.00, cpp: 0.043, materialCost: 2.550, type: 'Paper' },
- { id: '2001', name: 'Cream Cardstock Sheet', quantity: 2.50, cpp: 0.122, materialCost: 0.306, type: 'Cardstock' },
- { id: '3001', name: 'Lineco PVA Glue', quantity: 1.00, cpp: 1.837, materialCost: 1.837, type: 'Glue' },
- { id: '3002', name: 'Glue Strips', quantity: 1.00, cpp: 0.224, materialCost: 0.224, type: 'Glue' }
- ]
- }
- };
- const MARKET_DATA = [
- { sizeCategory: 'Small/Pocket', dimensions: '3.5" x 5.5"', marketPercent: '30-35%', typicalSKU: '3-5 variants', yourProduct: 'Mini', yourPrice: 8.00, competitorAvg: 22.28, pricePosition: '64-71% cheaper', suggestion: '$8-10', coverage: '✓ Mini + Mini Basic' },
- { sizeCategory: 'Standard/Medium', dimensions: '4.25" x 7.25" to 5" x 8"', marketPercent: '40-45%', typicalSKU: '4-6 variants', yourProduct: 'Companion', yourPrice: 13.00, competitorAvg: 16.26, pricePosition: '19-50% cheaper', suggestion: '$13-15', coverage: '✓ Companion + Companion Basic' },
- { sizeCategory: 'Large', dimensions: '7" x 10" to 8" x 10"', marketPercent: '15-20%', typicalSKU: '2-3 variants', yourProduct: 'One Beaver', yourPrice: 20.00, competitorAvg: 22.30, pricePosition: '9-33% cheaper', suggestion: '$20-22', coverage: '✓ One Beaver + One Beaver Basic' },
- { sizeCategory: 'Extra Large', dimensions: '8.5" x 11"', marketPercent: '5-10%', typicalSKU: '1-2 variants', yourProduct: 'King', yourPrice: 25.00, competitorAvg: 26.35, pricePosition: '4-29% cheaper', suggestion: '$25-28', coverage: '✓ King' }
- ];
- function BusinessTracker() {
- const [activeTab, setActiveTab] = useState('dashboard');
- // Materials State
- const [materials, setMaterials] = useState(INITIAL_MATERIALS);
- const [editingMaterial, setEditingMaterial] = useState(null);
- const [showAddMaterial, setShowAddMaterial] = useState(false);
- const [materialFilter, setMaterialFilter] = useState({ type: 'all', category: 'all' });
- const [newMaterial, setNewMaterial] = useState({
- id: '', item: '', type: 'Paper', category: '', supplier: '', itemSize: '', grade: '',
- packCount: '', units: '', packCost: '', packCostAfterTax: '', cpp: '', source: '-'
- });
- // Products State
- const [productMaterials, setProductMaterials] = useState(INITIAL_PRODUCT_MATERIALS);
- const [editingProduct, setEditingProduct] = useState(null);
- const [editingProductField, setEditingProductField] = useState({});
- const [showAddProduct, setShowAddProduct] = useState(false);
- const [productFilter, setProductFilter] = useState('all');
- const [newProduct, setNewProduct] = useState({
- id: '', name: '', category: '', size: '', sellingPrice: '', timeToMake: '', materials: []
- });
- const [expandedProducts, setExpandedProducts] = useState({});
- // Expense State
- const [expenses, setExpenses] = useState([]);
- const [expenseForm, setExpenseForm] = useState({
- date: new Date().toISOString().split('T')[0],
- supplier: '', category: '', itemDescription: '', cost: '', materialId: '', notes: ''
- });
- // Sales State
- const [sales, setSales] = useState([]);
- const [salesForm, setSalesForm] = useState({
- date: new Date().toISOString().split('T')[0],
- location: '', product: '', units: '1', pricePerUnit: '',
- customerId: '', repeatCustomer: 'No', customerNotes: '',
- complaint: 'No', returned: 'No', refunded: 'No'
- });
- // Estimator State
- const [estimatorMode, setEstimatorMode] = useState('profit-maximizer');
- const [productionTime, setProductionTime] = useState(40);
- const [premiumRatio, setPremiumRatio] = useState(50);
- const [manualQuantities, setManualQuantities] = useState({});
- const [savedScenarios, setSavedScenarios] = useState([]);
- // Drag and drop refs
- const materialsTableRef = useRef(null);
- const productsListRef = useRef(null);
- const expenseTableRef = useRef(null);
- const salesTableRef = useRef(null);
- // Chart refs
- const salesChartRef = useRef(null);
- const expenseChartRef = useRef(null);
- const marginChartRef = useRef(null);
- const materialCostChartRef = useRef(null);
- const expenseCategories = [
- 'Materials - Paper', 'Materials - Cardstock', 'Materials - Glue', 'Materials - Cover',
- 'Tools & Equipment', 'Packaging', 'Shipping', 'Marketing', 'Booth Fees', 'Other'
- ];
- // Drag and Drop Setup
- useEffect(() => {
- if (materialsTableRef.current && activeTab === 'materials') {
- const tbody = materialsTableRef.current.querySelector('tbody');
- if (tbody) {
- new Sortable(tbody, {
- animation: 150,
- handle: '.drag-handle',
- onEnd: (evt) => {
- const newMaterials = [...materials];
- const [movedItem] = newMaterials.splice(evt.oldIndex, 1);
- newMaterials.splice(evt.newIndex, 0, movedItem);
- setMaterials(newMaterials);
- }
- });
- }
- }
- }, [activeTab, materials.length]);
- useEffect(() => {
- if (productsListRef.current && activeTab === 'products') {
- new Sortable(productsListRef.current, {
- animation: 150,
- handle: '.drag-handle',
- onEnd: (evt) => {
- const productIds = Object.keys(productMaterials);
- const [movedId] = productIds.splice(evt.oldIndex, 1);
- productIds.splice(evt.newIndex, 0, movedId);
- const reordered = {};
- productIds.forEach(id => {
- reordered[id] = productMaterials[id];
- });
- setProductMaterials(reordered);
- }
- });
- }
- }, [activeTab, Object.keys(productMaterials).length]);
- // Dashboard Analytics
- const dashboardStats = useMemo(() => {
- const totalMaterials = materials.length;
- const totalProducts = Object.keys(productMaterials).length;
- const avgMargin = Object.values(productMaterials).reduce((sum, p) => sum + p.profitMargin, 0) / totalProducts || 0;
- const totalRevenue = sales.reduce((sum, sale) => sum + sale.total, 0);
- const totalExpenses = expenses.reduce((sum, exp) => sum + exp.cost, 0);
- const netProfit = totalRevenue - totalExpenses;
- // Product performance
- const productSales = {};
- sales.forEach(sale => {
- if (!productSales[sale.product]) {
- productSales[sale.product] = { units: 0, revenue: 0 };
- }
- productSales[sale.product].units += sale.units;
- productSales[sale.product].revenue += sale.total;
- });
- // Top performers
- const topProductsByMargin = Object.entries(productMaterials)
- .sort(([,a], [,b]) => b.profitMargin - a.profitMargin)
- .slice(0, 3);
- const topProductsByRevenue = Object.entries(productSales)
- .sort(([,a], [,b]) => b.revenue - a.revenue)
- .slice(0, 3);
- // Expense breakdown
- const expenseByCategory = {};
- expenses.forEach(exp => {
- if (!expenseByCategory[exp.category]) {
- expenseByCategory[exp.category] = 0;
- }
- expenseByCategory[exp.category] += exp.cost;
- });
- // Material cost contribution
- const materialCostBreakdown = {};
- Object.values(productMaterials).forEach(product => {
- product.materials.forEach(mat => {
- if (!materialCostBreakdown[mat.type]) {
- materialCostBreakdown[mat.type] = 0;
- }
- materialCostBreakdown[mat.type] += mat.materialCost;
- });
- });
- return {
- totalMaterials,
- totalProducts,
- avgMargin,
- totalRevenue,
- totalExpenses,
- netProfit,
- productSales,
- topProductsByMargin,
- topProductsByRevenue,
- expenseByCategory,
- materialCostBreakdown
- };
- }, [materials, productMaterials, sales, expenses]);
- // Update charts when dashboard is active
- useEffect(() => {
- if (activeTab === 'dashboard' && dashboardStats.productSales) {
- // Sales by product chart
- if (salesChartRef.current) {
- const ctx = salesChartRef.current.getContext('2d');
- if (salesChartRef.current.chart) {
- salesChartRef.current.chart.destroy();
- }
- salesChartRef.current.chart = new Chart(ctx, {
- type: 'bar',
- data: {
- labels: Object.keys(dashboardStats.productSales),
- datasets: [{
- label: 'Revenue',
- data: Object.values(dashboardStats.productSales).map(p => p.revenue),
- backgroundColor: 'rgba(34, 197, 94, 0.7)',
- borderColor: 'rgba(34, 197, 94, 1)',
- borderWidth: 1
- }]
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- plugins: {
- legend: { display: false },
- title: { display: true, text: 'Revenue by Product' }
- },
- scales: {
- y: { beginAtZero: true }
- }
- }
- });
- }
- // Expense by category chart
- if (expenseChartRef.current) {
- const ctx = expenseChartRef.current.getContext('2d');
- if (expenseChartRef.current.chart) {
- expenseChartRef.current.chart.destroy();
- }
- expenseChartRef.current.chart = new Chart(ctx, {
- type: 'doughnut',
- data: {
- labels: Object.keys(dashboardStats.expenseByCategory),
- datasets: [{
- data: Object.values(dashboardStats.expenseByCategory),
- backgroundColor: [
- 'rgba(59, 130, 246, 0.7)',
- 'rgba(16, 185, 129, 0.7)',
- 'rgba(245, 158, 11, 0.7)',
- 'rgba(239, 68, 68, 0.7)',
- 'rgba(139, 92, 246, 0.7)',
- 'rgba(236, 72, 153, 0.7)',
- 'rgba(14, 165, 233, 0.7)',
- 'rgba(132, 204, 22, 0.7)',
- 'rgba(251, 146, 60, 0.7)',
- 'rgba(107, 114, 128, 0.7)'
- ]
- }]
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- plugins: {
- legend: { position: 'right' },
- title: { display: true, text: 'Expenses by Category' }
- }
- }
- });
- }
- // Margin comparison chart
- if (marginChartRef.current) {
- const ctx = marginChartRef.current.getContext('2d');
- if (marginChartRef.current.chart) {
- marginChartRef.current.chart.destroy();
- }
- const products = Object.entries(productMaterials);
- marginChartRef.current.chart = new Chart(ctx, {
- type: 'bar',
- data: {
- labels: products.map(([, p]) => p.name),
- datasets: [{
- label: 'Profit Margin %',
- data: products.map(([, p]) => p.profitMargin),
- backgroundColor: products.map(([, p]) =>
- p.profitMargin >= 70 ? 'rgba(34, 197, 94, 0.7)' :
- p.profitMargin >= 60 ? 'rgba(234, 179, 8, 0.7)' :
- 'rgba(239, 68, 68, 0.7)'
- ),
- borderWidth: 1
- }]
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- plugins: {
- legend: { display: false },
- title: { display: true, text: 'Profit Margin by Product' }
- },
- scales: {
- y: { beginAtZero: true, max: 100 }
- }
- }
- });
- }
- // Material cost breakdown
- if (materialCostChartRef.current) {
- const ctx = materialCostChartRef.current.getContext('2d');
- if (materialCostChartRef.current.chart) {
- materialCostChartRef.current.chart.destroy();
- }
- materialCostChartRef.current.chart = new Chart(ctx, {
- type: 'pie',
- data: {
- labels: Object.keys(dashboardStats.materialCostBreakdown),
- datasets: [{
- data: Object.values(dashboardStats.materialCostBreakdown),
- backgroundColor: [
- 'rgba(59, 130, 246, 0.7)',
- 'rgba(16, 185, 129, 0.7)',
- 'rgba(245, 158, 11, 0.7)',
- 'rgba(239, 68, 68, 0.7)',
- 'rgba(139, 92, 246, 0.7)',
- 'rgba(236, 72, 153, 0.7)'
- ]
- }]
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- plugins: {
- legend: { position: 'right' },
- title: { display: true, text: 'Material Cost Distribution' }
- }
- }
- });
- }
- }
- }, [activeTab, dashboardStats]);
- // Add Material
- const addMaterial = () => {
- if (!newMaterial.id || !newMaterial.item || !newMaterial.packCount || !newMaterial.packCostAfterTax) {
- alert('Please fill in required fields: ID, Item, Pack Count, Pack Cost After Tax');
- return;
- }
- const cpp = parseFloat(newMaterial.packCostAfterTax) / parseFloat(newMaterial.packCount);
- setMaterials([...materials, {
- ...newMaterial,
- packCount: parseFloat(newMaterial.packCount),
- packCost: parseFloat(newMaterial.packCost) || 0,
- packCostAfterTax: parseFloat(newMaterial.packCostAfterTax),
- cpp: parseFloat(cpp.toFixed(3))
- }]);
- setNewMaterial({
- id: '', item: '', type: 'Paper', category: '', supplier: '', itemSize: '', grade: '',
- packCount: '', units: '', packCost: '', packCostAfterTax: '', cpp: '', source: '-'
- });
- setShowAddMaterial(false);
- };
- // Edit Material
- const updateMaterial = (id, updates) => {
- setMaterials(materials.map(m => {
- if (m.id === id) {
- const updated = { ...m, ...updates };
- if (updates.packCostAfterTax || updates.packCount) {
- updated.cpp = parseFloat((updated.packCostAfterTax / updated.packCount).toFixed(3));
- }
- return updated;
- }
- return m;
- }));
- };
- // Delete Material
- const deleteMaterial = (id) => {
- if (confirm('Delete this material? This will affect products using it.')) {
- setMaterials(materials.filter(m => m.id !== id));
- }
- };
- // Update product fields
- const updateProductField = (productId, field, value) => {
- const product = productMaterials[productId];
- const updatedProduct = { ...product };
- if (field === 'sellingPrice') {
- updatedProduct.sellingPrice = parseFloat(value) || 0;
- updatedProduct.profit = updatedProduct.sellingPrice - updatedProduct.totalCost;
- updatedProduct.profitMargin = (updatedProduct.profit / updatedProduct.sellingPrice) * 100;
- } else if (field === 'timeToMake') {
- updatedProduct.timeToMake = parseFloat(value) || 0;
- } else if (field === 'category') {
- updatedProduct.category = value;
- } else if (field === 'name') {
- updatedProduct.name = value;
- } else if (field === 'size') {
- updatedProduct.size = value;
- }
- setProductMaterials({
- ...productMaterials,
- [productId]: updatedProduct
- });
- };
- // Add Product
- const addProduct = () => {
- if (!newProduct.id || !newProduct.name || !newProduct.sellingPrice) {
- alert('Please fill in required fields: Product Number, Name, Selling Price');
- return;
- }
- const totalCost = newProduct.materials.reduce((sum, m) => sum + m.materialCost, 0);
- const profit = parseFloat(newProduct.sellingPrice) - totalCost;
- const profitMargin = (profit / parseFloat(newProduct.sellingPrice)) * 100;
- const toMake1k = totalCost > 0 ? Math.ceil(1000 / profit) : 0;
- setProductMaterials({
- ...productMaterials,
- [newProduct.id]: {
- name: newProduct.name,
- category: newProduct.category || 'Uncategorized',
- size: newProduct.size,
- sellingPrice: parseFloat(newProduct.sellingPrice),
- totalCost,
- profit,
- profitMargin,
- timeToMake: parseFloat(newProduct.timeToMake) || 2,
- toMake1k,
- materials: newProduct.materials
- }
- });
- setNewProduct({ id: '', name: '', category: '', size: '', sellingPrice: '', timeToMake: '', materials: [] });
- setShowAddProduct(false);
- };
- // Add material to new product
- const addMaterialToNewProduct = (materialId) => {
- const material = materials.find(m => m.id === materialId);
- if (!material) return;
- const newMaterialEntry = {
- id: material.id,
- name: material.item,
- quantity: 1,
- cpp: material.cpp,
- materialCost: material.cpp * 1,
- type: material.type
- };
- setNewProduct({
- ...newProduct,
- materials: [...newProduct.materials, newMaterialEntry]
- });
- };
- // Update material in new product form
- const updateNewProductMaterial = (index, field, value) => {
- const updatedMaterials = [...newProduct.materials];
- updatedMaterials[index] = { ...updatedMaterials[index], [field]: parseFloat(value) || 0 };
- if (field === 'quantity') {
- updatedMaterials[index].materialCost = updatedMaterials[index].quantity * updatedMaterials[index].cpp;
- }
- setNewProduct({ ...newProduct, materials: updatedMaterials });
- };
- // Remove material from new product
- const removeNewProductMaterial = (index) => {
- setNewProduct({
- ...newProduct,
- materials: newProduct.materials.filter((_, i) => i !== index)
- });
- };
- // Add material to existing product
- const addMaterialToProduct = (productId, materialId) => {
- const material = materials.find(m => m.id === materialId);
- if (!material) return;
- const product = productMaterials[productId];
- const newMaterialEntry = {
- id: material.id,
- name: material.item,
- quantity: 1,
- cpp: material.cpp,
- materialCost: material.cpp * 1,
- type: material.type
- };
- const updatedMaterials = [...product.materials, newMaterialEntry];
- const totalCost = updatedMaterials.reduce((sum, m) => sum + m.materialCost, 0);
- const profit = product.sellingPrice - totalCost;
- const profitMargin = (profit / product.sellingPrice) * 100;
- setProductMaterials({
- ...productMaterials,
- [productId]: {
- ...product,
- materials: updatedMaterials,
- totalCost,
- profit,
- profitMargin
- }
- });
- };
- // Remove material from product
- const removeMaterialFromProduct = (productId, materialIndex) => {
- const product = productMaterials[productId];
- const updatedMaterials = product.materials.filter((_, i) => i !== materialIndex);
- const totalCost = updatedMaterials.reduce((sum, m) => sum + m.materialCost, 0);
- const profit = product.sellingPrice - totalCost;
- const profitMargin = (profit / product.sellingPrice) * 100;
- setProductMaterials({
- ...productMaterials,
- [productId]: {
- ...product,
- materials: updatedMaterials,
- totalCost,
- profit,
- profitMargin
- }
- });
- };
- // Delete Product
- const deleteProduct = (id) => {
- if (confirm('Delete this product?')) {
- const updated = { ...productMaterials };
- delete updated[id];
- setProductMaterials(updated);
- }
- };
- // Update Product Materials
- const updateProductMaterial = (productId, materialIndex, field, value) => {
- const product = productMaterials[productId];
- const updatedMaterials = [...product.materials];
- updatedMaterials[materialIndex] = { ...updatedMaterials[materialIndex], [field]: parseFloat(value) || 0 };
- if (field === 'quantity' || field === 'cpp') {
- updatedMaterials[materialIndex].materialCost = updatedMaterials[materialIndex].quantity * updatedMaterials[materialIndex].cpp;
- }
- const totalCost = updatedMaterials.reduce((sum, m) => sum + m.materialCost, 0);
- const profit = product.sellingPrice - totalCost;
- const profitMargin = (profit / product.sellingPrice) * 100;
- setProductMaterials({
- ...productMaterials,
- [productId]: {
- ...product,
- materials: updatedMaterials,
- totalCost,
- profit,
- profitMargin
- }
- });
- };
- // Add expense
- const addExpense = () => {
- if (!expenseForm.supplier || !expenseForm.category || !expenseForm.cost) {
- alert('Please fill in required fields');
- return;
- }
- setExpenses([...expenses, { ...expenseForm, id: Date.now(), cost: parseFloat(expenseForm.cost) }]);
- setExpenseForm({ date: new Date().toISOString().split('T')[0], supplier: '', category: '', itemDescription: '', cost: '', materialId: '', notes: '' });
- };
- // Add sale
- const addSale = () => {
- if (!salesForm.location || !salesForm.product || !salesForm.pricePerUnit) {
- alert('Please fill in required fields');
- return;
- }
- const units = parseInt(salesForm.units);
- const price = parseFloat(salesForm.pricePerUnit);
- setSales([...sales, { ...salesForm, id: Date.now(), units, pricePerUnit: price, total: units * price }]);
- setSalesForm({ date: new Date().toISOString().split('T')[0], location: '', product: '', units: '1', pricePerUnit: '', customerId: '', repeatCustomer: 'No', customerNotes: '', complaint: 'No', returned: 'No', refunded: 'No' });
- };
- // Estimator
- const estimatorResults = useMemo(() => {
- const allProducts = Object.entries(productMaterials).map(([id, data]) => ({ id, ...data }));
- let production = [];
- if (estimatorMode === 'manual') {
- production = allProducts.map(product => ({
- ...product,
- units: manualQuantities[product.id] || 0,
- productProfit: product.profit * (manualQuantities[product.id] || 0),
- productRevenue: product.sellingPrice * (manualQuantities[product.id] || 0)
- }));
- } else if (estimatorMode === 'profit-maximizer') {
- const sorted = [...allProducts].sort((a, b) => b.profitMargin - a.profitMargin);
- let remainingTime = productionTime;
- for (const product of sorted) {
- if (remainingTime <= 0) break;
- const unitsToMake = Math.min(Math.floor(remainingTime / product.timeToMake), 50);
- if (unitsToMake > 0) {
- production.push({
- ...product,
- units: unitsToMake,
- productProfit: product.profit * unitsToMake,
- productRevenue: product.sellingPrice * unitsToMake
- });
- remainingTime -= unitsToMake * product.timeToMake;
- }
- }
- } else if (estimatorMode === 'premium-basic-ratio') {
- const premiumProducts = allProducts.filter(p => !p.name.includes('Basic'));
- const basicProducts = allProducts.filter(p => p.name.includes('Basic'));
- premiumProducts.forEach(product => {
- const units = Math.floor((premiumRatio / 100) * 20);
- production.push({
- ...product,
- units,
- productProfit: product.profit * units,
- productRevenue: product.sellingPrice * units
- });
- });
- basicProducts.forEach(product => {
- const units = Math.floor(((100 - premiumRatio) / 100) * 20);
- production.push({
- ...product,
- units,
- productProfit: product.profit * units,
- productRevenue: product.sellingPrice * units
- });
- });
- }
- const totalUnits = production.reduce((sum, p) => sum + p.units, 0);
- const totalProfit = production.reduce((sum, p) => sum + p.productProfit, 0);
- const totalRevenue = production.reduce((sum, p) => sum + p.productRevenue, 0);
- const timeEstimate = production.reduce((sum, p) => sum + (p.units * p.timeToMake), 0);
- return { production, totalUnits, totalProfit, totalRevenue, timeEstimate };
- }, [estimatorMode, productionTime, premiumRatio, manualQuantities, productMaterials]);
- // Master export/import
- const masterExport = () => {
- const masterData = { expenses, sales, productMaterials, materials, savedScenarios, exportedAt: new Date().toISOString() };
- const dataStr = JSON.stringify(masterData, null, 2);
- const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
- const link = document.createElement('a');
- link.setAttribute('href', dataUri);
- link.setAttribute('download', `one-beaver-master-${new Date().toISOString().split('T')[0]}.json`);
- link.click();
- };
- const masterImport = (e) => {
- const file = e.target.files[0];
- if (file) {
- const reader = new FileReader();
- reader.onload = (event) => {
- try {
- const imported = JSON.parse(event.target.result);
- if (imported.expenses) setExpenses(imported.expenses);
- if (imported.sales) setSales(imported.sales);
- if (imported.productMaterials) setProductMaterials(imported.productMaterials);
- if (imported.materials) setMaterials(imported.materials);
- if (imported.savedScenarios) setSavedScenarios(imported.savedScenarios);
- alert('All data imported successfully!');
- } catch (err) {
- alert('Error importing file.');
- }
- };
- reader.readAsText(file);
- }
- };
- // Get unique categories
- const materialTypes = ['all', ...new Set(materials.map(m => m.type))];
- const materialCategories = ['all', ...new Set(materials.map(m => m.category).filter(Boolean))];
- const productCategories = ['all', ...new Set(Object.values(productMaterials).map(p => p.category).filter(Boolean))];
- // Filtered materials
- const filteredMaterials = materials.filter(m => {
- if (materialFilter.type !== 'all' && m.type !== materialFilter.type) return false;
- if (materialFilter.category !== 'all' && m.category !== materialFilter.category) return false;
- return true;
- });
- // Filtered products
- const filteredProducts = Object.entries(productMaterials).filter(([id, product]) => {
- if (productFilter !== 'all' && product.category !== productFilter) return false;
- return true;
- });
- const products = Object.keys(productMaterials).map(id => ({ id, name: productMaterials[id].name }));
- return (
- <div className="min-h-screen bg-gray-50">
- {/* Header */}
- <div className="bg-white border-b border-gray-200">
- <div className="max-w-7xl mx-auto px-4 py-4">
- <div className="flex justify-between items-center">
- <h1 className="text-2xl font-semibold text-gray-900">One Beaver 1300 - Business Tracker Enhanced</h1>
- <div className="flex gap-2">
- <button onClick={masterExport} className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 text-sm font-semibold">
- ⬇ Master Export
- </button>
- <label className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 cursor-pointer text-sm font-semibold">
- ⬆ Master Import
- <input type="file" accept=".json" onChange={masterImport} className="hidden" />
- </label>
- </div>
- </div>
- </div>
- </div>
- {/* Navigation Tabs */}
- <div className="bg-white border-b border-gray-200">
- <div className="max-w-7xl mx-auto px-4">
- <nav className="flex space-x-8">
- {['dashboard', 'materials', 'products', 'estimator', 'sales', 'expenses', 'market-research'].map(tab => (
- <button
- key={tab}
- onClick={() => setActiveTab(tab)}
- className={`py-4 px-1 border-b-2 font-medium text-sm ${
- activeTab === tab ? 'border-amber-700 text-amber-700' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
- }`}
- >
- {tab === 'dashboard' ? '📊 Overview' : tab.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')}
- </button>
- ))}
- </nav>
- </div>
- </div>
- {/* Main Content */}
- <div className="max-w-7xl mx-auto px-4 py-6">
- {/* DASHBOARD TAB */}
- {activeTab === 'dashboard' && (
- <div className="space-y-6">
- <div className="bg-blue-50 rounded-lg border border-blue-200 p-6">
- <h2 className="text-lg font-semibold text-gray-900 mb-2">Business Dashboard</h2>
- <p className="text-sm text-gray-600">
- Comprehensive view of your business metrics, product performance, and financial health.
- </p>
- </div>
- <div className="grid grid-cols-3 gap-4">
- <div className="bg-white rounded-lg border-l-4 border-blue-500 p-6">
- <div className="text-sm text-gray-600 mb-1">Total Materials</div>
- <div className="text-3xl font-semibold text-blue-600">{dashboardStats.totalMaterials}</div>
- </div>
- <div className="bg-white rounded-lg border-l-4 border-red-500 p-6">
- <div className="text-sm text-gray-600 mb-1">Total Products</div>
- <div className="text-3xl font-semibold text-red-600">{dashboardStats.totalProducts}</div>
- </div>
- <div className="bg-white rounded-lg border-l-4 border-green-500 p-6">
- <div className="text-sm text-gray-600 mb-1">Avg Product Margin</div>
- <div className="text-3xl font-semibold text-green-600">{dashboardStats.avgMargin.toFixed(1)}%</div>
- </div>
- </div>
- <div className="grid grid-cols-3 gap-4">
- <div className="bg-green-50 rounded-lg border border-green-200 p-6">
- <div className="text-sm text-gray-600 mb-1">Total Revenue</div>
- <div className="text-3xl font-semibold text-green-600">${dashboardStats.totalRevenue.toFixed(2)}</div>
- </div>
- <div className="bg-yellow-50 rounded-lg border border-yellow-200 p-6">
- <div className="text-sm text-gray-600 mb-1">Total Expenses</div>
- <div className="text-3xl font-semibold text-yellow-600">${dashboardStats.totalExpenses.toFixed(2)}</div>
- </div>
- <div className="bg-blue-50 rounded-lg border border-blue-200 p-6">
- <div className="text-sm text-gray-600 mb-1">Net Profit</div>
- <div className={`text-3xl font-semibold ${dashboardStats.netProfit >= 0 ? 'text-blue-600' : 'text-red-600'}`}>
- ${dashboardStats.netProfit.toFixed(2)}
- </div>
- </div>
- </div>
- {/* Charts */}
- <div className="grid grid-cols-2 gap-4">
- <div className="bg-white rounded-lg border border-gray-200 p-6">
- <div style={{height: '300px'}}>
- <canvas ref={salesChartRef}></canvas>
- </div>
- </div>
- <div className="bg-white rounded-lg border border-gray-200 p-6">
- <div style={{height: '300px'}}>
- <canvas ref={expenseChartRef}></canvas>
- </div>
- </div>
- </div>
- <div className="grid grid-cols-2 gap-4">
- <div className="bg-white rounded-lg border border-gray-200 p-6">
- <div style={{height: '300px'}}>
- <canvas ref={marginChartRef}></canvas>
- </div>
- </div>
- <div className="bg-white rounded-lg border border-gray-200 p-6">
- <div style={{height: '300px'}}>
- <canvas ref={materialCostChartRef}></canvas>
- </div>
- </div>
- </div>
- {/* Top Performers */}
- <div className="grid grid-cols-2 gap-4">
- <div className="bg-white rounded-lg border border-gray-200 p-6">
- <h3 className="text-lg font-semibold text-gray-900 mb-4">Top Products by Margin</h3>
- <div className="space-y-3">
- {dashboardStats.topProductsByMargin.map(([id, product], idx) => (
- <div key={id} className="flex justify-between items-center p-3 bg-green-50 rounded">
- <div>
- <div className="font-semibold text-gray-900">{idx + 1}. {product.name}</div>
- <div className="text-sm text-gray-600">{product.size}</div>
- </div>
- <div className="text-xl font-semibold text-green-600">
- {product.profitMargin.toFixed(1)}%
- </div>
- </div>
- ))}
- </div>
- </div>
- <div className="bg-white rounded-lg border border-gray-200 p-6">
- <h3 className="text-lg font-semibold text-gray-900 mb-4">Top Products by Revenue</h3>
- <div className="space-y-3">
- {dashboardStats.topProductsByRevenue.length > 0 ? (
- dashboardStats.topProductsByRevenue.map(([name, data], idx) => (
- <div key={name} className="flex justify-between items-center p-3 bg-blue-50 rounded">
- <div>
- <div className="font-semibold text-gray-900">{idx + 1}. {name}</div>
- <div className="text-sm text-gray-600">{data.units} units sold</div>
- </div>
- <div className="text-xl font-semibold text-blue-600">
- ${data.revenue.toFixed(2)}
- </div>
- </div>
- ))
- ) : (
- <div className="text-center text-gray-500 py-4">No sales recorded yet</div>
- )}
- </div>
- </div>
- </div>
- {/* Quick Stats */}
- <div className="bg-white rounded-lg border border-gray-200 p-6">
- <h3 className="text-lg font-semibold text-gray-900 mb-4">Quick Stats</h3>
- <div className="grid grid-cols-4 gap-4 text-sm">
- <div className="flex justify-between p-3 bg-gray-50 rounded">
- <span className="text-gray-600">Total Sales:</span>
- <span className="font-semibold">{sales.length}</span>
- </div>
- <div className="flex justify-between p-3 bg-gray-50 rounded">
- <span className="text-gray-600">Total Expenses:</span>
- <span className="font-semibold">{expenses.length}</span>
- </div>
- <div className="flex justify-between p-3 bg-gray-50 rounded">
- <span className="text-gray-600">Avg Sale Value:</span>
- <span className="font-semibold">
- ${sales.length > 0 ? (sales.reduce((sum, s) => sum + s.total, 0) / sales.length).toFixed(2) : '0.00'}
- </span>
- </div>
- <div className="flex justify-between p-3 bg-gray-50 rounded">
- <span className="text-gray-600">Most Expensive Material:</span>
- <span className="font-semibold">
- {materials.length > 0 ? materials.sort((a, b) => b.cpp - a.cpp)[0].item.substring(0, 15) + '...' : 'N/A'}
- </span>
- </div>
- </div>
- </div>
- </div>
- )}
- {/* MATERIALS TAB */}
- {activeTab === 'materials' && (
- <div className="space-y-6">
- <div className="bg-blue-50 rounded-lg border border-blue-200 p-4">
- <p className="text-sm text-gray-700">
- <span className="font-semibold">How it works:</span> Materials are the building blocks. Add materials here with their costs, and they'll be available to assign to products. CPP (Cost Per Portion) is auto-calculated. Drag rows to reorder.
- </p>
- </div>
- <div className="flex gap-3 items-center flex-wrap">
- <button
- onClick={() => setShowAddMaterial(true)}
- className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 font-semibold"
- >
- ➕ Add New Material
- </button>
- <button
- onClick={() => {
- const dataStr = JSON.stringify(materials, null, 2);
- const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
- const link = document.createElement('a');
- link.setAttribute('href', dataUri);
- link.setAttribute('download', `materials-${new Date().toISOString().split('T')[0]}.json`);
- link.click();
- }}
- className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
- >
- Export Materials
- </button>
- <div className="flex gap-2 ml-auto">
- <select
- value={materialFilter.type}
- onChange={(e) => setMaterialFilter({...materialFilter, type: e.target.value})}
- className="px-3 py-2 border border-gray-300 rounded-md text-sm"
- >
- <option value="all">All Types</option>
- {materialTypes.filter(t => t !== 'all').map(type => (
- <option key={type} value={type}>{type}</option>
- ))}
- </select>
- <select
- value={materialFilter.category}
- onChange={(e) => setMaterialFilter({...materialFilter, category: e.target.value})}
- className="px-3 py-2 border border-gray-300 rounded-md text-sm"
- >
- <option value="all">All Categories</option>
- {materialCategories.filter(c => c !== 'all').map(cat => (
- <option key={cat} value={cat}>{cat}</option>
- ))}
- </select>
- </div>
- </div>
- {showAddMaterial && (
- <div className="bg-white rounded-lg border border-gray-200 p-6">
- <h3 className="text-lg font-semibold text-gray-900 mb-4">Add New Material</h3>
- <div className="grid grid-cols-4 gap-4 mb-4">
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">ID *</label>
- <input
- type="text"
- value={newMaterial.id}
- onChange={(e) => setNewMaterial({...newMaterial, id: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- placeholder="1001"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Item *</label>
- <input
- type="text"
- value={newMaterial.item}
- onChange={(e) => setNewMaterial({...newMaterial, item: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Type</label>
- <select
- value={newMaterial.type}
- onChange={(e) => setNewMaterial({...newMaterial, type: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- >
- <option>Paper</option>
- <option>Cardstock</option>
- <option>Glue</option>
- <option>Cover</option>
- <option>Ribbon</option>
- <option>Thread</option>
- <option>Cloth</option>
- </select>
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Category</label>
- <input
- type="text"
- value={newMaterial.category}
- onChange={(e) => setNewMaterial({...newMaterial, category: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- placeholder="Core Materials"
- />
- </div>
- </div>
- <div className="grid grid-cols-4 gap-4 mb-4">
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Supplier</label>
- <input
- type="text"
- value={newMaterial.supplier}
- onChange={(e) => setNewMaterial({...newMaterial, supplier: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Item Size</label>
- <input
- type="text"
- value={newMaterial.itemSize}
- onChange={(e) => setNewMaterial({...newMaterial, itemSize: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Grade</label>
- <input
- type="text"
- value={newMaterial.grade}
- onChange={(e) => setNewMaterial({...newMaterial, grade: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Pack Count *</label>
- <input
- type="number"
- value={newMaterial.packCount}
- onChange={(e) => setNewMaterial({...newMaterial, packCount: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- />
- </div>
- </div>
- <div className="grid grid-cols-4 gap-4 mb-4">
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Units</label>
- <input
- type="text"
- value={newMaterial.units}
- onChange={(e) => setNewMaterial({...newMaterial, units: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- placeholder="Sheets"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Pack Cost</label>
- <input
- type="number"
- step="0.01"
- value={newMaterial.packCost}
- onChange={(e) => setNewMaterial({...newMaterial, packCost: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Pack Cost After Tax *</label>
- <input
- type="number"
- step="0.01"
- value={newMaterial.packCostAfterTax}
- onChange={(e) => setNewMaterial({...newMaterial, packCostAfterTax: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Source</label>
- <input
- type="text"
- value={newMaterial.source}
- onChange={(e) => setNewMaterial({...newMaterial, source: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- />
- </div>
- </div>
- <div className="flex gap-3">
- <button
- onClick={addMaterial}
- className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700"
- >
- Add Material
- </button>
- <button
- onClick={() => setShowAddMaterial(false)}
- className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400"
- >
- Cancel
- </button>
- </div>
- </div>
- )}
- <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
- <div className="overflow-x-auto">
- <table className="w-full text-sm" ref={materialsTableRef}>
- <thead className="bg-gray-50">
- <tr>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Drag</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Item</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Category</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Supplier</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Item Size</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Grade</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Pack Count</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Units</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Pack Cost After Tax</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">CPP</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
- </tr>
- </thead>
- <tbody className="divide-y divide-gray-200">
- {filteredMaterials.map(material => (
- <tr key={material.id} className="hover:bg-gray-50">
- <td className="px-4 py-3">
- <span className="drag-handle cursor-move text-gray-400 hover:text-gray-600 text-lg">☰</span>
- </td>
- <td className="px-4 py-3">
- <span className={`px-2 py-1 rounded text-xs font-semibold ${
- material.type === 'Paper' ? 'bg-blue-100 text-blue-800' :
- material.type === 'Cardstock' ? 'bg-cyan-100 text-cyan-800' :
- material.type === 'Glue' ? 'bg-teal-100 text-teal-800' :
- material.type === 'Cover' ? 'bg-yellow-100 text-yellow-800' :
- material.type === 'Ribbon' ? 'bg-purple-100 text-purple-800' :
- material.type === 'Thread' ? 'bg-green-100 text-green-800' :
- 'bg-gray-100 text-gray-800'
- }`}>
- {material.id}
- </span>
- </td>
- <td className="px-4 py-3 font-semibold">{material.item}</td>
- <td className="px-4 py-3 text-gray-600">{material.category || '-'}</td>
- <td className="px-4 py-3">{material.supplier}</td>
- <td className="px-4 py-3">{material.itemSize}</td>
- <td className="px-4 py-3">{material.grade}</td>
- <td className="px-4 py-3">{material.packCount}</td>
- <td className="px-4 py-3">{material.units}</td>
- <td className="px-4 py-3">${material.packCostAfterTax.toFixed(2)}</td>
- <td className="px-4 py-3 font-mono font-semibold">${material.cpp.toFixed(3)}</td>
- <td className="px-4 py-3">
- <div className="flex gap-2">
- <button
- onClick={() => deleteMaterial(material.id)}
- className="px-3 py-1 bg-red-600 text-white rounded text-xs hover:bg-red-700"
- >
- Delete
- </button>
- </div>
- </td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- </div>
- </div>
- )}
- {/* PRODUCTS TAB */}
- {activeTab === 'products' && (
- <div className="space-y-6">
- <div className="bg-blue-50 rounded-lg border border-blue-200 p-4">
- <p className="text-sm text-gray-700">
- <span className="font-semibold">How it works:</span> Products are built from materials. Add materials to products and costs will auto-calculate. Click on fields to edit selling price, time to make, and category. Drag products to reorder.
- </p>
- </div>
- <div className="flex gap-3 items-center">
- <button
- onClick={() => setShowAddProduct(true)}
- className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 font-semibold"
- >
- ➕ Add New Product
- </button>
- <button
- onClick={() => {
- const dataStr = JSON.stringify(productMaterials, null, 2);
- const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
- const link = document.createElement('a');
- link.setAttribute('href', dataUri);
- link.setAttribute('download', `products-${new Date().toISOString().split('T')[0]}.json`);
- link.click();
- }}
- className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
- >
- Export Products
- </button>
- <div className="ml-auto">
- <select
- value={productFilter}
- onChange={(e) => setProductFilter(e.target.value)}
- className="px-3 py-2 border border-gray-300 rounded-md text-sm"
- >
- <option value="all">All Categories</option>
- {productCategories.filter(c => c !== 'all').map(cat => (
- <option key={cat} value={cat}>{cat}</option>
- ))}
- </select>
- </div>
- </div>
- {showAddProduct && (
- <div className="bg-white rounded-lg border border-gray-200 p-6">
- <h3 className="text-lg font-semibold text-gray-900 mb-4">Add New Product</h3>
- <div className="grid grid-cols-4 gap-4 mb-4">
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Product Number *</label>
- <input
- type="text"
- value={newProduct.id}
- onChange={(e) => setNewProduct({...newProduct, id: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- placeholder="PB-BB-XXX-1300-0825"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Product Name *</label>
- <input
- type="text"
- value={newProduct.name}
- onChange={(e) => setNewProduct({...newProduct, name: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- placeholder="One Beaver - Mini"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Category</label>
- <input
- type="text"
- value={newProduct.category}
- onChange={(e) => setNewProduct({...newProduct, category: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- placeholder="Premium"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Size</label>
- <input
- type="text"
- value={newProduct.size}
- onChange={(e) => setNewProduct({...newProduct, size: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- placeholder="3.5 x 5.5"
- />
- </div>
- </div>
- <div className="grid grid-cols-2 gap-4 mb-4">
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Selling Price *</label>
- <input
- type="number"
- step="0.01"
- value={newProduct.sellingPrice}
- onChange={(e) => setNewProduct({...newProduct, sellingPrice: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- placeholder="8.00"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Time to Make (hours)</label>
- <input
- type="number"
- step="0.5"
- value={newProduct.timeToMake}
- onChange={(e) => setNewProduct({...newProduct, timeToMake: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- placeholder="2.0"
- />
- </div>
- </div>
- <div className="mb-4">
- <div className="flex justify-between items-center mb-2">
- <label className="block text-sm font-medium text-gray-700">Materials</label>
- <select
- onChange={(e) => {
- if (e.target.value) {
- addMaterialToNewProduct(e.target.value);
- e.target.value = '';
- }
- }}
- className="px-3 py-1 border border-gray-300 rounded-md text-sm"
- >
- <option value="">+ Add Material</option>
- {materials.map(mat => (
- <option key={mat.id} value={mat.id}>
- {mat.id} - {mat.item} (${mat.cpp.toFixed(3)})
- </option>
- ))}
- </select>
- </div>
- {newProduct.materials.length > 0 && (
- <table className="w-full text-sm border border-gray-200">
- <thead className="bg-gray-50">
- <tr>
- <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Material</th>
- <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Quantity</th>
- <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">CPP</th>
- <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Cost</th>
- <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Actions</th>
- </tr>
- </thead>
- <tbody>
- {newProduct.materials.map((mat, idx) => (
- <tr key={idx} className="border-t">
- <td className="px-3 py-2">{mat.name}</td>
- <td className="px-3 py-2">
- <input
- type="number"
- step="0.01"
- value={mat.quantity}
- onChange={(e) => updateNewProductMaterial(idx, 'quantity', e.target.value)}
- className="w-20 px-2 py-1 border border-gray-300 rounded"
- />
- </td>
- <td className="px-3 py-2 font-mono">${mat.cpp.toFixed(3)}</td>
- <td className="px-3 py-2 font-semibold">${mat.materialCost.toFixed(3)}</td>
- <td className="px-3 py-2">
- <button
- onClick={() => removeNewProductMaterial(idx)}
- className="px-2 py-1 bg-red-600 text-white rounded text-xs hover:bg-red-700"
- >
- Remove
- </button>
- </td>
- </tr>
- ))}
- <tr className="border-t bg-gray-50 font-semibold">
- <td className="px-3 py-2" colSpan="3">Total Cost</td>
- <td className="px-3 py-2">${newProduct.materials.reduce((sum, m) => sum + m.materialCost, 0).toFixed(2)}</td>
- <td></td>
- </tr>
- </tbody>
- </table>
- )}
- </div>
- <div className="flex gap-3">
- <button
- onClick={addProduct}
- className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700"
- >
- Add Product
- </button>
- <button
- onClick={() => {
- setShowAddProduct(false);
- setNewProduct({ id: '', name: '', category: '', size: '', sellingPrice: '', timeToMake: '', materials: [] });
- }}
- className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400"
- >
- Cancel
- </button>
- </div>
- </div>
- )}
- <div className="space-y-3" ref={productsListRef}>
- {filteredProducts.map(([productId, product]) => (
- <div key={productId} className="bg-white rounded-lg border border-gray-200">
- <div
- className="px-6 py-4 bg-gray-50 border-b border-gray-200 flex justify-between items-center hover:bg-gray-100"
- >
- <div className="flex items-center gap-3">
- <span className="drag-handle cursor-move text-gray-400 hover:text-gray-600 text-lg">☰</span>
- <div className="flex-1">
- <div className="flex items-center gap-2 mb-1">
- {editingProductField[productId] === 'name' ? (
- <input
- type="text"
- value={product.name}
- onChange={(e) => updateProductField(productId, 'name', e.target.value)}
- onBlur={() => setEditingProductField({...editingProductField, [productId]: null})}
- className="text-lg font-semibold px-2 py-1 border border-blue-500 rounded"
- autoFocus
- />
- ) : (
- <h3
- className="text-lg font-semibold text-gray-900 cursor-pointer hover:text-blue-600"
- onClick={() => setEditingProductField({...editingProductField, [productId]: 'name'})}
- >
- {product.name}
- </h3>
- )}
- {editingProductField[productId] === 'category' ? (
- <input
- type="text"
- value={product.category}
- onChange={(e) => updateProductField(productId, 'category', e.target.value)}
- onBlur={() => setEditingProductField({...editingProductField, [productId]: null})}
- className="px-2 py-1 border border-blue-500 rounded text-xs"
- autoFocus
- />
- ) : (
- <span
- className={`px-2 py-1 rounded text-xs font-semibold cursor-pointer hover:ring-2 hover:ring-blue-500 ${
- product.category === 'Premium' ? 'bg-purple-100 text-purple-800' :
- product.category === 'Basic' ? 'bg-gray-100 text-gray-800' :
- 'bg-blue-100 text-blue-800'
- }`}
- onClick={() => setEditingProductField({...editingProductField, [productId]: 'category'})}
- >
- {product.category}
- </span>
- )}
- </div>
- <p className="text-sm text-gray-600">
- {product.size} | <span className="font-mono">{productId}</span> |
- {editingProductField[productId] === 'timeToMake' ? (
- <input
- type="number"
- step="0.5"
- value={product.timeToMake}
- onChange={(e) => updateProductField(productId, 'timeToMake', e.target.value)}
- onBlur={() => setEditingProductField({...editingProductField, [productId]: null})}
- className="w-16 px-1 py-0 border border-blue-500 rounded ml-1"
- autoFocus
- />
- ) : (
- <span
- className="cursor-pointer hover:text-blue-600 ml-1"
- onClick={() => setEditingProductField({...editingProductField, [productId]: 'timeToMake'})}
- >
- Time: {product.timeToMake}hrs
- </span>
- )}
- </p>
- </div>
- </div>
- <div className="flex items-center gap-6">
- <div className="text-right">
- <div className="text-sm text-gray-600">Selling Price</div>
- {editingProductField[productId] === 'sellingPrice' ? (
- <input
- type="number"
- step="0.01"
- value={product.sellingPrice}
- onChange={(e) => updateProductField(productId, 'sellingPrice', e.target.value)}
- onBlur={() => setEditingProductField({...editingProductField, [productId]: null})}
- className="text-xl font-semibold w-24 px-2 py-1 border border-blue-500 rounded"
- autoFocus
- />
- ) : (
- <div
- className="text-xl font-semibold text-gray-900 cursor-pointer hover:text-blue-600"
- onClick={() => setEditingProductField({...editingProductField, [productId]: 'sellingPrice'})}
- >
- ${product.sellingPrice.toFixed(2)}
- </div>
- )}
- </div>
- <div className="text-right">
- <div className="text-sm text-gray-600">Total Cost</div>
- <div className="text-xl font-semibold text-gray-900">${product.totalCost.toFixed(2)}</div>
- </div>
- <div className="text-right">
- <div className="text-sm text-gray-600">Profit</div>
- <div className="text-xl font-semibold text-green-600">${product.profit.toFixed(2)}</div>
- </div>
- <div className="text-right">
- <div className="text-sm text-gray-600">Margin</div>
- <div className={`text-xl font-semibold px-3 py-1 rounded ${
- product.profitMargin >= 70 ? 'bg-green-100 text-green-800' :
- product.profitMargin >= 60 ? 'bg-green-50 text-green-700' : 'bg-yellow-50 text-yellow-700'
- }`}>
- {product.profitMargin.toFixed(1)}%
- </div>
- </div>
- <div className="flex gap-2">
- <button
- onClick={() => setExpandedProducts({...expandedProducts, [productId]: !expandedProducts[productId]})}
- className="px-3 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700"
- >
- {expandedProducts[productId] ? 'Hide' : 'Materials'}
- </button>
- <select
- onChange={(e) => {
- if (e.target.value) {
- addMaterialToProduct(productId, e.target.value);
- e.target.value = '';
- }
- }}
- className="px-3 py-1 border border-green-600 bg-white text-green-700 rounded text-sm hover:bg-green-50"
- >
- <option value="">➕ Material</option>
- {materials.filter(mat => !product.materials.some(pm => pm.id === mat.id)).map(mat => (
- <option key={mat.id} value={mat.id}>
- {mat.id} - {mat.item} (${mat.cpp.toFixed(3)})
- </option>
- ))}
- </select>
- <button
- onClick={() => deleteProduct(productId)}
- className="px-3 py-1 bg-red-600 text-white rounded text-sm hover:bg-red-700"
- >
- Delete
- </button>
- </div>
- </div>
- </div>
- {expandedProducts[productId] && (
- <div className="p-6">
- <h4 className="text-md font-semibold text-gray-900 mb-3">Materials Breakdown</h4>
- <table className="w-full text-sm">
- <thead className="bg-gray-50">
- <tr>
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Material ID</th>
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Material</th>
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Quantity</th>
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">CPP</th>
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Material Cost</th>
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">% of Total</th>
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
- </tr>
- </thead>
- <tbody className="divide-y divide-gray-200">
- {product.materials.map((material, idx) => (
- <tr key={idx}>
- <td className="px-4 py-2 font-mono text-xs">{material.id}</td>
- <td className="px-4 py-2">{material.name}</td>
- <td className="px-4 py-2">
- <input
- type="number"
- step="0.01"
- value={material.quantity}
- onChange={(e) => updateProductMaterial(productId, idx, 'quantity', e.target.value)}
- className="w-24 px-2 py-1 border border-gray-300 rounded"
- />
- </td>
- <td className="px-4 py-2 font-mono">${material.cpp.toFixed(3)}</td>
- <td className="px-4 py-2 font-semibold">${material.materialCost.toFixed(3)}</td>
- <td className="px-4 py-2">
- {((material.materialCost / product.totalCost) * 100).toFixed(1)}%
- </td>
- <td className="px-4 py-2">
- <button
- onClick={() => removeMaterialFromProduct(productId, idx)}
- className="px-2 py-1 bg-red-600 text-white rounded text-xs hover:bg-red-700"
- >
- Remove
- </button>
- </td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- )}
- </div>
- ))}
- </div>
- </div>
- )}
- {/* ESTIMATOR TAB */}
- {activeTab === 'estimator' && (
- <div className="space-y-6">
- <div className="bg-amber-50 rounded-lg border border-amber-200 p-6">
- <h2 className="text-lg font-semibold text-gray-900 mb-2">Production Estimator</h2>
- <p className="text-sm text-gray-600">
- Choose a strategy to calculate your optimal production mix, or manually set quantities for each product.
- </p>
- </div>
- <div className="bg-white rounded-lg border border-gray-200 p-6">
- <h3 className="text-md font-semibold text-gray-900 mb-4">Select Strategy</h3>
- <div className="grid grid-cols-3 gap-4">
- <button
- onClick={() => setEstimatorMode('manual')}
- className={`p-4 border-2 rounded-lg text-left ${
- estimatorMode === 'manual' ? 'border-amber-700 bg-amber-50' : 'border-gray-200 hover:border-gray-300'
- }`}
- >
- <div className="font-semibold text-gray-900 mb-1">Manual Quantities</div>
- <div className="text-xs text-gray-600">Set specific quantities for each product in the table below.</div>
- </button>
- <button
- onClick={() => setEstimatorMode('profit-maximizer')}
- className={`p-4 border-2 rounded-lg text-left ${
- estimatorMode === 'profit-maximizer' ? 'border-amber-700 bg-amber-50' : 'border-gray-200 hover:border-gray-300'
- }`}
- >
- <div className="font-semibold text-gray-900 mb-1">Profit Maximizer</div>
- <div className="text-xs text-gray-600">Auto-calculate optimal mix within your time budget.</div>
- </button>
- <button
- onClick={() => setEstimatorMode('premium-basic-ratio')}
- className={`p-4 border-2 rounded-lg text-left ${
- estimatorMode === 'premium-basic-ratio' ? 'border-amber-700 bg-amber-50' : 'border-gray-200 hover:border-gray-300'
- }`}
- >
- <div className="font-semibold text-gray-900 mb-1">Premium/Basic Ratio</div>
- <div className="text-xs text-gray-600">Distribute production by premium vs basic focus.</div>
- </button>
- </div>
- </div>
- {estimatorMode !== 'manual' && (
- <div className="bg-white rounded-lg border border-gray-200 p-6">
- <h3 className="text-md font-semibold text-gray-900 mb-4">Configuration</h3>
- {estimatorMode === 'profit-maximizer' && (
- <div className="mb-4">
- <label className="block text-sm font-medium text-gray-700 mb-2">
- Available Production Time: {productionTime} hours
- </label>
- <input
- type="range"
- min="1"
- max="160"
- value={productionTime}
- onChange={(e) => setProductionTime(parseInt(e.target.value))}
- className="w-full"
- />
- </div>
- )}
- {estimatorMode === 'premium-basic-ratio' && (
- <div className="mb-4">
- <label className="block text-sm font-medium text-gray-700 mb-2">
- Premium: {premiumRatio}% | Basic: {100 - premiumRatio}%
- </label>
- <input
- type="range"
- min="0"
- max="100"
- value={premiumRatio}
- onChange={(e) => setPremiumRatio(parseInt(e.target.value))}
- className="w-full"
- />
- </div>
- )}
- </div>
- )}
- <div className="bg-white rounded-lg border border-gray-200 p-6">
- <h3 className="text-md font-semibold text-gray-900 mb-4">Production Plan</h3>
- <div className="grid grid-cols-4 gap-4 mb-6 p-4 bg-green-50 rounded-lg border border-green-200">
- <div>
- <div className="text-sm text-gray-600">Total Units</div>
- <div className="text-2xl font-semibold text-gray-900">{estimatorResults.totalUnits}</div>
- </div>
- <div>
- <div className="text-sm text-gray-600">Total Revenue</div>
- <div className="text-2xl font-semibold text-blue-600">${estimatorResults.totalRevenue.toFixed(2)}</div>
- </div>
- <div>
- <div className="text-sm text-gray-600">Total Profit</div>
- <div className="text-2xl font-semibold text-green-600">${estimatorResults.totalProfit.toFixed(2)}</div>
- </div>
- <div>
- <div className="text-sm text-gray-600">Time Estimate</div>
- <div className="text-2xl font-semibold text-gray-900">{estimatorResults.timeEstimate.toFixed(1)}hrs</div>
- </div>
- </div>
- <div className="overflow-x-auto">
- <table className="w-full text-sm">
- <thead className="bg-gray-50">
- <tr>
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Product</th>
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Size</th>
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">
- {estimatorMode === 'manual' ? 'Quantity (Edit)' : 'Quantity'}
- </th>
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Unit Price</th>
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Revenue</th>
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Unit Profit</th>
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Total Profit</th>
- <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Margin</th>
- </tr>
- </thead>
- <tbody className="divide-y divide-gray-200">
- {estimatorResults.production.map((item, idx) => (
- <tr key={idx}>
- <td className="px-4 py-3 font-semibold">{item.name}</td>
- <td className="px-4 py-3">{item.size}</td>
- <td className="px-4 py-3">
- {estimatorMode === 'manual' ? (
- <input
- type="number"
- min="0"
- value={manualQuantities[item.id] || 0}
- onChange={(e) => setManualQuantities({
- ...manualQuantities,
- [item.id]: parseInt(e.target.value) || 0
- })}
- className="w-20 px-2 py-1 border border-gray-300 rounded font-semibold"
- />
- ) : (
- <span className="font-semibold">{item.units}</span>
- )}
- </td>
- <td className="px-4 py-3">${item.sellingPrice.toFixed(2)}</td>
- <td className="px-4 py-3">${item.productRevenue.toFixed(2)}</td>
- <td className="px-4 py-3 text-green-600">${item.profit.toFixed(2)}</td>
- <td className="px-4 py-3 text-green-600 font-semibold">${item.productProfit.toFixed(2)}</td>
- <td className="px-4 py-3">{item.profitMargin.toFixed(1)}%</td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- </div>
- </div>
- )}
- {/* SALES TAB */}
- {activeTab === 'sales' && (
- <div className="space-y-6">
- <div className="bg-white rounded-lg border border-gray-200 p-6">
- <h2 className="text-lg font-semibold text-gray-900 mb-4">Record New Sale</h2>
- <div className="grid grid-cols-2 gap-4 mb-4">
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Date *</label>
- <input
- type="date"
- value={salesForm.date}
- onChange={(e) => setSalesForm({...salesForm, date: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Location/Venue *</label>
- <input
- type="text"
- placeholder="e.g., Main Street Craft Market"
- value={salesForm.location}
- onChange={(e) => setSalesForm({...salesForm, location: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- />
- </div>
- </div>
- <div className="grid grid-cols-3 gap-4 mb-4">
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Product Sold *</label>
- <select
- value={salesForm.product}
- onChange={(e) => setSalesForm({...salesForm, product: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- >
- <option value="">Select product...</option>
- {products.map(prod => <option key={prod.id} value={prod.name}>{prod.name}</option>)}
- </select>
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Units Sold *</label>
- <input
- type="number"
- min="1"
- value={salesForm.units}
- onChange={(e) => setSalesForm({...salesForm, units: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Price Per Unit *</label>
- <input
- type="number"
- step="0.01"
- placeholder="Actual price sold"
- value={salesForm.pricePerUnit}
- onChange={(e) => setSalesForm({...salesForm, pricePerUnit: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- />
- </div>
- </div>
- <button
- onClick={addSale}
- className="px-6 py-2 bg-amber-800 text-white rounded-md hover:bg-amber-900"
- >
- Add Sale Record
- </button>
- </div>
- <div className="bg-blue-50 rounded-lg border border-blue-200 p-6">
- <h3 className="text-lg font-semibold text-gray-900 mb-4">Sales Summary</h3>
- <div className="grid grid-cols-3 gap-6">
- <div>
- <div className="text-sm text-gray-600">Total Sales</div>
- <div className="text-2xl font-semibold text-gray-900">{sales.length}</div>
- </div>
- <div>
- <div className="text-sm text-gray-600">Total Revenue</div>
- <div className="text-2xl font-semibold text-gray-900">${sales.reduce((sum, s) => sum + s.total, 0).toFixed(2)}</div>
- </div>
- <div>
- <div className="text-sm text-gray-600">Avg Sale Value</div>
- <div className="text-2xl font-semibold text-gray-900">
- ${sales.length > 0 ? (sales.reduce((sum, s) => sum + s.total, 0) / sales.length).toFixed(2) : '0.00'}
- </div>
- </div>
- </div>
- </div>
- {sales.length > 0 && (
- <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
- <div className="overflow-x-auto">
- <table className="w-full text-sm">
- <thead className="bg-gray-50">
- <tr>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Date</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Location</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Product</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Units</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Price/Unit</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Total</th>
- </tr>
- </thead>
- <tbody className="divide-y divide-gray-200">
- {sales.map(sale => (
- <tr key={sale.id}>
- <td className="px-4 py-3">{sale.date}</td>
- <td className="px-4 py-3">{sale.location}</td>
- <td className="px-4 py-3 font-semibold">{sale.product}</td>
- <td className="px-4 py-3">{sale.units}</td>
- <td className="px-4 py-3">${sale.pricePerUnit.toFixed(2)}</td>
- <td className="px-4 py-3 font-semibold">${sale.total.toFixed(2)}</td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- </div>
- )}
- </div>
- )}
- {/* EXPENSES TAB */}
- {activeTab === 'expenses' && (
- <div className="space-y-6">
- <div className="bg-white rounded-lg border border-gray-200 p-6">
- <h2 className="text-lg font-semibold text-gray-900 mb-4">Record New Expense</h2>
- <div className="grid grid-cols-2 gap-4 mb-4">
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Date *</label>
- <input
- type="date"
- value={expenseForm.date}
- onChange={(e) => setExpenseForm({...expenseForm, date: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Supplier/Store *</label>
- <input
- type="text"
- placeholder="e.g., Staples, Amazon"
- value={expenseForm.supplier}
- onChange={(e) => setExpenseForm({...expenseForm, supplier: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- />
- </div>
- </div>
- <div className="grid grid-cols-4 gap-4 mb-4">
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Category *</label>
- <select
- value={expenseForm.category}
- onChange={(e) => setExpenseForm({...expenseForm, category: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- >
- <option value="">Select...</option>
- {expenseCategories.map(cat => <option key={cat} value={cat}>{cat}</option>)}
- </select>
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Item Description *</label>
- <input
- type="text"
- value={expenseForm.itemDescription}
- onChange={(e) => setExpenseForm({...expenseForm, itemDescription: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Cost *</label>
- <input
- type="number"
- step="0.01"
- value={expenseForm.cost}
- onChange={(e) => setExpenseForm({...expenseForm, cost: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- />
- </div>
- <div>
- <label className="block text-sm font-medium text-gray-700 mb-1">Material ID</label>
- <select
- value={expenseForm.materialId}
- onChange={(e) => setExpenseForm({...expenseForm, materialId: e.target.value})}
- className="w-full px-3 py-2 border border-gray-300 rounded-md"
- >
- <option value="">Optional</option>
- {materials.map(mat => <option key={mat.id} value={mat.id}>{mat.id} - {mat.item}</option>)}
- </select>
- </div>
- </div>
- <button
- onClick={addExpense}
- className="px-6 py-2 bg-amber-800 text-white rounded-md hover:bg-amber-900"
- >
- Add Expense
- </button>
- </div>
- <div className="bg-green-50 rounded-lg border border-green-200 p-6">
- <h3 className="text-lg font-semibold text-gray-900 mb-4">Expense Summary</h3>
- <div className="grid grid-cols-3 gap-6">
- <div>
- <div className="text-sm text-gray-600">Total Expenses</div>
- <div className="text-2xl font-semibold text-gray-900">${expenses.reduce((sum, e) => sum + e.cost, 0).toFixed(2)}</div>
- </div>
- <div>
- <div className="text-sm text-gray-600">Total Transactions</div>
- <div className="text-2xl font-semibold text-gray-900">{expenses.length}</div>
- </div>
- <div>
- <div className="text-sm text-gray-600">Avg Transaction</div>
- <div className="text-2xl font-semibold text-gray-900">
- ${expenses.length > 0 ? (expenses.reduce((sum, e) => sum + e.cost, 0) / expenses.length).toFixed(2) : '0.00'}
- </div>
- </div>
- </div>
- </div>
- {expenses.length > 0 && (
- <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
- <div className="overflow-x-auto">
- <table className="w-full text-sm">
- <thead className="bg-gray-50">
- <tr>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Date</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Supplier</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Category</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Description</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Cost</th>
- <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Material ID</th>
- </tr>
- </thead>
- <tbody className="divide-y divide-gray-200">
- {expenses.map(expense => (
- <tr key={expense.id}>
- <td className="px-4 py-3">{expense.date}</td>
- <td className="px-4 py-3">{expense.supplier}</td>
- <td className="px-4 py-3">{expense.category}</td>
- <td className="px-4 py-3">{expense.itemDescription}</td>
- <td className="px-4 py-3 font-semibold">${expense.cost.toFixed(2)}</td>
- <td className="px-4 py-3 font-mono text-xs">{expense.materialId || '-'}</td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- </div>
- )}
- </div>
- )}
- {/* MARKET RESEARCH TAB */}
- {activeTab === 'market-research' && (
- <div className="space-y-6">
- <div className="bg-purple-50 rounded-lg border border-purple-200 p-6">
- <h2 className="text-lg font-semibold text-gray-900 mb-2">Market Research</h2>
- <p className="text-sm text-gray-600">Industry analysis based on major retailers.</p>
- </div>
- <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
- <table className="w-full text-sm">
- <thead className="bg-gray-50">
- <tr>
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Size Category</th>
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Your Product</th>
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Your Price</th>
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Competitor Avg</th>
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Price Position</th>
- <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Suggestion</th>
- </tr>
- </thead>
- <tbody className="divide-y divide-gray-200">
- {MARKET_DATA.map((item, idx) => (
- <tr key={idx}>
- <td className="px-6 py-4">{item.sizeCategory}</td>
- <td className="px-6 py-4 font-semibold">{item.yourProduct}</td>
- <td className="px-6 py-4 text-green-600 font-semibold">${item.yourPrice.toFixed(2)}</td>
- <td className="px-6 py-4">${item.competitorAvg.toFixed(2)}</td>
- <td className="px-6 py-4">
- <span className="px-2 py-1 bg-green-100 text-green-800 rounded text-xs font-semibold">
- {item.pricePosition}
- </span>
- </td>
- <td className="px-6 py-4">{item.suggestion}</td>
- </tr>
- ))}
- </tbody>
- </table>
- </div>
- </div>
- )}
- </div>
- </div>
- );
- }
- ReactDOM.render(<BusinessTracker />, document.getElementById('root'));
- </script>
- </body>
- </html>
Add Comment
Please, Sign In to add comment