nickmf30

Untitled

Oct 26th, 2025
12
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 148.37 KB | None | 0 0
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>Business Tracker - One Beaver 1300 Enhanced</title>
  7. <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
  8. <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
  9. <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  10. <script src="https://cdn.tailwindcss.com"></script>
  11. <script src="https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.js"></script>
  12. <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
  13. </head>
  14. <body>
  15. <div id="root"></div>
  16. <script type="text/babel">
  17. const { useState, useMemo, useEffect, useRef } = React;
  18.  
  19. // Initial materials data
  20. const INITIAL_MATERIALS = [
  21. { 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: '-' },
  22. { 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: '-' },
  23. { 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: '-' },
  24. { 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: '-' },
  25. { 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: '-' },
  26. { 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: '-' },
  27. { 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: '-' },
  28. { 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: '-' },
  29. { 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: '-' },
  30. { 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: '-' }
  31. ];
  32.  
  33. // Initial product materials data
  34. const INITIAL_PRODUCT_MATERIALS = {
  35. 'PB-BB-BVR1M-1300-0825': {
  36. name: 'One Beaver - 1300 - Mini',
  37. category: 'Premium',
  38. size: '3.5 x 5.5',
  39. sellingPrice: 8.00,
  40. totalCost: 1.813,
  41. profit: 6.187,
  42. profitMargin: 77.3,
  43. timeToMake: 1.5,
  44. toMake1k: 162,
  45. materials: [
  46. { id: '1001', name: 'Pastel Sheets', quantity: 15.00, cpp: 0.043, materialCost: 0.638, type: 'Paper' },
  47. { id: '2001', name: 'Cream Cardstock Sheet', quantity: 1.00, cpp: 0.122, materialCost: 0.122, type: 'Cardstock' },
  48. { id: '2003', name: 'Cream Cardstock Sheets', quantity: 0.50, cpp: 0.246, materialCost: 0.123, type: 'Cardstock' },
  49. { id: '3001', name: 'Lineco PVA Glue', quantity: 0.30, cpp: 1.837, materialCost: 0.551, type: 'Glue' },
  50. { id: '3002', name: 'Glue Strips', quantity: 0.25, cpp: 0.224, materialCost: 0.056, type: 'Glue' },
  51. { id: '4001', name: 'Light Brown PUV Material', quantity: 38.50, cpp: 0.008, materialCost: 0.323, type: 'Cover' }
  52. ]
  53. },
  54. 'PB-BB-BVR1K-1300-0825': {
  55. name: 'One Beaver - 1300 - King',
  56. category: 'Premium',
  57. size: '8.5 x 11',
  58. sellingPrice: 25.00,
  59. totalCost: 11.478,
  60. profit: 13.522,
  61. profitMargin: 54.1,
  62. timeToMake: 3.5,
  63. toMake1k: 74,
  64. materials: [
  65. { id: '1001', name: 'Pastel Sheets', quantity: 100.00, cpp: 0.043, materialCost: 4.250, type: 'Paper' },
  66. { id: '2001', name: 'Cream Cardstock Sheet', quantity: 4.00, cpp: 0.122, materialCost: 0.489, type: 'Cardstock' },
  67. { id: '2002', name: 'Cream Cardstock Sheets', quantity: 4.00, cpp: 0.269, materialCost: 1.074, type: 'Cardstock' },
  68. { id: '2003', name: 'Cream Cardstock Sheets', quantity: 2.50, cpp: 0.246, materialCost: 0.616, type: 'Cardstock' },
  69. { id: '3001', name: 'Lineco PVA Glue', quantity: 2.00, cpp: 1.837, materialCost: 3.674, type: 'Glue' },
  70. { id: '3002', name: 'Glue Strips', quantity: 1.00, cpp: 0.224, materialCost: 0.224, type: 'Glue' },
  71. { id: '4001', name: 'Light Brown PUV Material', quantity: 137.00, cpp: 0.008, materialCost: 1.151, type: 'Cover' }
  72. ]
  73. },
  74. 'PB-BB-BVR1-1300-0825': {
  75. name: 'One Beaver - 1300',
  76. category: 'Premium',
  77. size: '7 x 10',
  78. sellingPrice: 20.00,
  79. totalCost: 8.377,
  80. profit: 11.623,
  81. profitMargin: 58.1,
  82. timeToMake: 2.5,
  83. toMake1k: 86,
  84. materials: [
  85. { id: '1001', name: 'Pastel Sheets', quantity: 80.00, cpp: 0.043, materialCost: 3.400, type: 'Paper' },
  86. { id: '2001', name: 'Cream Cardstock Sheet', quantity: 3.00, cpp: 0.122, materialCost: 0.367, type: 'Cardstock' },
  87. { id: '2002', name: 'Cream Cardstock Sheets', quantity: 3.00, cpp: 0.269, materialCost: 0.806, type: 'Cardstock' },
  88. { id: '2003', name: 'Cream Cardstock Sheets', quantity: 2.50, cpp: 0.246, materialCost: 0.616, type: 'Cardstock' },
  89. { id: '3001', name: 'Lineco PVA Glue', quantity: 1.12, cpp: 1.837, materialCost: 2.058, type: 'Glue' },
  90. { id: '3002', name: 'Glue Strips', quantity: 1.00, cpp: 0.224, materialCost: 0.224, type: 'Glue' },
  91. { id: '4001', name: 'Light Brown PUV Material', quantity: 108.00, cpp: 0.008, materialCost: 0.907, type: 'Cover' }
  92. ]
  93. },
  94. 'PB-BB-BVR1C-1300-0825': {
  95. name: 'One Beaver - 1300 - Companion',
  96. category: 'Premium',
  97. size: '4.25 x 7.25',
  98. sellingPrice: 13.00,
  99. totalCost: 4.609,
  100. profit: 8.391,
  101. profitMargin: 64.5,
  102. timeToMake: 2.0,
  103. toMake1k: 119,
  104. materials: [
  105. { id: '1001', name: 'Pastel Sheets', quantity: 40.00, cpp: 0.043, materialCost: 1.700, type: 'Paper' },
  106. { id: '2001', name: 'Cream Cardstock Sheet', quantity: 1.50, cpp: 0.122, materialCost: 0.183, type: 'Cardstock' },
  107. { id: '2002', name: 'Cream Cardstock Sheets', quantity: 2.00, cpp: 0.269, materialCost: 0.537, type: 'Cardstock' },
  108. { id: '2003', name: 'Cream Cardstock Sheets', quantity: 1.00, cpp: 0.246, materialCost: 0.246, type: 'Cardstock' },
  109. { id: '3001', name: 'Lineco PVA Glue', quantity: 0.60, cpp: 1.837, materialCost: 1.102, type: 'Glue' },
  110. { id: '3002', name: 'Glue Strips', quantity: 0.75, cpp: 0.224, materialCost: 0.168, type: 'Glue' },
  111. { id: '4001', name: 'Light Brown PUV Material', quantity: 80.00, cpp: 0.008, materialCost: 0.672, type: 'Cover' }
  112. ]
  113. },
  114. 'PB-BB-BVR1MB-1300-0825': {
  115. name: 'One Beaver - 1300 - Mini - Basic',
  116. category: 'Basic',
  117. size: '3.5 x 5.5',
  118. sellingPrice: 5.00,
  119. totalCost: 1.673,
  120. profit: 3.327,
  121. profitMargin: 66.5,
  122. timeToMake: 1.0,
  123. toMake1k: 301,
  124. materials: [
  125. { id: '1001', name: 'Pastel Sheets', quantity: 15.00, cpp: 0.043, materialCost: 0.638, type: 'Paper' },
  126. { id: '2001', name: 'Cream Cardstock Sheet', quantity: 0.50, cpp: 0.122, materialCost: 0.061, type: 'Cardstock' },
  127. { id: '3001', name: 'Lineco PVA Glue', quantity: 0.50, cpp: 1.837, materialCost: 0.919, type: 'Glue' },
  128. { id: '3002', name: 'Glue Strips', quantity: 0.25, cpp: 0.224, materialCost: 0.056, type: 'Glue' }
  129. ]
  130. },
  131. 'PB-BB-BVR1CB-1300-0825': {
  132. name: 'One Beaver - 1300 - Companion - Basic',
  133. category: 'Basic',
  134. size: '4.25 x 7.25',
  135. sellingPrice: 9.00,
  136. totalCost: 2.943,
  137. profit: 6.057,
  138. profitMargin: 67.3,
  139. timeToMake: 1.5,
  140. toMake1k: 165,
  141. materials: [
  142. { id: '1001', name: 'Pastel Sheets', quantity: 30.00, cpp: 0.043, materialCost: 1.275, type: 'Paper' },
  143. { id: '2001', name: 'Cream Cardstock Sheet', quantity: 1.00, cpp: 0.122, materialCost: 0.122, type: 'Cardstock' },
  144. { id: '3001', name: 'Lineco PVA Glue', quantity: 0.75, cpp: 1.837, materialCost: 1.378, type: 'Glue' },
  145. { id: '3002', name: 'Glue Strips', quantity: 0.75, cpp: 0.224, materialCost: 0.168, type: 'Glue' }
  146. ]
  147. },
  148. 'PB-BB-BVR1B-1300-0825': {
  149. name: 'One Beaver - 1300 - Basic',
  150. category: 'Basic',
  151. size: '7 x 10',
  152. sellingPrice: 15.00,
  153. totalCost: 4.917,
  154. profit: 10.083,
  155. profitMargin: 67.2,
  156. timeToMake: 2.0,
  157. toMake1k: 99,
  158. materials: [
  159. { id: '1001', name: 'Pastel Sheets', quantity: 60.00, cpp: 0.043, materialCost: 2.550, type: 'Paper' },
  160. { id: '2001', name: 'Cream Cardstock Sheet', quantity: 2.50, cpp: 0.122, materialCost: 0.306, type: 'Cardstock' },
  161. { id: '3001', name: 'Lineco PVA Glue', quantity: 1.00, cpp: 1.837, materialCost: 1.837, type: 'Glue' },
  162. { id: '3002', name: 'Glue Strips', quantity: 1.00, cpp: 0.224, materialCost: 0.224, type: 'Glue' }
  163. ]
  164. }
  165. };
  166.  
  167. const MARKET_DATA = [
  168. { 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' },
  169. { 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' },
  170. { 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' },
  171. { 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' }
  172. ];
  173.  
  174. function BusinessTracker() {
  175. const [activeTab, setActiveTab] = useState('dashboard');
  176.  
  177. // Materials State
  178. const [materials, setMaterials] = useState(INITIAL_MATERIALS);
  179. const [editingMaterial, setEditingMaterial] = useState(null);
  180. const [showAddMaterial, setShowAddMaterial] = useState(false);
  181. const [materialFilter, setMaterialFilter] = useState({ type: 'all', category: 'all' });
  182. const [newMaterial, setNewMaterial] = useState({
  183. id: '', item: '', type: 'Paper', category: '', supplier: '', itemSize: '', grade: '',
  184. packCount: '', units: '', packCost: '', packCostAfterTax: '', cpp: '', source: '-'
  185. });
  186.  
  187. // Products State
  188. const [productMaterials, setProductMaterials] = useState(INITIAL_PRODUCT_MATERIALS);
  189. const [editingProduct, setEditingProduct] = useState(null);
  190. const [editingProductField, setEditingProductField] = useState({});
  191. const [showAddProduct, setShowAddProduct] = useState(false);
  192. const [productFilter, setProductFilter] = useState('all');
  193. const [newProduct, setNewProduct] = useState({
  194. id: '', name: '', category: '', size: '', sellingPrice: '', timeToMake: '', materials: []
  195. });
  196. const [expandedProducts, setExpandedProducts] = useState({});
  197.  
  198. // Expense State
  199. const [expenses, setExpenses] = useState([]);
  200. const [expenseForm, setExpenseForm] = useState({
  201. date: new Date().toISOString().split('T')[0],
  202. supplier: '', category: '', itemDescription: '', cost: '', materialId: '', notes: ''
  203. });
  204.  
  205. // Sales State
  206. const [sales, setSales] = useState([]);
  207. const [salesForm, setSalesForm] = useState({
  208. date: new Date().toISOString().split('T')[0],
  209. location: '', product: '', units: '1', pricePerUnit: '',
  210. customerId: '', repeatCustomer: 'No', customerNotes: '',
  211. complaint: 'No', returned: 'No', refunded: 'No'
  212. });
  213.  
  214. // Estimator State
  215. const [estimatorMode, setEstimatorMode] = useState('profit-maximizer');
  216. const [productionTime, setProductionTime] = useState(40);
  217. const [premiumRatio, setPremiumRatio] = useState(50);
  218. const [manualQuantities, setManualQuantities] = useState({});
  219. const [savedScenarios, setSavedScenarios] = useState([]);
  220.  
  221. // Drag and drop refs
  222. const materialsTableRef = useRef(null);
  223. const productsListRef = useRef(null);
  224. const expenseTableRef = useRef(null);
  225. const salesTableRef = useRef(null);
  226.  
  227. // Chart refs
  228. const salesChartRef = useRef(null);
  229. const expenseChartRef = useRef(null);
  230. const marginChartRef = useRef(null);
  231. const materialCostChartRef = useRef(null);
  232.  
  233. const expenseCategories = [
  234. 'Materials - Paper', 'Materials - Cardstock', 'Materials - Glue', 'Materials - Cover',
  235. 'Tools & Equipment', 'Packaging', 'Shipping', 'Marketing', 'Booth Fees', 'Other'
  236. ];
  237.  
  238. // Drag and Drop Setup
  239. useEffect(() => {
  240. if (materialsTableRef.current && activeTab === 'materials') {
  241. const tbody = materialsTableRef.current.querySelector('tbody');
  242. if (tbody) {
  243. new Sortable(tbody, {
  244. animation: 150,
  245. handle: '.drag-handle',
  246. onEnd: (evt) => {
  247. const newMaterials = [...materials];
  248. const [movedItem] = newMaterials.splice(evt.oldIndex, 1);
  249. newMaterials.splice(evt.newIndex, 0, movedItem);
  250. setMaterials(newMaterials);
  251. }
  252. });
  253. }
  254. }
  255. }, [activeTab, materials.length]);
  256.  
  257. useEffect(() => {
  258. if (productsListRef.current && activeTab === 'products') {
  259. new Sortable(productsListRef.current, {
  260. animation: 150,
  261. handle: '.drag-handle',
  262. onEnd: (evt) => {
  263. const productIds = Object.keys(productMaterials);
  264. const [movedId] = productIds.splice(evt.oldIndex, 1);
  265. productIds.splice(evt.newIndex, 0, movedId);
  266.  
  267. const reordered = {};
  268. productIds.forEach(id => {
  269. reordered[id] = productMaterials[id];
  270. });
  271. setProductMaterials(reordered);
  272. }
  273. });
  274. }
  275. }, [activeTab, Object.keys(productMaterials).length]);
  276.  
  277. // Dashboard Analytics
  278. const dashboardStats = useMemo(() => {
  279. const totalMaterials = materials.length;
  280. const totalProducts = Object.keys(productMaterials).length;
  281. const avgMargin = Object.values(productMaterials).reduce((sum, p) => sum + p.profitMargin, 0) / totalProducts || 0;
  282. const totalRevenue = sales.reduce((sum, sale) => sum + sale.total, 0);
  283. const totalExpenses = expenses.reduce((sum, exp) => sum + exp.cost, 0);
  284. const netProfit = totalRevenue - totalExpenses;
  285.  
  286. // Product performance
  287. const productSales = {};
  288. sales.forEach(sale => {
  289. if (!productSales[sale.product]) {
  290. productSales[sale.product] = { units: 0, revenue: 0 };
  291. }
  292. productSales[sale.product].units += sale.units;
  293. productSales[sale.product].revenue += sale.total;
  294. });
  295.  
  296. // Top performers
  297. const topProductsByMargin = Object.entries(productMaterials)
  298. .sort(([,a], [,b]) => b.profitMargin - a.profitMargin)
  299. .slice(0, 3);
  300.  
  301. const topProductsByRevenue = Object.entries(productSales)
  302. .sort(([,a], [,b]) => b.revenue - a.revenue)
  303. .slice(0, 3);
  304.  
  305. // Expense breakdown
  306. const expenseByCategory = {};
  307. expenses.forEach(exp => {
  308. if (!expenseByCategory[exp.category]) {
  309. expenseByCategory[exp.category] = 0;
  310. }
  311. expenseByCategory[exp.category] += exp.cost;
  312. });
  313.  
  314. // Material cost contribution
  315. const materialCostBreakdown = {};
  316. Object.values(productMaterials).forEach(product => {
  317. product.materials.forEach(mat => {
  318. if (!materialCostBreakdown[mat.type]) {
  319. materialCostBreakdown[mat.type] = 0;
  320. }
  321. materialCostBreakdown[mat.type] += mat.materialCost;
  322. });
  323. });
  324.  
  325. return {
  326. totalMaterials,
  327. totalProducts,
  328. avgMargin,
  329. totalRevenue,
  330. totalExpenses,
  331. netProfit,
  332. productSales,
  333. topProductsByMargin,
  334. topProductsByRevenue,
  335. expenseByCategory,
  336. materialCostBreakdown
  337. };
  338. }, [materials, productMaterials, sales, expenses]);
  339.  
  340. // Update charts when dashboard is active
  341. useEffect(() => {
  342. if (activeTab === 'dashboard' && dashboardStats.productSales) {
  343. // Sales by product chart
  344. if (salesChartRef.current) {
  345. const ctx = salesChartRef.current.getContext('2d');
  346. if (salesChartRef.current.chart) {
  347. salesChartRef.current.chart.destroy();
  348. }
  349. salesChartRef.current.chart = new Chart(ctx, {
  350. type: 'bar',
  351. data: {
  352. labels: Object.keys(dashboardStats.productSales),
  353. datasets: [{
  354. label: 'Revenue',
  355. data: Object.values(dashboardStats.productSales).map(p => p.revenue),
  356. backgroundColor: 'rgba(34, 197, 94, 0.7)',
  357. borderColor: 'rgba(34, 197, 94, 1)',
  358. borderWidth: 1
  359. }]
  360. },
  361. options: {
  362. responsive: true,
  363. maintainAspectRatio: false,
  364. plugins: {
  365. legend: { display: false },
  366. title: { display: true, text: 'Revenue by Product' }
  367. },
  368. scales: {
  369. y: { beginAtZero: true }
  370. }
  371. }
  372. });
  373. }
  374.  
  375. // Expense by category chart
  376. if (expenseChartRef.current) {
  377. const ctx = expenseChartRef.current.getContext('2d');
  378. if (expenseChartRef.current.chart) {
  379. expenseChartRef.current.chart.destroy();
  380. }
  381. expenseChartRef.current.chart = new Chart(ctx, {
  382. type: 'doughnut',
  383. data: {
  384. labels: Object.keys(dashboardStats.expenseByCategory),
  385. datasets: [{
  386. data: Object.values(dashboardStats.expenseByCategory),
  387. backgroundColor: [
  388. 'rgba(59, 130, 246, 0.7)',
  389. 'rgba(16, 185, 129, 0.7)',
  390. 'rgba(245, 158, 11, 0.7)',
  391. 'rgba(239, 68, 68, 0.7)',
  392. 'rgba(139, 92, 246, 0.7)',
  393. 'rgba(236, 72, 153, 0.7)',
  394. 'rgba(14, 165, 233, 0.7)',
  395. 'rgba(132, 204, 22, 0.7)',
  396. 'rgba(251, 146, 60, 0.7)',
  397. 'rgba(107, 114, 128, 0.7)'
  398. ]
  399. }]
  400. },
  401. options: {
  402. responsive: true,
  403. maintainAspectRatio: false,
  404. plugins: {
  405. legend: { position: 'right' },
  406. title: { display: true, text: 'Expenses by Category' }
  407. }
  408. }
  409. });
  410. }
  411.  
  412. // Margin comparison chart
  413. if (marginChartRef.current) {
  414. const ctx = marginChartRef.current.getContext('2d');
  415. if (marginChartRef.current.chart) {
  416. marginChartRef.current.chart.destroy();
  417. }
  418. const products = Object.entries(productMaterials);
  419. marginChartRef.current.chart = new Chart(ctx, {
  420. type: 'bar',
  421. data: {
  422. labels: products.map(([, p]) => p.name),
  423. datasets: [{
  424. label: 'Profit Margin %',
  425. data: products.map(([, p]) => p.profitMargin),
  426. backgroundColor: products.map(([, p]) =>
  427. p.profitMargin >= 70 ? 'rgba(34, 197, 94, 0.7)' :
  428. p.profitMargin >= 60 ? 'rgba(234, 179, 8, 0.7)' :
  429. 'rgba(239, 68, 68, 0.7)'
  430. ),
  431. borderWidth: 1
  432. }]
  433. },
  434. options: {
  435. responsive: true,
  436. maintainAspectRatio: false,
  437. plugins: {
  438. legend: { display: false },
  439. title: { display: true, text: 'Profit Margin by Product' }
  440. },
  441. scales: {
  442. y: { beginAtZero: true, max: 100 }
  443. }
  444. }
  445. });
  446. }
  447.  
  448. // Material cost breakdown
  449. if (materialCostChartRef.current) {
  450. const ctx = materialCostChartRef.current.getContext('2d');
  451. if (materialCostChartRef.current.chart) {
  452. materialCostChartRef.current.chart.destroy();
  453. }
  454. materialCostChartRef.current.chart = new Chart(ctx, {
  455. type: 'pie',
  456. data: {
  457. labels: Object.keys(dashboardStats.materialCostBreakdown),
  458. datasets: [{
  459. data: Object.values(dashboardStats.materialCostBreakdown),
  460. backgroundColor: [
  461. 'rgba(59, 130, 246, 0.7)',
  462. 'rgba(16, 185, 129, 0.7)',
  463. 'rgba(245, 158, 11, 0.7)',
  464. 'rgba(239, 68, 68, 0.7)',
  465. 'rgba(139, 92, 246, 0.7)',
  466. 'rgba(236, 72, 153, 0.7)'
  467. ]
  468. }]
  469. },
  470. options: {
  471. responsive: true,
  472. maintainAspectRatio: false,
  473. plugins: {
  474. legend: { position: 'right' },
  475. title: { display: true, text: 'Material Cost Distribution' }
  476. }
  477. }
  478. });
  479. }
  480. }
  481. }, [activeTab, dashboardStats]);
  482.  
  483. // Add Material
  484. const addMaterial = () => {
  485. if (!newMaterial.id || !newMaterial.item || !newMaterial.packCount || !newMaterial.packCostAfterTax) {
  486. alert('Please fill in required fields: ID, Item, Pack Count, Pack Cost After Tax');
  487. return;
  488. }
  489. const cpp = parseFloat(newMaterial.packCostAfterTax) / parseFloat(newMaterial.packCount);
  490. setMaterials([...materials, {
  491. ...newMaterial,
  492. packCount: parseFloat(newMaterial.packCount),
  493. packCost: parseFloat(newMaterial.packCost) || 0,
  494. packCostAfterTax: parseFloat(newMaterial.packCostAfterTax),
  495. cpp: parseFloat(cpp.toFixed(3))
  496. }]);
  497. setNewMaterial({
  498. id: '', item: '', type: 'Paper', category: '', supplier: '', itemSize: '', grade: '',
  499. packCount: '', units: '', packCost: '', packCostAfterTax: '', cpp: '', source: '-'
  500. });
  501. setShowAddMaterial(false);
  502. };
  503.  
  504. // Edit Material
  505. const updateMaterial = (id, updates) => {
  506. setMaterials(materials.map(m => {
  507. if (m.id === id) {
  508. const updated = { ...m, ...updates };
  509. if (updates.packCostAfterTax || updates.packCount) {
  510. updated.cpp = parseFloat((updated.packCostAfterTax / updated.packCount).toFixed(3));
  511. }
  512. return updated;
  513. }
  514. return m;
  515. }));
  516. };
  517.  
  518. // Delete Material
  519. const deleteMaterial = (id) => {
  520. if (confirm('Delete this material? This will affect products using it.')) {
  521. setMaterials(materials.filter(m => m.id !== id));
  522. }
  523. };
  524.  
  525. // Update product fields
  526. const updateProductField = (productId, field, value) => {
  527. const product = productMaterials[productId];
  528. const updatedProduct = { ...product };
  529.  
  530. if (field === 'sellingPrice') {
  531. updatedProduct.sellingPrice = parseFloat(value) || 0;
  532. updatedProduct.profit = updatedProduct.sellingPrice - updatedProduct.totalCost;
  533. updatedProduct.profitMargin = (updatedProduct.profit / updatedProduct.sellingPrice) * 100;
  534. } else if (field === 'timeToMake') {
  535. updatedProduct.timeToMake = parseFloat(value) || 0;
  536. } else if (field === 'category') {
  537. updatedProduct.category = value;
  538. } else if (field === 'name') {
  539. updatedProduct.name = value;
  540. } else if (field === 'size') {
  541. updatedProduct.size = value;
  542. }
  543.  
  544. setProductMaterials({
  545. ...productMaterials,
  546. [productId]: updatedProduct
  547. });
  548. };
  549.  
  550. // Add Product
  551. const addProduct = () => {
  552. if (!newProduct.id || !newProduct.name || !newProduct.sellingPrice) {
  553. alert('Please fill in required fields: Product Number, Name, Selling Price');
  554. return;
  555. }
  556. const totalCost = newProduct.materials.reduce((sum, m) => sum + m.materialCost, 0);
  557. const profit = parseFloat(newProduct.sellingPrice) - totalCost;
  558. const profitMargin = (profit / parseFloat(newProduct.sellingPrice)) * 100;
  559. const toMake1k = totalCost > 0 ? Math.ceil(1000 / profit) : 0;
  560.  
  561. setProductMaterials({
  562. ...productMaterials,
  563. [newProduct.id]: {
  564. name: newProduct.name,
  565. category: newProduct.category || 'Uncategorized',
  566. size: newProduct.size,
  567. sellingPrice: parseFloat(newProduct.sellingPrice),
  568. totalCost,
  569. profit,
  570. profitMargin,
  571. timeToMake: parseFloat(newProduct.timeToMake) || 2,
  572. toMake1k,
  573. materials: newProduct.materials
  574. }
  575. });
  576. setNewProduct({ id: '', name: '', category: '', size: '', sellingPrice: '', timeToMake: '', materials: [] });
  577. setShowAddProduct(false);
  578. };
  579.  
  580. // Add material to new product
  581. const addMaterialToNewProduct = (materialId) => {
  582. const material = materials.find(m => m.id === materialId);
  583. if (!material) return;
  584.  
  585. const newMaterialEntry = {
  586. id: material.id,
  587. name: material.item,
  588. quantity: 1,
  589. cpp: material.cpp,
  590. materialCost: material.cpp * 1,
  591. type: material.type
  592. };
  593. setNewProduct({
  594. ...newProduct,
  595. materials: [...newProduct.materials, newMaterialEntry]
  596. });
  597. };
  598.  
  599. // Update material in new product form
  600. const updateNewProductMaterial = (index, field, value) => {
  601. const updatedMaterials = [...newProduct.materials];
  602. updatedMaterials[index] = { ...updatedMaterials[index], [field]: parseFloat(value) || 0 };
  603. if (field === 'quantity') {
  604. updatedMaterials[index].materialCost = updatedMaterials[index].quantity * updatedMaterials[index].cpp;
  605. }
  606. setNewProduct({ ...newProduct, materials: updatedMaterials });
  607. };
  608.  
  609. // Remove material from new product
  610. const removeNewProductMaterial = (index) => {
  611. setNewProduct({
  612. ...newProduct,
  613. materials: newProduct.materials.filter((_, i) => i !== index)
  614. });
  615. };
  616.  
  617. // Add material to existing product
  618. const addMaterialToProduct = (productId, materialId) => {
  619. const material = materials.find(m => m.id === materialId);
  620. if (!material) return;
  621.  
  622. const product = productMaterials[productId];
  623. const newMaterialEntry = {
  624. id: material.id,
  625. name: material.item,
  626. quantity: 1,
  627. cpp: material.cpp,
  628. materialCost: material.cpp * 1,
  629. type: material.type
  630. };
  631.  
  632. const updatedMaterials = [...product.materials, newMaterialEntry];
  633. const totalCost = updatedMaterials.reduce((sum, m) => sum + m.materialCost, 0);
  634. const profit = product.sellingPrice - totalCost;
  635. const profitMargin = (profit / product.sellingPrice) * 100;
  636.  
  637. setProductMaterials({
  638. ...productMaterials,
  639. [productId]: {
  640. ...product,
  641. materials: updatedMaterials,
  642. totalCost,
  643. profit,
  644. profitMargin
  645. }
  646. });
  647. };
  648.  
  649. // Remove material from product
  650. const removeMaterialFromProduct = (productId, materialIndex) => {
  651. const product = productMaterials[productId];
  652. const updatedMaterials = product.materials.filter((_, i) => i !== materialIndex);
  653. const totalCost = updatedMaterials.reduce((sum, m) => sum + m.materialCost, 0);
  654. const profit = product.sellingPrice - totalCost;
  655. const profitMargin = (profit / product.sellingPrice) * 100;
  656.  
  657. setProductMaterials({
  658. ...productMaterials,
  659. [productId]: {
  660. ...product,
  661. materials: updatedMaterials,
  662. totalCost,
  663. profit,
  664. profitMargin
  665. }
  666. });
  667. };
  668.  
  669. // Delete Product
  670. const deleteProduct = (id) => {
  671. if (confirm('Delete this product?')) {
  672. const updated = { ...productMaterials };
  673. delete updated[id];
  674. setProductMaterials(updated);
  675. }
  676. };
  677.  
  678. // Update Product Materials
  679. const updateProductMaterial = (productId, materialIndex, field, value) => {
  680. const product = productMaterials[productId];
  681. const updatedMaterials = [...product.materials];
  682. updatedMaterials[materialIndex] = { ...updatedMaterials[materialIndex], [field]: parseFloat(value) || 0 };
  683.  
  684. if (field === 'quantity' || field === 'cpp') {
  685. updatedMaterials[materialIndex].materialCost = updatedMaterials[materialIndex].quantity * updatedMaterials[materialIndex].cpp;
  686. }
  687.  
  688. const totalCost = updatedMaterials.reduce((sum, m) => sum + m.materialCost, 0);
  689. const profit = product.sellingPrice - totalCost;
  690. const profitMargin = (profit / product.sellingPrice) * 100;
  691.  
  692. setProductMaterials({
  693. ...productMaterials,
  694. [productId]: {
  695. ...product,
  696. materials: updatedMaterials,
  697. totalCost,
  698. profit,
  699. profitMargin
  700. }
  701. });
  702. };
  703.  
  704. // Add expense
  705. const addExpense = () => {
  706. if (!expenseForm.supplier || !expenseForm.category || !expenseForm.cost) {
  707. alert('Please fill in required fields');
  708. return;
  709. }
  710. setExpenses([...expenses, { ...expenseForm, id: Date.now(), cost: parseFloat(expenseForm.cost) }]);
  711. setExpenseForm({ date: new Date().toISOString().split('T')[0], supplier: '', category: '', itemDescription: '', cost: '', materialId: '', notes: '' });
  712. };
  713.  
  714. // Add sale
  715. const addSale = () => {
  716. if (!salesForm.location || !salesForm.product || !salesForm.pricePerUnit) {
  717. alert('Please fill in required fields');
  718. return;
  719. }
  720. const units = parseInt(salesForm.units);
  721. const price = parseFloat(salesForm.pricePerUnit);
  722. setSales([...sales, { ...salesForm, id: Date.now(), units, pricePerUnit: price, total: units * price }]);
  723. setSalesForm({ date: new Date().toISOString().split('T')[0], location: '', product: '', units: '1', pricePerUnit: '', customerId: '', repeatCustomer: 'No', customerNotes: '', complaint: 'No', returned: 'No', refunded: 'No' });
  724. };
  725.  
  726. // Estimator
  727. const estimatorResults = useMemo(() => {
  728. const allProducts = Object.entries(productMaterials).map(([id, data]) => ({ id, ...data }));
  729. let production = [];
  730.  
  731. if (estimatorMode === 'manual') {
  732. production = allProducts.map(product => ({
  733. ...product,
  734. units: manualQuantities[product.id] || 0,
  735. productProfit: product.profit * (manualQuantities[product.id] || 0),
  736. productRevenue: product.sellingPrice * (manualQuantities[product.id] || 0)
  737. }));
  738. } else if (estimatorMode === 'profit-maximizer') {
  739. const sorted = [...allProducts].sort((a, b) => b.profitMargin - a.profitMargin);
  740. let remainingTime = productionTime;
  741. for (const product of sorted) {
  742. if (remainingTime <= 0) break;
  743. const unitsToMake = Math.min(Math.floor(remainingTime / product.timeToMake), 50);
  744. if (unitsToMake > 0) {
  745. production.push({
  746. ...product,
  747. units: unitsToMake,
  748. productProfit: product.profit * unitsToMake,
  749. productRevenue: product.sellingPrice * unitsToMake
  750. });
  751. remainingTime -= unitsToMake * product.timeToMake;
  752. }
  753. }
  754. } else if (estimatorMode === 'premium-basic-ratio') {
  755. const premiumProducts = allProducts.filter(p => !p.name.includes('Basic'));
  756. const basicProducts = allProducts.filter(p => p.name.includes('Basic'));
  757.  
  758. premiumProducts.forEach(product => {
  759. const units = Math.floor((premiumRatio / 100) * 20);
  760. production.push({
  761. ...product,
  762. units,
  763. productProfit: product.profit * units,
  764. productRevenue: product.sellingPrice * units
  765. });
  766. });
  767.  
  768. basicProducts.forEach(product => {
  769. const units = Math.floor(((100 - premiumRatio) / 100) * 20);
  770. production.push({
  771. ...product,
  772. units,
  773. productProfit: product.profit * units,
  774. productRevenue: product.sellingPrice * units
  775. });
  776. });
  777. }
  778.  
  779. const totalUnits = production.reduce((sum, p) => sum + p.units, 0);
  780. const totalProfit = production.reduce((sum, p) => sum + p.productProfit, 0);
  781. const totalRevenue = production.reduce((sum, p) => sum + p.productRevenue, 0);
  782. const timeEstimate = production.reduce((sum, p) => sum + (p.units * p.timeToMake), 0);
  783.  
  784. return { production, totalUnits, totalProfit, totalRevenue, timeEstimate };
  785. }, [estimatorMode, productionTime, premiumRatio, manualQuantities, productMaterials]);
  786.  
  787. // Master export/import
  788. const masterExport = () => {
  789. const masterData = { expenses, sales, productMaterials, materials, savedScenarios, exportedAt: new Date().toISOString() };
  790. const dataStr = JSON.stringify(masterData, null, 2);
  791. const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
  792. const link = document.createElement('a');
  793. link.setAttribute('href', dataUri);
  794. link.setAttribute('download', `one-beaver-master-${new Date().toISOString().split('T')[0]}.json`);
  795. link.click();
  796. };
  797.  
  798. const masterImport = (e) => {
  799. const file = e.target.files[0];
  800. if (file) {
  801. const reader = new FileReader();
  802. reader.onload = (event) => {
  803. try {
  804. const imported = JSON.parse(event.target.result);
  805. if (imported.expenses) setExpenses(imported.expenses);
  806. if (imported.sales) setSales(imported.sales);
  807. if (imported.productMaterials) setProductMaterials(imported.productMaterials);
  808. if (imported.materials) setMaterials(imported.materials);
  809. if (imported.savedScenarios) setSavedScenarios(imported.savedScenarios);
  810. alert('All data imported successfully!');
  811. } catch (err) {
  812. alert('Error importing file.');
  813. }
  814. };
  815. reader.readAsText(file);
  816. }
  817. };
  818.  
  819. // Get unique categories
  820. const materialTypes = ['all', ...new Set(materials.map(m => m.type))];
  821. const materialCategories = ['all', ...new Set(materials.map(m => m.category).filter(Boolean))];
  822. const productCategories = ['all', ...new Set(Object.values(productMaterials).map(p => p.category).filter(Boolean))];
  823.  
  824. // Filtered materials
  825. const filteredMaterials = materials.filter(m => {
  826. if (materialFilter.type !== 'all' && m.type !== materialFilter.type) return false;
  827. if (materialFilter.category !== 'all' && m.category !== materialFilter.category) return false;
  828. return true;
  829. });
  830.  
  831. // Filtered products
  832. const filteredProducts = Object.entries(productMaterials).filter(([id, product]) => {
  833. if (productFilter !== 'all' && product.category !== productFilter) return false;
  834. return true;
  835. });
  836.  
  837. const products = Object.keys(productMaterials).map(id => ({ id, name: productMaterials[id].name }));
  838.  
  839. return (
  840. <div className="min-h-screen bg-gray-50">
  841. {/* Header */}
  842. <div className="bg-white border-b border-gray-200">
  843. <div className="max-w-7xl mx-auto px-4 py-4">
  844. <div className="flex justify-between items-center">
  845. <h1 className="text-2xl font-semibold text-gray-900">One Beaver 1300 - Business Tracker Enhanced</h1>
  846. <div className="flex gap-2">
  847. <button onClick={masterExport} className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 text-sm font-semibold">
  848. ⬇ Master Export
  849. </button>
  850. <label className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 cursor-pointer text-sm font-semibold">
  851. ⬆ Master Import
  852. <input type="file" accept=".json" onChange={masterImport} className="hidden" />
  853. </label>
  854. </div>
  855. </div>
  856. </div>
  857. </div>
  858.  
  859. {/* Navigation Tabs */}
  860. <div className="bg-white border-b border-gray-200">
  861. <div className="max-w-7xl mx-auto px-4">
  862. <nav className="flex space-x-8">
  863. {['dashboard', 'materials', 'products', 'estimator', 'sales', 'expenses', 'market-research'].map(tab => (
  864. <button
  865. key={tab}
  866. onClick={() => setActiveTab(tab)}
  867. className={`py-4 px-1 border-b-2 font-medium text-sm ${
  868. activeTab === tab ? 'border-amber-700 text-amber-700' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
  869. }`}
  870. >
  871. {tab === 'dashboard' ? '📊 Overview' : tab.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')}
  872. </button>
  873. ))}
  874. </nav>
  875. </div>
  876. </div>
  877.  
  878. {/* Main Content */}
  879. <div className="max-w-7xl mx-auto px-4 py-6">
  880. {/* DASHBOARD TAB */}
  881. {activeTab === 'dashboard' && (
  882. <div className="space-y-6">
  883. <div className="bg-blue-50 rounded-lg border border-blue-200 p-6">
  884. <h2 className="text-lg font-semibold text-gray-900 mb-2">Business Dashboard</h2>
  885. <p className="text-sm text-gray-600">
  886. Comprehensive view of your business metrics, product performance, and financial health.
  887. </p>
  888. </div>
  889.  
  890. <div className="grid grid-cols-3 gap-4">
  891. <div className="bg-white rounded-lg border-l-4 border-blue-500 p-6">
  892. <div className="text-sm text-gray-600 mb-1">Total Materials</div>
  893. <div className="text-3xl font-semibold text-blue-600">{dashboardStats.totalMaterials}</div>
  894. </div>
  895. <div className="bg-white rounded-lg border-l-4 border-red-500 p-6">
  896. <div className="text-sm text-gray-600 mb-1">Total Products</div>
  897. <div className="text-3xl font-semibold text-red-600">{dashboardStats.totalProducts}</div>
  898. </div>
  899. <div className="bg-white rounded-lg border-l-4 border-green-500 p-6">
  900. <div className="text-sm text-gray-600 mb-1">Avg Product Margin</div>
  901. <div className="text-3xl font-semibold text-green-600">{dashboardStats.avgMargin.toFixed(1)}%</div>
  902. </div>
  903. </div>
  904.  
  905. <div className="grid grid-cols-3 gap-4">
  906. <div className="bg-green-50 rounded-lg border border-green-200 p-6">
  907. <div className="text-sm text-gray-600 mb-1">Total Revenue</div>
  908. <div className="text-3xl font-semibold text-green-600">${dashboardStats.totalRevenue.toFixed(2)}</div>
  909. </div>
  910. <div className="bg-yellow-50 rounded-lg border border-yellow-200 p-6">
  911. <div className="text-sm text-gray-600 mb-1">Total Expenses</div>
  912. <div className="text-3xl font-semibold text-yellow-600">${dashboardStats.totalExpenses.toFixed(2)}</div>
  913. </div>
  914. <div className="bg-blue-50 rounded-lg border border-blue-200 p-6">
  915. <div className="text-sm text-gray-600 mb-1">Net Profit</div>
  916. <div className={`text-3xl font-semibold ${dashboardStats.netProfit >= 0 ? 'text-blue-600' : 'text-red-600'}`}>
  917. ${dashboardStats.netProfit.toFixed(2)}
  918. </div>
  919. </div>
  920. </div>
  921.  
  922. {/* Charts */}
  923. <div className="grid grid-cols-2 gap-4">
  924. <div className="bg-white rounded-lg border border-gray-200 p-6">
  925. <div style={{height: '300px'}}>
  926. <canvas ref={salesChartRef}></canvas>
  927. </div>
  928. </div>
  929. <div className="bg-white rounded-lg border border-gray-200 p-6">
  930. <div style={{height: '300px'}}>
  931. <canvas ref={expenseChartRef}></canvas>
  932. </div>
  933. </div>
  934. </div>
  935.  
  936. <div className="grid grid-cols-2 gap-4">
  937. <div className="bg-white rounded-lg border border-gray-200 p-6">
  938. <div style={{height: '300px'}}>
  939. <canvas ref={marginChartRef}></canvas>
  940. </div>
  941. </div>
  942. <div className="bg-white rounded-lg border border-gray-200 p-6">
  943. <div style={{height: '300px'}}>
  944. <canvas ref={materialCostChartRef}></canvas>
  945. </div>
  946. </div>
  947. </div>
  948.  
  949. {/* Top Performers */}
  950. <div className="grid grid-cols-2 gap-4">
  951. <div className="bg-white rounded-lg border border-gray-200 p-6">
  952. <h3 className="text-lg font-semibold text-gray-900 mb-4">Top Products by Margin</h3>
  953. <div className="space-y-3">
  954. {dashboardStats.topProductsByMargin.map(([id, product], idx) => (
  955. <div key={id} className="flex justify-between items-center p-3 bg-green-50 rounded">
  956. <div>
  957. <div className="font-semibold text-gray-900">{idx + 1}. {product.name}</div>
  958. <div className="text-sm text-gray-600">{product.size}</div>
  959. </div>
  960. <div className="text-xl font-semibold text-green-600">
  961. {product.profitMargin.toFixed(1)}%
  962. </div>
  963. </div>
  964. ))}
  965. </div>
  966. </div>
  967.  
  968. <div className="bg-white rounded-lg border border-gray-200 p-6">
  969. <h3 className="text-lg font-semibold text-gray-900 mb-4">Top Products by Revenue</h3>
  970. <div className="space-y-3">
  971. {dashboardStats.topProductsByRevenue.length > 0 ? (
  972. dashboardStats.topProductsByRevenue.map(([name, data], idx) => (
  973. <div key={name} className="flex justify-between items-center p-3 bg-blue-50 rounded">
  974. <div>
  975. <div className="font-semibold text-gray-900">{idx + 1}. {name}</div>
  976. <div className="text-sm text-gray-600">{data.units} units sold</div>
  977. </div>
  978. <div className="text-xl font-semibold text-blue-600">
  979. ${data.revenue.toFixed(2)}
  980. </div>
  981. </div>
  982. ))
  983. ) : (
  984. <div className="text-center text-gray-500 py-4">No sales recorded yet</div>
  985. )}
  986. </div>
  987. </div>
  988. </div>
  989.  
  990. {/* Quick Stats */}
  991. <div className="bg-white rounded-lg border border-gray-200 p-6">
  992. <h3 className="text-lg font-semibold text-gray-900 mb-4">Quick Stats</h3>
  993. <div className="grid grid-cols-4 gap-4 text-sm">
  994. <div className="flex justify-between p-3 bg-gray-50 rounded">
  995. <span className="text-gray-600">Total Sales:</span>
  996. <span className="font-semibold">{sales.length}</span>
  997. </div>
  998. <div className="flex justify-between p-3 bg-gray-50 rounded">
  999. <span className="text-gray-600">Total Expenses:</span>
  1000. <span className="font-semibold">{expenses.length}</span>
  1001. </div>
  1002. <div className="flex justify-between p-3 bg-gray-50 rounded">
  1003. <span className="text-gray-600">Avg Sale Value:</span>
  1004. <span className="font-semibold">
  1005. ${sales.length > 0 ? (sales.reduce((sum, s) => sum + s.total, 0) / sales.length).toFixed(2) : '0.00'}
  1006. </span>
  1007. </div>
  1008. <div className="flex justify-between p-3 bg-gray-50 rounded">
  1009. <span className="text-gray-600">Most Expensive Material:</span>
  1010. <span className="font-semibold">
  1011. {materials.length > 0 ? materials.sort((a, b) => b.cpp - a.cpp)[0].item.substring(0, 15) + '...' : 'N/A'}
  1012. </span>
  1013. </div>
  1014. </div>
  1015. </div>
  1016. </div>
  1017. )}
  1018.  
  1019. {/* MATERIALS TAB */}
  1020. {activeTab === 'materials' && (
  1021. <div className="space-y-6">
  1022. <div className="bg-blue-50 rounded-lg border border-blue-200 p-4">
  1023. <p className="text-sm text-gray-700">
  1024. <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.
  1025. </p>
  1026. </div>
  1027.  
  1028. <div className="flex gap-3 items-center flex-wrap">
  1029. <button
  1030. onClick={() => setShowAddMaterial(true)}
  1031. className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 font-semibold"
  1032. >
  1033. ➕ Add New Material
  1034. </button>
  1035. <button
  1036. onClick={() => {
  1037. const dataStr = JSON.stringify(materials, null, 2);
  1038. const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
  1039. const link = document.createElement('a');
  1040. link.setAttribute('href', dataUri);
  1041. link.setAttribute('download', `materials-${new Date().toISOString().split('T')[0]}.json`);
  1042. link.click();
  1043. }}
  1044. className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
  1045. >
  1046. Export Materials
  1047. </button>
  1048.  
  1049. <div className="flex gap-2 ml-auto">
  1050. <select
  1051. value={materialFilter.type}
  1052. onChange={(e) => setMaterialFilter({...materialFilter, type: e.target.value})}
  1053. className="px-3 py-2 border border-gray-300 rounded-md text-sm"
  1054. >
  1055. <option value="all">All Types</option>
  1056. {materialTypes.filter(t => t !== 'all').map(type => (
  1057. <option key={type} value={type}>{type}</option>
  1058. ))}
  1059. </select>
  1060. <select
  1061. value={materialFilter.category}
  1062. onChange={(e) => setMaterialFilter({...materialFilter, category: e.target.value})}
  1063. className="px-3 py-2 border border-gray-300 rounded-md text-sm"
  1064. >
  1065. <option value="all">All Categories</option>
  1066. {materialCategories.filter(c => c !== 'all').map(cat => (
  1067. <option key={cat} value={cat}>{cat}</option>
  1068. ))}
  1069. </select>
  1070. </div>
  1071. </div>
  1072.  
  1073. {showAddMaterial && (
  1074. <div className="bg-white rounded-lg border border-gray-200 p-6">
  1075. <h3 className="text-lg font-semibold text-gray-900 mb-4">Add New Material</h3>
  1076. <div className="grid grid-cols-4 gap-4 mb-4">
  1077. <div>
  1078. <label className="block text-sm font-medium text-gray-700 mb-1">ID *</label>
  1079. <input
  1080. type="text"
  1081. value={newMaterial.id}
  1082. onChange={(e) => setNewMaterial({...newMaterial, id: e.target.value})}
  1083. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1084. placeholder="1001"
  1085. />
  1086. </div>
  1087. <div>
  1088. <label className="block text-sm font-medium text-gray-700 mb-1">Item *</label>
  1089. <input
  1090. type="text"
  1091. value={newMaterial.item}
  1092. onChange={(e) => setNewMaterial({...newMaterial, item: e.target.value})}
  1093. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1094. />
  1095. </div>
  1096. <div>
  1097. <label className="block text-sm font-medium text-gray-700 mb-1">Type</label>
  1098. <select
  1099. value={newMaterial.type}
  1100. onChange={(e) => setNewMaterial({...newMaterial, type: e.target.value})}
  1101. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1102. >
  1103. <option>Paper</option>
  1104. <option>Cardstock</option>
  1105. <option>Glue</option>
  1106. <option>Cover</option>
  1107. <option>Ribbon</option>
  1108. <option>Thread</option>
  1109. <option>Cloth</option>
  1110. </select>
  1111. </div>
  1112. <div>
  1113. <label className="block text-sm font-medium text-gray-700 mb-1">Category</label>
  1114. <input
  1115. type="text"
  1116. value={newMaterial.category}
  1117. onChange={(e) => setNewMaterial({...newMaterial, category: e.target.value})}
  1118. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1119. placeholder="Core Materials"
  1120. />
  1121. </div>
  1122. </div>
  1123. <div className="grid grid-cols-4 gap-4 mb-4">
  1124. <div>
  1125. <label className="block text-sm font-medium text-gray-700 mb-1">Supplier</label>
  1126. <input
  1127. type="text"
  1128. value={newMaterial.supplier}
  1129. onChange={(e) => setNewMaterial({...newMaterial, supplier: e.target.value})}
  1130. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1131. />
  1132. </div>
  1133. <div>
  1134. <label className="block text-sm font-medium text-gray-700 mb-1">Item Size</label>
  1135. <input
  1136. type="text"
  1137. value={newMaterial.itemSize}
  1138. onChange={(e) => setNewMaterial({...newMaterial, itemSize: e.target.value})}
  1139. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1140. />
  1141. </div>
  1142. <div>
  1143. <label className="block text-sm font-medium text-gray-700 mb-1">Grade</label>
  1144. <input
  1145. type="text"
  1146. value={newMaterial.grade}
  1147. onChange={(e) => setNewMaterial({...newMaterial, grade: e.target.value})}
  1148. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1149. />
  1150. </div>
  1151. <div>
  1152. <label className="block text-sm font-medium text-gray-700 mb-1">Pack Count *</label>
  1153. <input
  1154. type="number"
  1155. value={newMaterial.packCount}
  1156. onChange={(e) => setNewMaterial({...newMaterial, packCount: e.target.value})}
  1157. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1158. />
  1159. </div>
  1160. </div>
  1161. <div className="grid grid-cols-4 gap-4 mb-4">
  1162. <div>
  1163. <label className="block text-sm font-medium text-gray-700 mb-1">Units</label>
  1164. <input
  1165. type="text"
  1166. value={newMaterial.units}
  1167. onChange={(e) => setNewMaterial({...newMaterial, units: e.target.value})}
  1168. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1169. placeholder="Sheets"
  1170. />
  1171. </div>
  1172. <div>
  1173. <label className="block text-sm font-medium text-gray-700 mb-1">Pack Cost</label>
  1174. <input
  1175. type="number"
  1176. step="0.01"
  1177. value={newMaterial.packCost}
  1178. onChange={(e) => setNewMaterial({...newMaterial, packCost: e.target.value})}
  1179. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1180. />
  1181. </div>
  1182. <div>
  1183. <label className="block text-sm font-medium text-gray-700 mb-1">Pack Cost After Tax *</label>
  1184. <input
  1185. type="number"
  1186. step="0.01"
  1187. value={newMaterial.packCostAfterTax}
  1188. onChange={(e) => setNewMaterial({...newMaterial, packCostAfterTax: e.target.value})}
  1189. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1190. />
  1191. </div>
  1192. <div>
  1193. <label className="block text-sm font-medium text-gray-700 mb-1">Source</label>
  1194. <input
  1195. type="text"
  1196. value={newMaterial.source}
  1197. onChange={(e) => setNewMaterial({...newMaterial, source: e.target.value})}
  1198. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1199. />
  1200. </div>
  1201. </div>
  1202. <div className="flex gap-3">
  1203. <button
  1204. onClick={addMaterial}
  1205. className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700"
  1206. >
  1207. Add Material
  1208. </button>
  1209. <button
  1210. onClick={() => setShowAddMaterial(false)}
  1211. className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400"
  1212. >
  1213. Cancel
  1214. </button>
  1215. </div>
  1216. </div>
  1217. )}
  1218.  
  1219. <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
  1220. <div className="overflow-x-auto">
  1221. <table className="w-full text-sm" ref={materialsTableRef}>
  1222. <thead className="bg-gray-50">
  1223. <tr>
  1224. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Drag</th>
  1225. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th>
  1226. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Item</th>
  1227. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Category</th>
  1228. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Supplier</th>
  1229. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Item Size</th>
  1230. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Grade</th>
  1231. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Pack Count</th>
  1232. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Units</th>
  1233. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Pack Cost After Tax</th>
  1234. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">CPP</th>
  1235. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
  1236. </tr>
  1237. </thead>
  1238. <tbody className="divide-y divide-gray-200">
  1239. {filteredMaterials.map(material => (
  1240. <tr key={material.id} className="hover:bg-gray-50">
  1241. <td className="px-4 py-3">
  1242. <span className="drag-handle cursor-move text-gray-400 hover:text-gray-600 text-lg">☰</span>
  1243. </td>
  1244. <td className="px-4 py-3">
  1245. <span className={`px-2 py-1 rounded text-xs font-semibold ${
  1246. material.type === 'Paper' ? 'bg-blue-100 text-blue-800' :
  1247. material.type === 'Cardstock' ? 'bg-cyan-100 text-cyan-800' :
  1248. material.type === 'Glue' ? 'bg-teal-100 text-teal-800' :
  1249. material.type === 'Cover' ? 'bg-yellow-100 text-yellow-800' :
  1250. material.type === 'Ribbon' ? 'bg-purple-100 text-purple-800' :
  1251. material.type === 'Thread' ? 'bg-green-100 text-green-800' :
  1252. 'bg-gray-100 text-gray-800'
  1253. }`}>
  1254. {material.id}
  1255. </span>
  1256. </td>
  1257. <td className="px-4 py-3 font-semibold">{material.item}</td>
  1258. <td className="px-4 py-3 text-gray-600">{material.category || '-'}</td>
  1259. <td className="px-4 py-3">{material.supplier}</td>
  1260. <td className="px-4 py-3">{material.itemSize}</td>
  1261. <td className="px-4 py-3">{material.grade}</td>
  1262. <td className="px-4 py-3">{material.packCount}</td>
  1263. <td className="px-4 py-3">{material.units}</td>
  1264. <td className="px-4 py-3">${material.packCostAfterTax.toFixed(2)}</td>
  1265. <td className="px-4 py-3 font-mono font-semibold">${material.cpp.toFixed(3)}</td>
  1266. <td className="px-4 py-3">
  1267. <div className="flex gap-2">
  1268. <button
  1269. onClick={() => deleteMaterial(material.id)}
  1270. className="px-3 py-1 bg-red-600 text-white rounded text-xs hover:bg-red-700"
  1271. >
  1272. Delete
  1273. </button>
  1274. </div>
  1275. </td>
  1276. </tr>
  1277. ))}
  1278. </tbody>
  1279. </table>
  1280. </div>
  1281. </div>
  1282. </div>
  1283. )}
  1284.  
  1285. {/* PRODUCTS TAB */}
  1286. {activeTab === 'products' && (
  1287. <div className="space-y-6">
  1288. <div className="bg-blue-50 rounded-lg border border-blue-200 p-4">
  1289. <p className="text-sm text-gray-700">
  1290. <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.
  1291. </p>
  1292. </div>
  1293.  
  1294. <div className="flex gap-3 items-center">
  1295. <button
  1296. onClick={() => setShowAddProduct(true)}
  1297. className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 font-semibold"
  1298. >
  1299. ➕ Add New Product
  1300. </button>
  1301. <button
  1302. onClick={() => {
  1303. const dataStr = JSON.stringify(productMaterials, null, 2);
  1304. const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
  1305. const link = document.createElement('a');
  1306. link.setAttribute('href', dataUri);
  1307. link.setAttribute('download', `products-${new Date().toISOString().split('T')[0]}.json`);
  1308. link.click();
  1309. }}
  1310. className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
  1311. >
  1312. Export Products
  1313. </button>
  1314.  
  1315. <div className="ml-auto">
  1316. <select
  1317. value={productFilter}
  1318. onChange={(e) => setProductFilter(e.target.value)}
  1319. className="px-3 py-2 border border-gray-300 rounded-md text-sm"
  1320. >
  1321. <option value="all">All Categories</option>
  1322. {productCategories.filter(c => c !== 'all').map(cat => (
  1323. <option key={cat} value={cat}>{cat}</option>
  1324. ))}
  1325. </select>
  1326. </div>
  1327. </div>
  1328.  
  1329. {showAddProduct && (
  1330. <div className="bg-white rounded-lg border border-gray-200 p-6">
  1331. <h3 className="text-lg font-semibold text-gray-900 mb-4">Add New Product</h3>
  1332. <div className="grid grid-cols-4 gap-4 mb-4">
  1333. <div>
  1334. <label className="block text-sm font-medium text-gray-700 mb-1">Product Number *</label>
  1335. <input
  1336. type="text"
  1337. value={newProduct.id}
  1338. onChange={(e) => setNewProduct({...newProduct, id: e.target.value})}
  1339. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1340. placeholder="PB-BB-XXX-1300-0825"
  1341. />
  1342. </div>
  1343. <div>
  1344. <label className="block text-sm font-medium text-gray-700 mb-1">Product Name *</label>
  1345. <input
  1346. type="text"
  1347. value={newProduct.name}
  1348. onChange={(e) => setNewProduct({...newProduct, name: e.target.value})}
  1349. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1350. placeholder="One Beaver - Mini"
  1351. />
  1352. </div>
  1353. <div>
  1354. <label className="block text-sm font-medium text-gray-700 mb-1">Category</label>
  1355. <input
  1356. type="text"
  1357. value={newProduct.category}
  1358. onChange={(e) => setNewProduct({...newProduct, category: e.target.value})}
  1359. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1360. placeholder="Premium"
  1361. />
  1362. </div>
  1363. <div>
  1364. <label className="block text-sm font-medium text-gray-700 mb-1">Size</label>
  1365. <input
  1366. type="text"
  1367. value={newProduct.size}
  1368. onChange={(e) => setNewProduct({...newProduct, size: e.target.value})}
  1369. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1370. placeholder="3.5 x 5.5"
  1371. />
  1372. </div>
  1373. </div>
  1374. <div className="grid grid-cols-2 gap-4 mb-4">
  1375. <div>
  1376. <label className="block text-sm font-medium text-gray-700 mb-1">Selling Price *</label>
  1377. <input
  1378. type="number"
  1379. step="0.01"
  1380. value={newProduct.sellingPrice}
  1381. onChange={(e) => setNewProduct({...newProduct, sellingPrice: e.target.value})}
  1382. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1383. placeholder="8.00"
  1384. />
  1385. </div>
  1386. <div>
  1387. <label className="block text-sm font-medium text-gray-700 mb-1">Time to Make (hours)</label>
  1388. <input
  1389. type="number"
  1390. step="0.5"
  1391. value={newProduct.timeToMake}
  1392. onChange={(e) => setNewProduct({...newProduct, timeToMake: e.target.value})}
  1393. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1394. placeholder="2.0"
  1395. />
  1396. </div>
  1397. </div>
  1398. <div className="mb-4">
  1399. <div className="flex justify-between items-center mb-2">
  1400. <label className="block text-sm font-medium text-gray-700">Materials</label>
  1401. <select
  1402. onChange={(e) => {
  1403. if (e.target.value) {
  1404. addMaterialToNewProduct(e.target.value);
  1405. e.target.value = '';
  1406. }
  1407. }}
  1408. className="px-3 py-1 border border-gray-300 rounded-md text-sm"
  1409. >
  1410. <option value="">+ Add Material</option>
  1411. {materials.map(mat => (
  1412. <option key={mat.id} value={mat.id}>
  1413. {mat.id} - {mat.item} (${mat.cpp.toFixed(3)})
  1414. </option>
  1415. ))}
  1416. </select>
  1417. </div>
  1418. {newProduct.materials.length > 0 && (
  1419. <table className="w-full text-sm border border-gray-200">
  1420. <thead className="bg-gray-50">
  1421. <tr>
  1422. <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Material</th>
  1423. <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Quantity</th>
  1424. <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">CPP</th>
  1425. <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Cost</th>
  1426. <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Actions</th>
  1427. </tr>
  1428. </thead>
  1429. <tbody>
  1430. {newProduct.materials.map((mat, idx) => (
  1431. <tr key={idx} className="border-t">
  1432. <td className="px-3 py-2">{mat.name}</td>
  1433. <td className="px-3 py-2">
  1434. <input
  1435. type="number"
  1436. step="0.01"
  1437. value={mat.quantity}
  1438. onChange={(e) => updateNewProductMaterial(idx, 'quantity', e.target.value)}
  1439. className="w-20 px-2 py-1 border border-gray-300 rounded"
  1440. />
  1441. </td>
  1442. <td className="px-3 py-2 font-mono">${mat.cpp.toFixed(3)}</td>
  1443. <td className="px-3 py-2 font-semibold">${mat.materialCost.toFixed(3)}</td>
  1444. <td className="px-3 py-2">
  1445. <button
  1446. onClick={() => removeNewProductMaterial(idx)}
  1447. className="px-2 py-1 bg-red-600 text-white rounded text-xs hover:bg-red-700"
  1448. >
  1449. Remove
  1450. </button>
  1451. </td>
  1452. </tr>
  1453. ))}
  1454. <tr className="border-t bg-gray-50 font-semibold">
  1455. <td className="px-3 py-2" colSpan="3">Total Cost</td>
  1456. <td className="px-3 py-2">${newProduct.materials.reduce((sum, m) => sum + m.materialCost, 0).toFixed(2)}</td>
  1457. <td></td>
  1458. </tr>
  1459. </tbody>
  1460. </table>
  1461. )}
  1462. </div>
  1463. <div className="flex gap-3">
  1464. <button
  1465. onClick={addProduct}
  1466. className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700"
  1467. >
  1468. Add Product
  1469. </button>
  1470. <button
  1471. onClick={() => {
  1472. setShowAddProduct(false);
  1473. setNewProduct({ id: '', name: '', category: '', size: '', sellingPrice: '', timeToMake: '', materials: [] });
  1474. }}
  1475. className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400"
  1476. >
  1477. Cancel
  1478. </button>
  1479. </div>
  1480. </div>
  1481. )}
  1482.  
  1483. <div className="space-y-3" ref={productsListRef}>
  1484. {filteredProducts.map(([productId, product]) => (
  1485. <div key={productId} className="bg-white rounded-lg border border-gray-200">
  1486. <div
  1487. className="px-6 py-4 bg-gray-50 border-b border-gray-200 flex justify-between items-center hover:bg-gray-100"
  1488. >
  1489. <div className="flex items-center gap-3">
  1490. <span className="drag-handle cursor-move text-gray-400 hover:text-gray-600 text-lg">☰</span>
  1491. <div className="flex-1">
  1492. <div className="flex items-center gap-2 mb-1">
  1493. {editingProductField[productId] === 'name' ? (
  1494. <input
  1495. type="text"
  1496. value={product.name}
  1497. onChange={(e) => updateProductField(productId, 'name', e.target.value)}
  1498. onBlur={() => setEditingProductField({...editingProductField, [productId]: null})}
  1499. className="text-lg font-semibold px-2 py-1 border border-blue-500 rounded"
  1500. autoFocus
  1501. />
  1502. ) : (
  1503. <h3
  1504. className="text-lg font-semibold text-gray-900 cursor-pointer hover:text-blue-600"
  1505. onClick={() => setEditingProductField({...editingProductField, [productId]: 'name'})}
  1506. >
  1507. {product.name}
  1508. </h3>
  1509. )}
  1510. {editingProductField[productId] === 'category' ? (
  1511. <input
  1512. type="text"
  1513. value={product.category}
  1514. onChange={(e) => updateProductField(productId, 'category', e.target.value)}
  1515. onBlur={() => setEditingProductField({...editingProductField, [productId]: null})}
  1516. className="px-2 py-1 border border-blue-500 rounded text-xs"
  1517. autoFocus
  1518. />
  1519. ) : (
  1520. <span
  1521. className={`px-2 py-1 rounded text-xs font-semibold cursor-pointer hover:ring-2 hover:ring-blue-500 ${
  1522. product.category === 'Premium' ? 'bg-purple-100 text-purple-800' :
  1523. product.category === 'Basic' ? 'bg-gray-100 text-gray-800' :
  1524. 'bg-blue-100 text-blue-800'
  1525. }`}
  1526. onClick={() => setEditingProductField({...editingProductField, [productId]: 'category'})}
  1527. >
  1528. {product.category}
  1529. </span>
  1530. )}
  1531. </div>
  1532. <p className="text-sm text-gray-600">
  1533. {product.size} | <span className="font-mono">{productId}</span> |
  1534. {editingProductField[productId] === 'timeToMake' ? (
  1535. <input
  1536. type="number"
  1537. step="0.5"
  1538. value={product.timeToMake}
  1539. onChange={(e) => updateProductField(productId, 'timeToMake', e.target.value)}
  1540. onBlur={() => setEditingProductField({...editingProductField, [productId]: null})}
  1541. className="w-16 px-1 py-0 border border-blue-500 rounded ml-1"
  1542. autoFocus
  1543. />
  1544. ) : (
  1545. <span
  1546. className="cursor-pointer hover:text-blue-600 ml-1"
  1547. onClick={() => setEditingProductField({...editingProductField, [productId]: 'timeToMake'})}
  1548. >
  1549. Time: {product.timeToMake}hrs
  1550. </span>
  1551. )}
  1552. </p>
  1553. </div>
  1554. </div>
  1555. <div className="flex items-center gap-6">
  1556. <div className="text-right">
  1557. <div className="text-sm text-gray-600">Selling Price</div>
  1558. {editingProductField[productId] === 'sellingPrice' ? (
  1559. <input
  1560. type="number"
  1561. step="0.01"
  1562. value={product.sellingPrice}
  1563. onChange={(e) => updateProductField(productId, 'sellingPrice', e.target.value)}
  1564. onBlur={() => setEditingProductField({...editingProductField, [productId]: null})}
  1565. className="text-xl font-semibold w-24 px-2 py-1 border border-blue-500 rounded"
  1566. autoFocus
  1567. />
  1568. ) : (
  1569. <div
  1570. className="text-xl font-semibold text-gray-900 cursor-pointer hover:text-blue-600"
  1571. onClick={() => setEditingProductField({...editingProductField, [productId]: 'sellingPrice'})}
  1572. >
  1573. ${product.sellingPrice.toFixed(2)}
  1574. </div>
  1575. )}
  1576. </div>
  1577. <div className="text-right">
  1578. <div className="text-sm text-gray-600">Total Cost</div>
  1579. <div className="text-xl font-semibold text-gray-900">${product.totalCost.toFixed(2)}</div>
  1580. </div>
  1581. <div className="text-right">
  1582. <div className="text-sm text-gray-600">Profit</div>
  1583. <div className="text-xl font-semibold text-green-600">${product.profit.toFixed(2)}</div>
  1584. </div>
  1585. <div className="text-right">
  1586. <div className="text-sm text-gray-600">Margin</div>
  1587. <div className={`text-xl font-semibold px-3 py-1 rounded ${
  1588. product.profitMargin >= 70 ? 'bg-green-100 text-green-800' :
  1589. product.profitMargin >= 60 ? 'bg-green-50 text-green-700' : 'bg-yellow-50 text-yellow-700'
  1590. }`}>
  1591. {product.profitMargin.toFixed(1)}%
  1592. </div>
  1593. </div>
  1594. <div className="flex gap-2">
  1595. <button
  1596. onClick={() => setExpandedProducts({...expandedProducts, [productId]: !expandedProducts[productId]})}
  1597. className="px-3 py-1 bg-blue-600 text-white rounded text-sm hover:bg-blue-700"
  1598. >
  1599. {expandedProducts[productId] ? 'Hide' : 'Materials'}
  1600. </button>
  1601. <select
  1602. onChange={(e) => {
  1603. if (e.target.value) {
  1604. addMaterialToProduct(productId, e.target.value);
  1605. e.target.value = '';
  1606. }
  1607. }}
  1608. className="px-3 py-1 border border-green-600 bg-white text-green-700 rounded text-sm hover:bg-green-50"
  1609. >
  1610. <option value="">➕ Material</option>
  1611. {materials.filter(mat => !product.materials.some(pm => pm.id === mat.id)).map(mat => (
  1612. <option key={mat.id} value={mat.id}>
  1613. {mat.id} - {mat.item} (${mat.cpp.toFixed(3)})
  1614. </option>
  1615. ))}
  1616. </select>
  1617. <button
  1618. onClick={() => deleteProduct(productId)}
  1619. className="px-3 py-1 bg-red-600 text-white rounded text-sm hover:bg-red-700"
  1620. >
  1621. Delete
  1622. </button>
  1623. </div>
  1624. </div>
  1625. </div>
  1626. {expandedProducts[productId] && (
  1627. <div className="p-6">
  1628. <h4 className="text-md font-semibold text-gray-900 mb-3">Materials Breakdown</h4>
  1629. <table className="w-full text-sm">
  1630. <thead className="bg-gray-50">
  1631. <tr>
  1632. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Material ID</th>
  1633. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Material</th>
  1634. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Quantity</th>
  1635. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">CPP</th>
  1636. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Material Cost</th>
  1637. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">% of Total</th>
  1638. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
  1639. </tr>
  1640. </thead>
  1641. <tbody className="divide-y divide-gray-200">
  1642. {product.materials.map((material, idx) => (
  1643. <tr key={idx}>
  1644. <td className="px-4 py-2 font-mono text-xs">{material.id}</td>
  1645. <td className="px-4 py-2">{material.name}</td>
  1646. <td className="px-4 py-2">
  1647. <input
  1648. type="number"
  1649. step="0.01"
  1650. value={material.quantity}
  1651. onChange={(e) => updateProductMaterial(productId, idx, 'quantity', e.target.value)}
  1652. className="w-24 px-2 py-1 border border-gray-300 rounded"
  1653. />
  1654. </td>
  1655. <td className="px-4 py-2 font-mono">${material.cpp.toFixed(3)}</td>
  1656. <td className="px-4 py-2 font-semibold">${material.materialCost.toFixed(3)}</td>
  1657. <td className="px-4 py-2">
  1658. {((material.materialCost / product.totalCost) * 100).toFixed(1)}%
  1659. </td>
  1660. <td className="px-4 py-2">
  1661. <button
  1662. onClick={() => removeMaterialFromProduct(productId, idx)}
  1663. className="px-2 py-1 bg-red-600 text-white rounded text-xs hover:bg-red-700"
  1664. >
  1665. Remove
  1666. </button>
  1667. </td>
  1668. </tr>
  1669. ))}
  1670. </tbody>
  1671. </table>
  1672. </div>
  1673. )}
  1674. </div>
  1675. ))}
  1676. </div>
  1677. </div>
  1678. )}
  1679.  
  1680. {/* ESTIMATOR TAB */}
  1681. {activeTab === 'estimator' && (
  1682. <div className="space-y-6">
  1683. <div className="bg-amber-50 rounded-lg border border-amber-200 p-6">
  1684. <h2 className="text-lg font-semibold text-gray-900 mb-2">Production Estimator</h2>
  1685. <p className="text-sm text-gray-600">
  1686. Choose a strategy to calculate your optimal production mix, or manually set quantities for each product.
  1687. </p>
  1688. </div>
  1689.  
  1690. <div className="bg-white rounded-lg border border-gray-200 p-6">
  1691. <h3 className="text-md font-semibold text-gray-900 mb-4">Select Strategy</h3>
  1692. <div className="grid grid-cols-3 gap-4">
  1693. <button
  1694. onClick={() => setEstimatorMode('manual')}
  1695. className={`p-4 border-2 rounded-lg text-left ${
  1696. estimatorMode === 'manual' ? 'border-amber-700 bg-amber-50' : 'border-gray-200 hover:border-gray-300'
  1697. }`}
  1698. >
  1699. <div className="font-semibold text-gray-900 mb-1">Manual Quantities</div>
  1700. <div className="text-xs text-gray-600">Set specific quantities for each product in the table below.</div>
  1701. </button>
  1702. <button
  1703. onClick={() => setEstimatorMode('profit-maximizer')}
  1704. className={`p-4 border-2 rounded-lg text-left ${
  1705. estimatorMode === 'profit-maximizer' ? 'border-amber-700 bg-amber-50' : 'border-gray-200 hover:border-gray-300'
  1706. }`}
  1707. >
  1708. <div className="font-semibold text-gray-900 mb-1">Profit Maximizer</div>
  1709. <div className="text-xs text-gray-600">Auto-calculate optimal mix within your time budget.</div>
  1710. </button>
  1711. <button
  1712. onClick={() => setEstimatorMode('premium-basic-ratio')}
  1713. className={`p-4 border-2 rounded-lg text-left ${
  1714. estimatorMode === 'premium-basic-ratio' ? 'border-amber-700 bg-amber-50' : 'border-gray-200 hover:border-gray-300'
  1715. }`}
  1716. >
  1717. <div className="font-semibold text-gray-900 mb-1">Premium/Basic Ratio</div>
  1718. <div className="text-xs text-gray-600">Distribute production by premium vs basic focus.</div>
  1719. </button>
  1720. </div>
  1721. </div>
  1722.  
  1723. {estimatorMode !== 'manual' && (
  1724. <div className="bg-white rounded-lg border border-gray-200 p-6">
  1725. <h3 className="text-md font-semibold text-gray-900 mb-4">Configuration</h3>
  1726. {estimatorMode === 'profit-maximizer' && (
  1727. <div className="mb-4">
  1728. <label className="block text-sm font-medium text-gray-700 mb-2">
  1729. Available Production Time: {productionTime} hours
  1730. </label>
  1731. <input
  1732. type="range"
  1733. min="1"
  1734. max="160"
  1735. value={productionTime}
  1736. onChange={(e) => setProductionTime(parseInt(e.target.value))}
  1737. className="w-full"
  1738. />
  1739. </div>
  1740. )}
  1741. {estimatorMode === 'premium-basic-ratio' && (
  1742. <div className="mb-4">
  1743. <label className="block text-sm font-medium text-gray-700 mb-2">
  1744. Premium: {premiumRatio}% | Basic: {100 - premiumRatio}%
  1745. </label>
  1746. <input
  1747. type="range"
  1748. min="0"
  1749. max="100"
  1750. value={premiumRatio}
  1751. onChange={(e) => setPremiumRatio(parseInt(e.target.value))}
  1752. className="w-full"
  1753. />
  1754. </div>
  1755. )}
  1756. </div>
  1757. )}
  1758.  
  1759. <div className="bg-white rounded-lg border border-gray-200 p-6">
  1760. <h3 className="text-md font-semibold text-gray-900 mb-4">Production Plan</h3>
  1761. <div className="grid grid-cols-4 gap-4 mb-6 p-4 bg-green-50 rounded-lg border border-green-200">
  1762. <div>
  1763. <div className="text-sm text-gray-600">Total Units</div>
  1764. <div className="text-2xl font-semibold text-gray-900">{estimatorResults.totalUnits}</div>
  1765. </div>
  1766. <div>
  1767. <div className="text-sm text-gray-600">Total Revenue</div>
  1768. <div className="text-2xl font-semibold text-blue-600">${estimatorResults.totalRevenue.toFixed(2)}</div>
  1769. </div>
  1770. <div>
  1771. <div className="text-sm text-gray-600">Total Profit</div>
  1772. <div className="text-2xl font-semibold text-green-600">${estimatorResults.totalProfit.toFixed(2)}</div>
  1773. </div>
  1774. <div>
  1775. <div className="text-sm text-gray-600">Time Estimate</div>
  1776. <div className="text-2xl font-semibold text-gray-900">{estimatorResults.timeEstimate.toFixed(1)}hrs</div>
  1777. </div>
  1778. </div>
  1779.  
  1780. <div className="overflow-x-auto">
  1781. <table className="w-full text-sm">
  1782. <thead className="bg-gray-50">
  1783. <tr>
  1784. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Product</th>
  1785. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Size</th>
  1786. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">
  1787. {estimatorMode === 'manual' ? 'Quantity (Edit)' : 'Quantity'}
  1788. </th>
  1789. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Unit Price</th>
  1790. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Revenue</th>
  1791. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Unit Profit</th>
  1792. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Total Profit</th>
  1793. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Margin</th>
  1794. </tr>
  1795. </thead>
  1796. <tbody className="divide-y divide-gray-200">
  1797. {estimatorResults.production.map((item, idx) => (
  1798. <tr key={idx}>
  1799. <td className="px-4 py-3 font-semibold">{item.name}</td>
  1800. <td className="px-4 py-3">{item.size}</td>
  1801. <td className="px-4 py-3">
  1802. {estimatorMode === 'manual' ? (
  1803. <input
  1804. type="number"
  1805. min="0"
  1806. value={manualQuantities[item.id] || 0}
  1807. onChange={(e) => setManualQuantities({
  1808. ...manualQuantities,
  1809. [item.id]: parseInt(e.target.value) || 0
  1810. })}
  1811. className="w-20 px-2 py-1 border border-gray-300 rounded font-semibold"
  1812. />
  1813. ) : (
  1814. <span className="font-semibold">{item.units}</span>
  1815. )}
  1816. </td>
  1817. <td className="px-4 py-3">${item.sellingPrice.toFixed(2)}</td>
  1818. <td className="px-4 py-3">${item.productRevenue.toFixed(2)}</td>
  1819. <td className="px-4 py-3 text-green-600">${item.profit.toFixed(2)}</td>
  1820. <td className="px-4 py-3 text-green-600 font-semibold">${item.productProfit.toFixed(2)}</td>
  1821. <td className="px-4 py-3">{item.profitMargin.toFixed(1)}%</td>
  1822. </tr>
  1823. ))}
  1824. </tbody>
  1825. </table>
  1826. </div>
  1827. </div>
  1828. </div>
  1829. )}
  1830.  
  1831. {/* SALES TAB */}
  1832. {activeTab === 'sales' && (
  1833. <div className="space-y-6">
  1834. <div className="bg-white rounded-lg border border-gray-200 p-6">
  1835. <h2 className="text-lg font-semibold text-gray-900 mb-4">Record New Sale</h2>
  1836. <div className="grid grid-cols-2 gap-4 mb-4">
  1837. <div>
  1838. <label className="block text-sm font-medium text-gray-700 mb-1">Date *</label>
  1839. <input
  1840. type="date"
  1841. value={salesForm.date}
  1842. onChange={(e) => setSalesForm({...salesForm, date: e.target.value})}
  1843. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1844. />
  1845. </div>
  1846. <div>
  1847. <label className="block text-sm font-medium text-gray-700 mb-1">Location/Venue *</label>
  1848. <input
  1849. type="text"
  1850. placeholder="e.g., Main Street Craft Market"
  1851. value={salesForm.location}
  1852. onChange={(e) => setSalesForm({...salesForm, location: e.target.value})}
  1853. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1854. />
  1855. </div>
  1856. </div>
  1857. <div className="grid grid-cols-3 gap-4 mb-4">
  1858. <div>
  1859. <label className="block text-sm font-medium text-gray-700 mb-1">Product Sold *</label>
  1860. <select
  1861. value={salesForm.product}
  1862. onChange={(e) => setSalesForm({...salesForm, product: e.target.value})}
  1863. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1864. >
  1865. <option value="">Select product...</option>
  1866. {products.map(prod => <option key={prod.id} value={prod.name}>{prod.name}</option>)}
  1867. </select>
  1868. </div>
  1869. <div>
  1870. <label className="block text-sm font-medium text-gray-700 mb-1">Units Sold *</label>
  1871. <input
  1872. type="number"
  1873. min="1"
  1874. value={salesForm.units}
  1875. onChange={(e) => setSalesForm({...salesForm, units: e.target.value})}
  1876. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1877. />
  1878. </div>
  1879. <div>
  1880. <label className="block text-sm font-medium text-gray-700 mb-1">Price Per Unit *</label>
  1881. <input
  1882. type="number"
  1883. step="0.01"
  1884. placeholder="Actual price sold"
  1885. value={salesForm.pricePerUnit}
  1886. onChange={(e) => setSalesForm({...salesForm, pricePerUnit: e.target.value})}
  1887. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1888. />
  1889. </div>
  1890. </div>
  1891. <button
  1892. onClick={addSale}
  1893. className="px-6 py-2 bg-amber-800 text-white rounded-md hover:bg-amber-900"
  1894. >
  1895. Add Sale Record
  1896. </button>
  1897. </div>
  1898.  
  1899. <div className="bg-blue-50 rounded-lg border border-blue-200 p-6">
  1900. <h3 className="text-lg font-semibold text-gray-900 mb-4">Sales Summary</h3>
  1901. <div className="grid grid-cols-3 gap-6">
  1902. <div>
  1903. <div className="text-sm text-gray-600">Total Sales</div>
  1904. <div className="text-2xl font-semibold text-gray-900">{sales.length}</div>
  1905. </div>
  1906. <div>
  1907. <div className="text-sm text-gray-600">Total Revenue</div>
  1908. <div className="text-2xl font-semibold text-gray-900">${sales.reduce((sum, s) => sum + s.total, 0).toFixed(2)}</div>
  1909. </div>
  1910. <div>
  1911. <div className="text-sm text-gray-600">Avg Sale Value</div>
  1912. <div className="text-2xl font-semibold text-gray-900">
  1913. ${sales.length > 0 ? (sales.reduce((sum, s) => sum + s.total, 0) / sales.length).toFixed(2) : '0.00'}
  1914. </div>
  1915. </div>
  1916. </div>
  1917. </div>
  1918.  
  1919. {sales.length > 0 && (
  1920. <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
  1921. <div className="overflow-x-auto">
  1922. <table className="w-full text-sm">
  1923. <thead className="bg-gray-50">
  1924. <tr>
  1925. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Date</th>
  1926. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Location</th>
  1927. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Product</th>
  1928. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Units</th>
  1929. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Price/Unit</th>
  1930. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Total</th>
  1931. </tr>
  1932. </thead>
  1933. <tbody className="divide-y divide-gray-200">
  1934. {sales.map(sale => (
  1935. <tr key={sale.id}>
  1936. <td className="px-4 py-3">{sale.date}</td>
  1937. <td className="px-4 py-3">{sale.location}</td>
  1938. <td className="px-4 py-3 font-semibold">{sale.product}</td>
  1939. <td className="px-4 py-3">{sale.units}</td>
  1940. <td className="px-4 py-3">${sale.pricePerUnit.toFixed(2)}</td>
  1941. <td className="px-4 py-3 font-semibold">${sale.total.toFixed(2)}</td>
  1942. </tr>
  1943. ))}
  1944. </tbody>
  1945. </table>
  1946. </div>
  1947. </div>
  1948. )}
  1949. </div>
  1950. )}
  1951.  
  1952. {/* EXPENSES TAB */}
  1953. {activeTab === 'expenses' && (
  1954. <div className="space-y-6">
  1955. <div className="bg-white rounded-lg border border-gray-200 p-6">
  1956. <h2 className="text-lg font-semibold text-gray-900 mb-4">Record New Expense</h2>
  1957. <div className="grid grid-cols-2 gap-4 mb-4">
  1958. <div>
  1959. <label className="block text-sm font-medium text-gray-700 mb-1">Date *</label>
  1960. <input
  1961. type="date"
  1962. value={expenseForm.date}
  1963. onChange={(e) => setExpenseForm({...expenseForm, date: e.target.value})}
  1964. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1965. />
  1966. </div>
  1967. <div>
  1968. <label className="block text-sm font-medium text-gray-700 mb-1">Supplier/Store *</label>
  1969. <input
  1970. type="text"
  1971. placeholder="e.g., Staples, Amazon"
  1972. value={expenseForm.supplier}
  1973. onChange={(e) => setExpenseForm({...expenseForm, supplier: e.target.value})}
  1974. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1975. />
  1976. </div>
  1977. </div>
  1978. <div className="grid grid-cols-4 gap-4 mb-4">
  1979. <div>
  1980. <label className="block text-sm font-medium text-gray-700 mb-1">Category *</label>
  1981. <select
  1982. value={expenseForm.category}
  1983. onChange={(e) => setExpenseForm({...expenseForm, category: e.target.value})}
  1984. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1985. >
  1986. <option value="">Select...</option>
  1987. {expenseCategories.map(cat => <option key={cat} value={cat}>{cat}</option>)}
  1988. </select>
  1989. </div>
  1990. <div>
  1991. <label className="block text-sm font-medium text-gray-700 mb-1">Item Description *</label>
  1992. <input
  1993. type="text"
  1994. value={expenseForm.itemDescription}
  1995. onChange={(e) => setExpenseForm({...expenseForm, itemDescription: e.target.value})}
  1996. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  1997. />
  1998. </div>
  1999. <div>
  2000. <label className="block text-sm font-medium text-gray-700 mb-1">Cost *</label>
  2001. <input
  2002. type="number"
  2003. step="0.01"
  2004. value={expenseForm.cost}
  2005. onChange={(e) => setExpenseForm({...expenseForm, cost: e.target.value})}
  2006. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  2007. />
  2008. </div>
  2009. <div>
  2010. <label className="block text-sm font-medium text-gray-700 mb-1">Material ID</label>
  2011. <select
  2012. value={expenseForm.materialId}
  2013. onChange={(e) => setExpenseForm({...expenseForm, materialId: e.target.value})}
  2014. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  2015. >
  2016. <option value="">Optional</option>
  2017. {materials.map(mat => <option key={mat.id} value={mat.id}>{mat.id} - {mat.item}</option>)}
  2018. </select>
  2019. </div>
  2020. </div>
  2021. <button
  2022. onClick={addExpense}
  2023. className="px-6 py-2 bg-amber-800 text-white rounded-md hover:bg-amber-900"
  2024. >
  2025. Add Expense
  2026. </button>
  2027. </div>
  2028.  
  2029. <div className="bg-green-50 rounded-lg border border-green-200 p-6">
  2030. <h3 className="text-lg font-semibold text-gray-900 mb-4">Expense Summary</h3>
  2031. <div className="grid grid-cols-3 gap-6">
  2032. <div>
  2033. <div className="text-sm text-gray-600">Total Expenses</div>
  2034. <div className="text-2xl font-semibold text-gray-900">${expenses.reduce((sum, e) => sum + e.cost, 0).toFixed(2)}</div>
  2035. </div>
  2036. <div>
  2037. <div className="text-sm text-gray-600">Total Transactions</div>
  2038. <div className="text-2xl font-semibold text-gray-900">{expenses.length}</div>
  2039. </div>
  2040. <div>
  2041. <div className="text-sm text-gray-600">Avg Transaction</div>
  2042. <div className="text-2xl font-semibold text-gray-900">
  2043. ${expenses.length > 0 ? (expenses.reduce((sum, e) => sum + e.cost, 0) / expenses.length).toFixed(2) : '0.00'}
  2044. </div>
  2045. </div>
  2046. </div>
  2047. </div>
  2048.  
  2049. {expenses.length > 0 && (
  2050. <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
  2051. <div className="overflow-x-auto">
  2052. <table className="w-full text-sm">
  2053. <thead className="bg-gray-50">
  2054. <tr>
  2055. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Date</th>
  2056. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Supplier</th>
  2057. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Category</th>
  2058. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Description</th>
  2059. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Cost</th>
  2060. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Material ID</th>
  2061. </tr>
  2062. </thead>
  2063. <tbody className="divide-y divide-gray-200">
  2064. {expenses.map(expense => (
  2065. <tr key={expense.id}>
  2066. <td className="px-4 py-3">{expense.date}</td>
  2067. <td className="px-4 py-3">{expense.supplier}</td>
  2068. <td className="px-4 py-3">{expense.category}</td>
  2069. <td className="px-4 py-3">{expense.itemDescription}</td>
  2070. <td className="px-4 py-3 font-semibold">${expense.cost.toFixed(2)}</td>
  2071. <td className="px-4 py-3 font-mono text-xs">{expense.materialId || '-'}</td>
  2072. </tr>
  2073. ))}
  2074. </tbody>
  2075. </table>
  2076. </div>
  2077. </div>
  2078. )}
  2079. </div>
  2080. )}
  2081.  
  2082. {/* MARKET RESEARCH TAB */}
  2083. {activeTab === 'market-research' && (
  2084. <div className="space-y-6">
  2085. <div className="bg-purple-50 rounded-lg border border-purple-200 p-6">
  2086. <h2 className="text-lg font-semibold text-gray-900 mb-2">Market Research</h2>
  2087. <p className="text-sm text-gray-600">Industry analysis based on major retailers.</p>
  2088. </div>
  2089.  
  2090. <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
  2091. <table className="w-full text-sm">
  2092. <thead className="bg-gray-50">
  2093. <tr>
  2094. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Size Category</th>
  2095. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Your Product</th>
  2096. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Your Price</th>
  2097. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Competitor Avg</th>
  2098. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Price Position</th>
  2099. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Suggestion</th>
  2100. </tr>
  2101. </thead>
  2102. <tbody className="divide-y divide-gray-200">
  2103. {MARKET_DATA.map((item, idx) => (
  2104. <tr key={idx}>
  2105. <td className="px-6 py-4">{item.sizeCategory}</td>
  2106. <td className="px-6 py-4 font-semibold">{item.yourProduct}</td>
  2107. <td className="px-6 py-4 text-green-600 font-semibold">${item.yourPrice.toFixed(2)}</td>
  2108. <td className="px-6 py-4">${item.competitorAvg.toFixed(2)}</td>
  2109. <td className="px-6 py-4">
  2110. <span className="px-2 py-1 bg-green-100 text-green-800 rounded text-xs font-semibold">
  2111. {item.pricePosition}
  2112. </span>
  2113. </td>
  2114. <td className="px-6 py-4">{item.suggestion}</td>
  2115. </tr>
  2116. ))}
  2117. </tbody>
  2118. </table>
  2119. </div>
  2120. </div>
  2121. )}
  2122. </div>
  2123. </div>
  2124. );
  2125. }
  2126.  
  2127. ReactDOM.render(<BusinessTracker />, document.getElementById('root'));
  2128. </script>
  2129. </body>
  2130. </html>
Add Comment
Please, Sign In to add comment