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 Final</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>
- </head>
- <body>
- <div id="root"></div>
- <script type="text/babel">
- const { useState, useMemo, useEffect, useRef } = React;
- // Initial materials data from image 1
- const INITIAL_MATERIALS = [
- { id: '1001', item: 'Pastel Sheets', type: 'Paper', 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', 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', 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', 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', 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', 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', 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', 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', 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', 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',
- 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',
- 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',
- 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',
- 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',
- 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',
- 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',
- 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 [newMaterial, setNewMaterial] = useState({
- id: '', item: '', type: 'Paper', supplier: '', itemSize: '', grade: '',
- packCount: '', units: '', packCost: '', packCostAfterTax: '', cpp: '', source: '-'
- });
- // Products State
- const [productMaterials, setProductMaterials] = useState(INITIAL_PRODUCT_MATERIALS);
- const [editingProduct, setEditingProduct] = useState(null);
- const [showAddProduct, setShowAddProduct] = useState(false);
- const [newProduct, setNewProduct] = useState({
- id: '', name: '', 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 expenseTableRef = useRef(null);
- const salesTableRef = useRef(null);
- const expenseCategories = [
- 'Materials - Paper', 'Materials - Cardstock', 'Materials - Glue', 'Materials - Cover',
- 'Tools & Equipment', 'Packaging', 'Shipping', 'Marketing', 'Booth Fees', 'Other'
- ];
- // 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;
- return { totalMaterials, totalProducts, avgMargin, totalRevenue, totalExpenses, netProfit };
- }, [materials, productMaterials, sales, expenses]);
- // 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', 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));
- }
- };
- // 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,
- size: newProduct.size,
- sellingPrice: parseFloat(newProduct.sellingPrice),
- totalCost,
- profit,
- profitMargin,
- timeToMake: parseFloat(newProduct.timeToMake) || 2,
- toMake1k,
- materials: newProduct.materials
- }
- });
- setNewProduct({ id: '', name: '', 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/sale functions (same as before)
- 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: '' });
- };
- 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 with toggleable quantities
- 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);
- }
- };
- 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</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">
- Master Data System: Start by setting up your <span className="font-semibold">Materials Library</span> and <span className="font-semibold">Products</span>.
- All other tabs will automatically pull data from these master sources.
- </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 text-blue-600">${dashboardStats.netProfit.toFixed(2)}</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">Quick Stats</h3>
- <div className="grid grid-cols-2 gap-4 text-sm">
- <div className="flex justify-between">
- <span className="text-gray-600">Total Sales Recorded:</span>
- <span className="font-semibold">{sales.length}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-gray-600">Total Expenses Recorded:</span>
- <span className="font-semibold">{expenses.length}</span>
- </div>
- <div className="flex justify-between">
- <span className="text-gray-600">Highest Margin Product:</span>
- <span className="font-semibold">
- {Object.values(productMaterials).sort((a, b) => b.profitMargin - a.profitMargin)[0]?.name || 'N/A'}
- </span>
- </div>
- <div className="flex justify-between">
- <span className="text-gray-600">Most Expensive Material:</span>
- <span className="font-semibold">
- {materials.sort((a, b) => b.cpp - a.cpp)[0]?.item || '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.
- </p>
- </div>
- <div className="flex gap-3">
- <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>
- {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">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>
- <div className="grid grid-cols-4 gap-4 mb-4">
- <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>
- <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>
- <div className="grid grid-cols-3 gap-4 mb-4">
- <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">
- <thead className="bg-gray-50">
- <tr>
- <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">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</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">Source</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">
- {materials.map(material => (
- <tr key={material.id} className="hover:bg-gray-50">
- <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">{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.packCost.toFixed(2)}</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">{material.source}</td>
- <td className="px-4 py-3">
- <div className="flex gap-2">
- <button
- onClick={() => setEditingMaterial(material.id)}
- className="px-3 py-1 bg-blue-600 text-white rounded text-xs hover:bg-blue-700"
- >
- Edit
- </button>
- <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. Profit = Selling Price - Total Cost.
- </p>
- </div>
- <div className="flex gap-3">
- <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>
- {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-3 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">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: '', 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">
- {Object.entries(productMaterials).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 cursor-pointer hover:bg-gray-100"
- onClick={() => setExpandedProducts({...expandedProducts, [productId]: !expandedProducts[productId]})}
- >
- <div className="flex items-center gap-3">
- <span className="text-lg">{expandedProducts[productId] ? '▼' : '▶'}</span>
- <div>
- <h3 className="text-lg font-semibold text-gray-900">{product.name}</h3>
- <p className="text-sm text-gray-600">
- {product.size} | <span className="font-mono">{productId}</span> | Time to Make: {product.timeToMake}hrs
- </p>
- </div>
- </div>
- <div className="flex items-center gap-6">
- <div className="text-right">
- <div className="text-sm text-gray-600">Selling Price</div>
- <div className="text-xl font-semibold text-gray-900">${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">
- <select
- onChange={(e) => {
- e.stopPropagation();
- 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"
- onClick={(e) => e.stopPropagation()}
- >
- <option value="">➕ Add 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={(e) => {
- e.stopPropagation();
- 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 - keeping from previous version */}
- {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>
- </div>
- )}
- {/* EXPENSES TAB - keeping from previous version */}
- {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>
- </div>
- )}
- {/* MARKET RESEARCH TAB - keeping from previous version */}
- {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