nickmf30

Untitled

Oct 26th, 2025
11
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 109.27 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 Final</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. </head>
  13. <body>
  14. <div id="root"></div>
  15.  
  16. <script type="text/babel">
  17. const { useState, useMemo, useEffect, useRef } = React;
  18.  
  19. // Initial materials data from image 1
  20. const INITIAL_MATERIALS = [
  21. { 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: '-' },
  22. { 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: '-' },
  23. { 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: '-' },
  24. { 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: '-' },
  25. { 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: '-' },
  26. { 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: '-' },
  27. { 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: '-' },
  28. { 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: '-' },
  29. { 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: '-' },
  30. { 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: '-' }
  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. size: '3.5 x 5.5',
  38. sellingPrice: 8.00,
  39. totalCost: 1.813,
  40. profit: 6.187,
  41. profitMargin: 77.3,
  42. timeToMake: 1.5,
  43. toMake1k: 162,
  44. materials: [
  45. { id: '1001', name: 'Pastel Sheets', quantity: 15.00, cpp: 0.043, materialCost: 0.638, type: 'Paper' },
  46. { id: '2001', name: 'Cream Cardstock Sheet', quantity: 1.00, cpp: 0.122, materialCost: 0.122, type: 'Cardstock' },
  47. { id: '2003', name: 'Cream Cardstock Sheets', quantity: 0.50, cpp: 0.246, materialCost: 0.123, type: 'Cardstock' },
  48. { id: '3001', name: 'Lineco PVA Glue', quantity: 0.30, cpp: 1.837, materialCost: 0.551, type: 'Glue' },
  49. { id: '3002', name: 'Glue Strips', quantity: 0.25, cpp: 0.224, materialCost: 0.056, type: 'Glue' },
  50. { id: '4001', name: 'Light Brown PUV Material', quantity: 38.50, cpp: 0.008, materialCost: 0.323, type: 'Cover' }
  51. ]
  52. },
  53. 'PB-BB-BVR1K-1300-0825': {
  54. name: 'One Beaver - 1300 - King',
  55. size: '8.5 x 11',
  56. sellingPrice: 25.00,
  57. totalCost: 11.478,
  58. profit: 13.522,
  59. profitMargin: 54.1,
  60. timeToMake: 3.5,
  61. toMake1k: 74,
  62. materials: [
  63. { id: '1001', name: 'Pastel Sheets', quantity: 100.00, cpp: 0.043, materialCost: 4.250, type: 'Paper' },
  64. { id: '2001', name: 'Cream Cardstock Sheet', quantity: 4.00, cpp: 0.122, materialCost: 0.489, type: 'Cardstock' },
  65. { id: '2002', name: 'Cream Cardstock Sheets', quantity: 4.00, cpp: 0.269, materialCost: 1.074, type: 'Cardstock' },
  66. { id: '2003', name: 'Cream Cardstock Sheets', quantity: 2.50, cpp: 0.246, materialCost: 0.616, type: 'Cardstock' },
  67. { id: '3001', name: 'Lineco PVA Glue', quantity: 2.00, cpp: 1.837, materialCost: 3.674, type: 'Glue' },
  68. { id: '3002', name: 'Glue Strips', quantity: 1.00, cpp: 0.224, materialCost: 0.224, type: 'Glue' },
  69. { id: '4001', name: 'Light Brown PUV Material', quantity: 137.00, cpp: 0.008, materialCost: 1.151, type: 'Cover' }
  70. ]
  71. },
  72. 'PB-BB-BVR1-1300-0825': {
  73. name: 'One Beaver - 1300',
  74. size: '7 x 10',
  75. sellingPrice: 20.00,
  76. totalCost: 8.377,
  77. profit: 11.623,
  78. profitMargin: 58.1,
  79. timeToMake: 2.5,
  80. toMake1k: 86,
  81. materials: [
  82. { id: '1001', name: 'Pastel Sheets', quantity: 80.00, cpp: 0.043, materialCost: 3.400, type: 'Paper' },
  83. { id: '2001', name: 'Cream Cardstock Sheet', quantity: 3.00, cpp: 0.122, materialCost: 0.367, type: 'Cardstock' },
  84. { id: '2002', name: 'Cream Cardstock Sheets', quantity: 3.00, cpp: 0.269, materialCost: 0.806, type: 'Cardstock' },
  85. { id: '2003', name: 'Cream Cardstock Sheets', quantity: 2.50, cpp: 0.246, materialCost: 0.616, type: 'Cardstock' },
  86. { id: '3001', name: 'Lineco PVA Glue', quantity: 1.12, cpp: 1.837, materialCost: 2.058, type: 'Glue' },
  87. { id: '3002', name: 'Glue Strips', quantity: 1.00, cpp: 0.224, materialCost: 0.224, type: 'Glue' },
  88. { id: '4001', name: 'Light Brown PUV Material', quantity: 108.00, cpp: 0.008, materialCost: 0.907, type: 'Cover' }
  89. ]
  90. },
  91. 'PB-BB-BVR1C-1300-0825': {
  92. name: 'One Beaver - 1300 - Companion',
  93. size: '4.25 x 7.25',
  94. sellingPrice: 13.00,
  95. totalCost: 4.609,
  96. profit: 8.391,
  97. profitMargin: 64.5,
  98. timeToMake: 2.0,
  99. toMake1k: 119,
  100. materials: [
  101. { id: '1001', name: 'Pastel Sheets', quantity: 40.00, cpp: 0.043, materialCost: 1.700, type: 'Paper' },
  102. { id: '2001', name: 'Cream Cardstock Sheet', quantity: 1.50, cpp: 0.122, materialCost: 0.183, type: 'Cardstock' },
  103. { id: '2002', name: 'Cream Cardstock Sheets', quantity: 2.00, cpp: 0.269, materialCost: 0.537, type: 'Cardstock' },
  104. { id: '2003', name: 'Cream Cardstock Sheets', quantity: 1.00, cpp: 0.246, materialCost: 0.246, type: 'Cardstock' },
  105. { id: '3001', name: 'Lineco PVA Glue', quantity: 0.60, cpp: 1.837, materialCost: 1.102, type: 'Glue' },
  106. { id: '3002', name: 'Glue Strips', quantity: 0.75, cpp: 0.224, materialCost: 0.168, type: 'Glue' },
  107. { id: '4001', name: 'Light Brown PUV Material', quantity: 80.00, cpp: 0.008, materialCost: 0.672, type: 'Cover' }
  108. ]
  109. },
  110. 'PB-BB-BVR1MB-1300-0825': {
  111. name: 'One Beaver - 1300 - Mini - Basic',
  112. size: '3.5 x 5.5',
  113. sellingPrice: 5.00,
  114. totalCost: 1.673,
  115. profit: 3.327,
  116. profitMargin: 66.5,
  117. timeToMake: 1.0,
  118. toMake1k: 301,
  119. materials: [
  120. { id: '1001', name: 'Pastel Sheets', quantity: 15.00, cpp: 0.043, materialCost: 0.638, type: 'Paper' },
  121. { id: '2001', name: 'Cream Cardstock Sheet', quantity: 0.50, cpp: 0.122, materialCost: 0.061, type: 'Cardstock' },
  122. { id: '3001', name: 'Lineco PVA Glue', quantity: 0.50, cpp: 1.837, materialCost: 0.919, type: 'Glue' },
  123. { id: '3002', name: 'Glue Strips', quantity: 0.25, cpp: 0.224, materialCost: 0.056, type: 'Glue' }
  124. ]
  125. },
  126. 'PB-BB-BVR1CB-1300-0825': {
  127. name: 'One Beaver - 1300 - Companion - Basic',
  128. size: '4.25 x 7.25',
  129. sellingPrice: 9.00,
  130. totalCost: 2.943,
  131. profit: 6.057,
  132. profitMargin: 67.3,
  133. timeToMake: 1.5,
  134. toMake1k: 165,
  135. materials: [
  136. { id: '1001', name: 'Pastel Sheets', quantity: 30.00, cpp: 0.043, materialCost: 1.275, type: 'Paper' },
  137. { id: '2001', name: 'Cream Cardstock Sheet', quantity: 1.00, cpp: 0.122, materialCost: 0.122, type: 'Cardstock' },
  138. { id: '3001', name: 'Lineco PVA Glue', quantity: 0.75, cpp: 1.837, materialCost: 1.378, type: 'Glue' },
  139. { id: '3002', name: 'Glue Strips', quantity: 0.75, cpp: 0.224, materialCost: 0.168, type: 'Glue' }
  140. ]
  141. },
  142. 'PB-BB-BVR1B-1300-0825': {
  143. name: 'One Beaver - 1300 - Basic',
  144. size: '7 x 10',
  145. sellingPrice: 15.00,
  146. totalCost: 4.917,
  147. profit: 10.083,
  148. profitMargin: 67.2,
  149. timeToMake: 2.0,
  150. toMake1k: 99,
  151. materials: [
  152. { id: '1001', name: 'Pastel Sheets', quantity: 60.00, cpp: 0.043, materialCost: 2.550, type: 'Paper' },
  153. { id: '2001', name: 'Cream Cardstock Sheet', quantity: 2.50, cpp: 0.122, materialCost: 0.306, type: 'Cardstock' },
  154. { id: '3001', name: 'Lineco PVA Glue', quantity: 1.00, cpp: 1.837, materialCost: 1.837, type: 'Glue' },
  155. { id: '3002', name: 'Glue Strips', quantity: 1.00, cpp: 0.224, materialCost: 0.224, type: 'Glue' }
  156. ]
  157. }
  158. };
  159.  
  160. const MARKET_DATA = [
  161. { 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' },
  162. { 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' },
  163. { 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' },
  164. { 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' }
  165. ];
  166.  
  167. function BusinessTracker() {
  168. const [activeTab, setActiveTab] = useState('dashboard');
  169.  
  170. // Materials State
  171. const [materials, setMaterials] = useState(INITIAL_MATERIALS);
  172. const [editingMaterial, setEditingMaterial] = useState(null);
  173. const [showAddMaterial, setShowAddMaterial] = useState(false);
  174. const [newMaterial, setNewMaterial] = useState({
  175. id: '', item: '', type: 'Paper', supplier: '', itemSize: '', grade: '',
  176. packCount: '', units: '', packCost: '', packCostAfterTax: '', cpp: '', source: '-'
  177. });
  178.  
  179. // Products State
  180. const [productMaterials, setProductMaterials] = useState(INITIAL_PRODUCT_MATERIALS);
  181. const [editingProduct, setEditingProduct] = useState(null);
  182. const [showAddProduct, setShowAddProduct] = useState(false);
  183. const [newProduct, setNewProduct] = useState({
  184. id: '', name: '', size: '', sellingPrice: '', timeToMake: '', materials: []
  185. });
  186. const [expandedProducts, setExpandedProducts] = useState({});
  187.  
  188. // Expense State
  189. const [expenses, setExpenses] = useState([]);
  190. const [expenseForm, setExpenseForm] = useState({
  191. date: new Date().toISOString().split('T')[0],
  192. supplier: '', category: '', itemDescription: '', cost: '', materialId: '', notes: ''
  193. });
  194.  
  195. // Sales State
  196. const [sales, setSales] = useState([]);
  197. const [salesForm, setSalesForm] = useState({
  198. date: new Date().toISOString().split('T')[0],
  199. location: '', product: '', units: '1', pricePerUnit: '',
  200. customerId: '', repeatCustomer: 'No', customerNotes: '',
  201. complaint: 'No', returned: 'No', refunded: 'No'
  202. });
  203.  
  204. // Estimator State
  205. const [estimatorMode, setEstimatorMode] = useState('profit-maximizer');
  206. const [productionTime, setProductionTime] = useState(40);
  207. const [premiumRatio, setPremiumRatio] = useState(50);
  208. const [manualQuantities, setManualQuantities] = useState({});
  209. const [savedScenarios, setSavedScenarios] = useState([]);
  210.  
  211. // Drag and drop refs
  212. const expenseTableRef = useRef(null);
  213. const salesTableRef = useRef(null);
  214.  
  215. const expenseCategories = [
  216. 'Materials - Paper', 'Materials - Cardstock', 'Materials - Glue', 'Materials - Cover',
  217. 'Tools & Equipment', 'Packaging', 'Shipping', 'Marketing', 'Booth Fees', 'Other'
  218. ];
  219.  
  220. // Dashboard Analytics
  221. const dashboardStats = useMemo(() => {
  222. const totalMaterials = materials.length;
  223. const totalProducts = Object.keys(productMaterials).length;
  224. const avgMargin = Object.values(productMaterials).reduce((sum, p) => sum + p.profitMargin, 0) / totalProducts || 0;
  225. const totalRevenue = sales.reduce((sum, sale) => sum + sale.total, 0);
  226. const totalExpenses = expenses.reduce((sum, exp) => sum + exp.cost, 0);
  227. const netProfit = totalRevenue - totalExpenses;
  228.  
  229. return { totalMaterials, totalProducts, avgMargin, totalRevenue, totalExpenses, netProfit };
  230. }, [materials, productMaterials, sales, expenses]);
  231.  
  232. // Add Material
  233. const addMaterial = () => {
  234. if (!newMaterial.id || !newMaterial.item || !newMaterial.packCount || !newMaterial.packCostAfterTax) {
  235. alert('Please fill in required fields: ID, Item, Pack Count, Pack Cost After Tax');
  236. return;
  237. }
  238.  
  239. const cpp = parseFloat(newMaterial.packCostAfterTax) / parseFloat(newMaterial.packCount);
  240.  
  241. setMaterials([...materials, {
  242. ...newMaterial,
  243. packCount: parseFloat(newMaterial.packCount),
  244. packCost: parseFloat(newMaterial.packCost) || 0,
  245. packCostAfterTax: parseFloat(newMaterial.packCostAfterTax),
  246. cpp: parseFloat(cpp.toFixed(3))
  247. }]);
  248.  
  249. setNewMaterial({
  250. id: '', item: '', type: 'Paper', supplier: '', itemSize: '', grade: '',
  251. packCount: '', units: '', packCost: '', packCostAfterTax: '', cpp: '', source: '-'
  252. });
  253. setShowAddMaterial(false);
  254. };
  255.  
  256. // Edit Material
  257. const updateMaterial = (id, updates) => {
  258. setMaterials(materials.map(m => {
  259. if (m.id === id) {
  260. const updated = { ...m, ...updates };
  261. if (updates.packCostAfterTax || updates.packCount) {
  262. updated.cpp = parseFloat((updated.packCostAfterTax / updated.packCount).toFixed(3));
  263. }
  264. return updated;
  265. }
  266. return m;
  267. }));
  268. };
  269.  
  270. // Delete Material
  271. const deleteMaterial = (id) => {
  272. if (confirm('Delete this material? This will affect products using it.')) {
  273. setMaterials(materials.filter(m => m.id !== id));
  274. }
  275. };
  276.  
  277. // Add Product
  278. const addProduct = () => {
  279. if (!newProduct.id || !newProduct.name || !newProduct.sellingPrice) {
  280. alert('Please fill in required fields: Product Number, Name, Selling Price');
  281. return;
  282. }
  283.  
  284. const totalCost = newProduct.materials.reduce((sum, m) => sum + m.materialCost, 0);
  285. const profit = parseFloat(newProduct.sellingPrice) - totalCost;
  286. const profitMargin = (profit / parseFloat(newProduct.sellingPrice)) * 100;
  287. const toMake1k = totalCost > 0 ? Math.ceil(1000 / profit) : 0;
  288.  
  289. setProductMaterials({
  290. ...productMaterials,
  291. [newProduct.id]: {
  292. name: newProduct.name,
  293. size: newProduct.size,
  294. sellingPrice: parseFloat(newProduct.sellingPrice),
  295. totalCost,
  296. profit,
  297. profitMargin,
  298. timeToMake: parseFloat(newProduct.timeToMake) || 2,
  299. toMake1k,
  300. materials: newProduct.materials
  301. }
  302. });
  303.  
  304. setNewProduct({ id: '', name: '', size: '', sellingPrice: '', timeToMake: '', materials: [] });
  305. setShowAddProduct(false);
  306. };
  307.  
  308. // Add material to new product
  309. const addMaterialToNewProduct = (materialId) => {
  310. const material = materials.find(m => m.id === materialId);
  311. if (!material) return;
  312.  
  313. const newMaterialEntry = {
  314. id: material.id,
  315. name: material.item,
  316. quantity: 1,
  317. cpp: material.cpp,
  318. materialCost: material.cpp * 1,
  319. type: material.type
  320. };
  321.  
  322. setNewProduct({
  323. ...newProduct,
  324. materials: [...newProduct.materials, newMaterialEntry]
  325. });
  326. };
  327.  
  328. // Update material in new product form
  329. const updateNewProductMaterial = (index, field, value) => {
  330. const updatedMaterials = [...newProduct.materials];
  331. updatedMaterials[index] = { ...updatedMaterials[index], [field]: parseFloat(value) || 0 };
  332.  
  333. if (field === 'quantity') {
  334. updatedMaterials[index].materialCost = updatedMaterials[index].quantity * updatedMaterials[index].cpp;
  335. }
  336.  
  337. setNewProduct({ ...newProduct, materials: updatedMaterials });
  338. };
  339.  
  340. // Remove material from new product
  341. const removeNewProductMaterial = (index) => {
  342. setNewProduct({
  343. ...newProduct,
  344. materials: newProduct.materials.filter((_, i) => i !== index)
  345. });
  346. };
  347.  
  348. // Add material to existing product
  349. const addMaterialToProduct = (productId, materialId) => {
  350. const material = materials.find(m => m.id === materialId);
  351. if (!material) return;
  352.  
  353. const product = productMaterials[productId];
  354. const newMaterialEntry = {
  355. id: material.id,
  356. name: material.item,
  357. quantity: 1,
  358. cpp: material.cpp,
  359. materialCost: material.cpp * 1,
  360. type: material.type
  361. };
  362.  
  363. const updatedMaterials = [...product.materials, newMaterialEntry];
  364. const totalCost = updatedMaterials.reduce((sum, m) => sum + m.materialCost, 0);
  365. const profit = product.sellingPrice - totalCost;
  366. const profitMargin = (profit / product.sellingPrice) * 100;
  367.  
  368. setProductMaterials({
  369. ...productMaterials,
  370. [productId]: {
  371. ...product,
  372. materials: updatedMaterials,
  373. totalCost,
  374. profit,
  375. profitMargin
  376. }
  377. });
  378. };
  379.  
  380. // Remove material from product
  381. const removeMaterialFromProduct = (productId, materialIndex) => {
  382. const product = productMaterials[productId];
  383. const updatedMaterials = product.materials.filter((_, i) => i !== materialIndex);
  384. const totalCost = updatedMaterials.reduce((sum, m) => sum + m.materialCost, 0);
  385. const profit = product.sellingPrice - totalCost;
  386. const profitMargin = (profit / product.sellingPrice) * 100;
  387.  
  388. setProductMaterials({
  389. ...productMaterials,
  390. [productId]: {
  391. ...product,
  392. materials: updatedMaterials,
  393. totalCost,
  394. profit,
  395. profitMargin
  396. }
  397. });
  398. };
  399.  
  400. // Delete Product
  401. const deleteProduct = (id) => {
  402. if (confirm('Delete this product?')) {
  403. const updated = { ...productMaterials };
  404. delete updated[id];
  405. setProductMaterials(updated);
  406. }
  407. };
  408.  
  409. // Update Product Materials
  410. const updateProductMaterial = (productId, materialIndex, field, value) => {
  411. const product = productMaterials[productId];
  412. const updatedMaterials = [...product.materials];
  413. updatedMaterials[materialIndex] = { ...updatedMaterials[materialIndex], [field]: parseFloat(value) || 0 };
  414.  
  415. if (field === 'quantity' || field === 'cpp') {
  416. updatedMaterials[materialIndex].materialCost = updatedMaterials[materialIndex].quantity * updatedMaterials[materialIndex].cpp;
  417. }
  418.  
  419. const totalCost = updatedMaterials.reduce((sum, m) => sum + m.materialCost, 0);
  420. const profit = product.sellingPrice - totalCost;
  421. const profitMargin = (profit / product.sellingPrice) * 100;
  422.  
  423. setProductMaterials({
  424. ...productMaterials,
  425. [productId]: {
  426. ...product,
  427. materials: updatedMaterials,
  428. totalCost,
  429. profit,
  430. profitMargin
  431. }
  432. });
  433. };
  434.  
  435. // Add expense/sale functions (same as before)
  436. const addExpense = () => {
  437. if (!expenseForm.supplier || !expenseForm.category || !expenseForm.cost) {
  438. alert('Please fill in required fields');
  439. return;
  440. }
  441. setExpenses([...expenses, { ...expenseForm, id: Date.now(), cost: parseFloat(expenseForm.cost) }]);
  442. setExpenseForm({ date: new Date().toISOString().split('T')[0], supplier: '', category: '', itemDescription: '', cost: '', materialId: '', notes: '' });
  443. };
  444.  
  445. const addSale = () => {
  446. if (!salesForm.location || !salesForm.product || !salesForm.pricePerUnit) {
  447. alert('Please fill in required fields');
  448. return;
  449. }
  450. const units = parseInt(salesForm.units);
  451. const price = parseFloat(salesForm.pricePerUnit);
  452. setSales([...sales, { ...salesForm, id: Date.now(), units, pricePerUnit: price, total: units * price }]);
  453. setSalesForm({ date: new Date().toISOString().split('T')[0], location: '', product: '', units: '1', pricePerUnit: '', customerId: '', repeatCustomer: 'No', customerNotes: '', complaint: 'No', returned: 'No', refunded: 'No' });
  454. };
  455.  
  456. // Estimator with toggleable quantities
  457. const estimatorResults = useMemo(() => {
  458. const allProducts = Object.entries(productMaterials).map(([id, data]) => ({ id, ...data }));
  459. let production = [];
  460.  
  461. if (estimatorMode === 'manual') {
  462. production = allProducts.map(product => ({
  463. ...product,
  464. units: manualQuantities[product.id] || 0,
  465. productProfit: product.profit * (manualQuantities[product.id] || 0),
  466. productRevenue: product.sellingPrice * (manualQuantities[product.id] || 0)
  467. }));
  468. } else if (estimatorMode === 'profit-maximizer') {
  469. const sorted = [...allProducts].sort((a, b) => b.profitMargin - a.profitMargin);
  470. let remainingTime = productionTime;
  471.  
  472. for (const product of sorted) {
  473. if (remainingTime <= 0) break;
  474. const unitsToMake = Math.min(Math.floor(remainingTime / product.timeToMake), 50);
  475. if (unitsToMake > 0) {
  476. production.push({
  477. ...product,
  478. units: unitsToMake,
  479. productProfit: product.profit * unitsToMake,
  480. productRevenue: product.sellingPrice * unitsToMake
  481. });
  482. remainingTime -= unitsToMake * product.timeToMake;
  483. }
  484. }
  485. } else if (estimatorMode === 'premium-basic-ratio') {
  486. const premiumProducts = allProducts.filter(p => !p.name.includes('Basic'));
  487. const basicProducts = allProducts.filter(p => p.name.includes('Basic'));
  488.  
  489. premiumProducts.forEach(product => {
  490. const units = Math.floor((premiumRatio / 100) * 20);
  491. production.push({
  492. ...product,
  493. units,
  494. productProfit: product.profit * units,
  495. productRevenue: product.sellingPrice * units
  496. });
  497. });
  498.  
  499. basicProducts.forEach(product => {
  500. const units = Math.floor(((100 - premiumRatio) / 100) * 20);
  501. production.push({
  502. ...product,
  503. units,
  504. productProfit: product.profit * units,
  505. productRevenue: product.sellingPrice * units
  506. });
  507. });
  508. }
  509.  
  510. const totalUnits = production.reduce((sum, p) => sum + p.units, 0);
  511. const totalProfit = production.reduce((sum, p) => sum + p.productProfit, 0);
  512. const totalRevenue = production.reduce((sum, p) => sum + p.productRevenue, 0);
  513. const timeEstimate = production.reduce((sum, p) => sum + (p.units * p.timeToMake), 0);
  514.  
  515. return { production, totalUnits, totalProfit, totalRevenue, timeEstimate };
  516. }, [estimatorMode, productionTime, premiumRatio, manualQuantities, productMaterials]);
  517.  
  518. // Master export/import
  519. const masterExport = () => {
  520. const masterData = { expenses, sales, productMaterials, materials, savedScenarios, exportedAt: new Date().toISOString() };
  521. const dataStr = JSON.stringify(masterData, null, 2);
  522. const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
  523. const link = document.createElement('a');
  524. link.setAttribute('href', dataUri);
  525. link.setAttribute('download', `one-beaver-master-${new Date().toISOString().split('T')[0]}.json`);
  526. link.click();
  527. };
  528.  
  529. const masterImport = (e) => {
  530. const file = e.target.files[0];
  531. if (file) {
  532. const reader = new FileReader();
  533. reader.onload = (event) => {
  534. try {
  535. const imported = JSON.parse(event.target.result);
  536. if (imported.expenses) setExpenses(imported.expenses);
  537. if (imported.sales) setSales(imported.sales);
  538. if (imported.productMaterials) setProductMaterials(imported.productMaterials);
  539. if (imported.materials) setMaterials(imported.materials);
  540. if (imported.savedScenarios) setSavedScenarios(imported.savedScenarios);
  541. alert('All data imported successfully!');
  542. } catch (err) {
  543. alert('Error importing file.');
  544. }
  545. };
  546. reader.readAsText(file);
  547. }
  548. };
  549.  
  550. const products = Object.keys(productMaterials).map(id => ({ id, name: productMaterials[id].name }));
  551.  
  552. return (
  553. <div className="min-h-screen bg-gray-50">
  554. {/* Header */}
  555. <div className="bg-white border-b border-gray-200">
  556. <div className="max-w-7xl mx-auto px-4 py-4">
  557. <div className="flex justify-between items-center">
  558. <h1 className="text-2xl font-semibold text-gray-900">One Beaver 1300 - Business Tracker</h1>
  559. <div className="flex gap-2">
  560. <button onClick={masterExport} className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 text-sm font-semibold">
  561. ⬇ Master Export
  562. </button>
  563. <label className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 cursor-pointer text-sm font-semibold">
  564. ⬆ Master Import
  565. <input type="file" accept=".json" onChange={masterImport} className="hidden" />
  566. </label>
  567. </div>
  568. </div>
  569. </div>
  570. </div>
  571.  
  572. {/* Navigation Tabs */}
  573. <div className="bg-white border-b border-gray-200">
  574. <div className="max-w-7xl mx-auto px-4">
  575. <nav className="flex space-x-8">
  576. {['dashboard', 'materials', 'products', 'estimator', 'sales', 'expenses', 'market-research'].map(tab => (
  577. <button
  578. key={tab}
  579. onClick={() => setActiveTab(tab)}
  580. className={`py-4 px-1 border-b-2 font-medium text-sm ${
  581. activeTab === tab ? 'border-amber-700 text-amber-700' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
  582. }`}
  583. >
  584. {tab === 'dashboard' ? '📊 Overview' : tab.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')}
  585. </button>
  586. ))}
  587. </nav>
  588. </div>
  589. </div>
  590.  
  591. {/* Main Content */}
  592. <div className="max-w-7xl mx-auto px-4 py-6">
  593. {/* DASHBOARD TAB */}
  594. {activeTab === 'dashboard' && (
  595. <div className="space-y-6">
  596. <div className="bg-blue-50 rounded-lg border border-blue-200 p-6">
  597. <h2 className="text-lg font-semibold text-gray-900 mb-2">Business Dashboard</h2>
  598. <p className="text-sm text-gray-600">
  599. Master Data System: Start by setting up your <span className="font-semibold">Materials Library</span> and <span className="font-semibold">Products</span>.
  600. All other tabs will automatically pull data from these master sources.
  601. </p>
  602. </div>
  603.  
  604. <div className="grid grid-cols-3 gap-4">
  605. <div className="bg-white rounded-lg border-l-4 border-blue-500 p-6">
  606. <div className="text-sm text-gray-600 mb-1">Total Materials</div>
  607. <div className="text-3xl font-semibold text-blue-600">{dashboardStats.totalMaterials}</div>
  608. </div>
  609. <div className="bg-white rounded-lg border-l-4 border-red-500 p-6">
  610. <div className="text-sm text-gray-600 mb-1">Total Products</div>
  611. <div className="text-3xl font-semibold text-red-600">{dashboardStats.totalProducts}</div>
  612. </div>
  613. <div className="bg-white rounded-lg border-l-4 border-green-500 p-6">
  614. <div className="text-sm text-gray-600 mb-1">Avg Product Margin</div>
  615. <div className="text-3xl font-semibold text-green-600">{dashboardStats.avgMargin.toFixed(1)}%</div>
  616. </div>
  617. </div>
  618.  
  619. <div className="grid grid-cols-3 gap-4">
  620. <div className="bg-green-50 rounded-lg border border-green-200 p-6">
  621. <div className="text-sm text-gray-600 mb-1">Total Revenue</div>
  622. <div className="text-3xl font-semibold text-green-600">${dashboardStats.totalRevenue.toFixed(2)}</div>
  623. </div>
  624. <div className="bg-yellow-50 rounded-lg border border-yellow-200 p-6">
  625. <div className="text-sm text-gray-600 mb-1">Total Expenses</div>
  626. <div className="text-3xl font-semibold text-yellow-600">${dashboardStats.totalExpenses.toFixed(2)}</div>
  627. </div>
  628. <div className="bg-blue-50 rounded-lg border border-blue-200 p-6">
  629. <div className="text-sm text-gray-600 mb-1">Net Profit</div>
  630. <div className="text-3xl font-semibold text-blue-600">${dashboardStats.netProfit.toFixed(2)}</div>
  631. </div>
  632. </div>
  633.  
  634. <div className="bg-white rounded-lg border border-gray-200 p-6">
  635. <h3 className="text-lg font-semibold text-gray-900 mb-4">Quick Stats</h3>
  636. <div className="grid grid-cols-2 gap-4 text-sm">
  637. <div className="flex justify-between">
  638. <span className="text-gray-600">Total Sales Recorded:</span>
  639. <span className="font-semibold">{sales.length}</span>
  640. </div>
  641. <div className="flex justify-between">
  642. <span className="text-gray-600">Total Expenses Recorded:</span>
  643. <span className="font-semibold">{expenses.length}</span>
  644. </div>
  645. <div className="flex justify-between">
  646. <span className="text-gray-600">Highest Margin Product:</span>
  647. <span className="font-semibold">
  648. {Object.values(productMaterials).sort((a, b) => b.profitMargin - a.profitMargin)[0]?.name || 'N/A'}
  649. </span>
  650. </div>
  651. <div className="flex justify-between">
  652. <span className="text-gray-600">Most Expensive Material:</span>
  653. <span className="font-semibold">
  654. {materials.sort((a, b) => b.cpp - a.cpp)[0]?.item || 'N/A'}
  655. </span>
  656. </div>
  657. </div>
  658. </div>
  659. </div>
  660. )}
  661.  
  662. {/* MATERIALS TAB */}
  663. {activeTab === 'materials' && (
  664. <div className="space-y-6">
  665. <div className="bg-blue-50 rounded-lg border border-blue-200 p-4">
  666. <p className="text-sm text-gray-700">
  667. <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.
  668. </p>
  669. </div>
  670.  
  671. <div className="flex gap-3">
  672. <button
  673. onClick={() => setShowAddMaterial(true)}
  674. className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 font-semibold"
  675. >
  676. ➕ Add New Material
  677. </button>
  678. <button
  679. onClick={() => {
  680. const dataStr = JSON.stringify(materials, null, 2);
  681. const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
  682. const link = document.createElement('a');
  683. link.setAttribute('href', dataUri);
  684. link.setAttribute('download', `materials-${new Date().toISOString().split('T')[0]}.json`);
  685. link.click();
  686. }}
  687. className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
  688. >
  689. Export Materials
  690. </button>
  691. </div>
  692.  
  693. {showAddMaterial && (
  694. <div className="bg-white rounded-lg border border-gray-200 p-6">
  695. <h3 className="text-lg font-semibold text-gray-900 mb-4">Add New Material</h3>
  696. <div className="grid grid-cols-4 gap-4 mb-4">
  697. <div>
  698. <label className="block text-sm font-medium text-gray-700 mb-1">ID *</label>
  699. <input
  700. type="text"
  701. value={newMaterial.id}
  702. onChange={(e) => setNewMaterial({...newMaterial, id: e.target.value})}
  703. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  704. placeholder="1001"
  705. />
  706. </div>
  707. <div>
  708. <label className="block text-sm font-medium text-gray-700 mb-1">Item *</label>
  709. <input
  710. type="text"
  711. value={newMaterial.item}
  712. onChange={(e) => setNewMaterial({...newMaterial, item: e.target.value})}
  713. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  714. />
  715. </div>
  716. <div>
  717. <label className="block text-sm font-medium text-gray-700 mb-1">Type</label>
  718. <select
  719. value={newMaterial.type}
  720. onChange={(e) => setNewMaterial({...newMaterial, type: e.target.value})}
  721. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  722. >
  723. <option>Paper</option>
  724. <option>Cardstock</option>
  725. <option>Glue</option>
  726. <option>Cover</option>
  727. <option>Ribbon</option>
  728. <option>Thread</option>
  729. <option>Cloth</option>
  730. </select>
  731. </div>
  732. <div>
  733. <label className="block text-sm font-medium text-gray-700 mb-1">Supplier</label>
  734. <input
  735. type="text"
  736. value={newMaterial.supplier}
  737. onChange={(e) => setNewMaterial({...newMaterial, supplier: e.target.value})}
  738. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  739. />
  740. </div>
  741. </div>
  742. <div className="grid grid-cols-4 gap-4 mb-4">
  743. <div>
  744. <label className="block text-sm font-medium text-gray-700 mb-1">Item Size</label>
  745. <input
  746. type="text"
  747. value={newMaterial.itemSize}
  748. onChange={(e) => setNewMaterial({...newMaterial, itemSize: e.target.value})}
  749. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  750. />
  751. </div>
  752. <div>
  753. <label className="block text-sm font-medium text-gray-700 mb-1">Grade</label>
  754. <input
  755. type="text"
  756. value={newMaterial.grade}
  757. onChange={(e) => setNewMaterial({...newMaterial, grade: e.target.value})}
  758. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  759. />
  760. </div>
  761. <div>
  762. <label className="block text-sm font-medium text-gray-700 mb-1">Pack Count *</label>
  763. <input
  764. type="number"
  765. value={newMaterial.packCount}
  766. onChange={(e) => setNewMaterial({...newMaterial, packCount: e.target.value})}
  767. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  768. />
  769. </div>
  770. <div>
  771. <label className="block text-sm font-medium text-gray-700 mb-1">Units</label>
  772. <input
  773. type="text"
  774. value={newMaterial.units}
  775. onChange={(e) => setNewMaterial({...newMaterial, units: e.target.value})}
  776. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  777. placeholder="Sheets"
  778. />
  779. </div>
  780. </div>
  781. <div className="grid grid-cols-3 gap-4 mb-4">
  782. <div>
  783. <label className="block text-sm font-medium text-gray-700 mb-1">Pack Cost</label>
  784. <input
  785. type="number"
  786. step="0.01"
  787. value={newMaterial.packCost}
  788. onChange={(e) => setNewMaterial({...newMaterial, packCost: e.target.value})}
  789. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  790. />
  791. </div>
  792. <div>
  793. <label className="block text-sm font-medium text-gray-700 mb-1">Pack Cost After Tax *</label>
  794. <input
  795. type="number"
  796. step="0.01"
  797. value={newMaterial.packCostAfterTax}
  798. onChange={(e) => setNewMaterial({...newMaterial, packCostAfterTax: e.target.value})}
  799. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  800. />
  801. </div>
  802. <div>
  803. <label className="block text-sm font-medium text-gray-700 mb-1">Source</label>
  804. <input
  805. type="text"
  806. value={newMaterial.source}
  807. onChange={(e) => setNewMaterial({...newMaterial, source: e.target.value})}
  808. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  809. />
  810. </div>
  811. </div>
  812. <div className="flex gap-3">
  813. <button
  814. onClick={addMaterial}
  815. className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700"
  816. >
  817. Add Material
  818. </button>
  819. <button
  820. onClick={() => setShowAddMaterial(false)}
  821. className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400"
  822. >
  823. Cancel
  824. </button>
  825. </div>
  826. </div>
  827. )}
  828.  
  829. <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
  830. <div className="overflow-x-auto">
  831. <table className="w-full text-sm">
  832. <thead className="bg-gray-50">
  833. <tr>
  834. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th>
  835. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Item</th>
  836. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Supplier</th>
  837. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Item Size</th>
  838. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Grade</th>
  839. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Pack Count</th>
  840. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Units</th>
  841. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Pack Cost</th>
  842. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Pack Cost After Tax</th>
  843. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">CPP</th>
  844. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Source</th>
  845. <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
  846. </tr>
  847. </thead>
  848. <tbody className="divide-y divide-gray-200">
  849. {materials.map(material => (
  850. <tr key={material.id} className="hover:bg-gray-50">
  851. <td className="px-4 py-3">
  852. <span className={`px-2 py-1 rounded text-xs font-semibold ${
  853. material.type === 'Paper' ? 'bg-blue-100 text-blue-800' :
  854. material.type === 'Cardstock' ? 'bg-cyan-100 text-cyan-800' :
  855. material.type === 'Glue' ? 'bg-teal-100 text-teal-800' :
  856. material.type === 'Cover' ? 'bg-yellow-100 text-yellow-800' :
  857. material.type === 'Ribbon' ? 'bg-purple-100 text-purple-800' :
  858. material.type === 'Thread' ? 'bg-green-100 text-green-800' :
  859. 'bg-gray-100 text-gray-800'
  860. }`}>
  861. {material.id}
  862. </span>
  863. </td>
  864. <td className="px-4 py-3 font-semibold">{material.item}</td>
  865. <td className="px-4 py-3">{material.supplier}</td>
  866. <td className="px-4 py-3">{material.itemSize}</td>
  867. <td className="px-4 py-3">{material.grade}</td>
  868. <td className="px-4 py-3">{material.packCount}</td>
  869. <td className="px-4 py-3">{material.units}</td>
  870. <td className="px-4 py-3">${material.packCost.toFixed(2)}</td>
  871. <td className="px-4 py-3">${material.packCostAfterTax.toFixed(2)}</td>
  872. <td className="px-4 py-3 font-mono font-semibold">${material.cpp.toFixed(3)}</td>
  873. <td className="px-4 py-3">{material.source}</td>
  874. <td className="px-4 py-3">
  875. <div className="flex gap-2">
  876. <button
  877. onClick={() => setEditingMaterial(material.id)}
  878. className="px-3 py-1 bg-blue-600 text-white rounded text-xs hover:bg-blue-700"
  879. >
  880. Edit
  881. </button>
  882. <button
  883. onClick={() => deleteMaterial(material.id)}
  884. className="px-3 py-1 bg-red-600 text-white rounded text-xs hover:bg-red-700"
  885. >
  886. Delete
  887. </button>
  888. </div>
  889. </td>
  890. </tr>
  891. ))}
  892. </tbody>
  893. </table>
  894. </div>
  895. </div>
  896. </div>
  897. )}
  898.  
  899. {/* PRODUCTS TAB */}
  900. {activeTab === 'products' && (
  901. <div className="space-y-6">
  902. <div className="bg-blue-50 rounded-lg border border-blue-200 p-4">
  903. <p className="text-sm text-gray-700">
  904. <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.
  905. </p>
  906. </div>
  907.  
  908. <div className="flex gap-3">
  909. <button
  910. onClick={() => setShowAddProduct(true)}
  911. className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 font-semibold"
  912. >
  913. ➕ Add New Product
  914. </button>
  915. <button
  916. onClick={() => {
  917. const dataStr = JSON.stringify(productMaterials, null, 2);
  918. const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
  919. const link = document.createElement('a');
  920. link.setAttribute('href', dataUri);
  921. link.setAttribute('download', `products-${new Date().toISOString().split('T')[0]}.json`);
  922. link.click();
  923. }}
  924. className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
  925. >
  926. Export Products
  927. </button>
  928. </div>
  929.  
  930. {showAddProduct && (
  931. <div className="bg-white rounded-lg border border-gray-200 p-6">
  932. <h3 className="text-lg font-semibold text-gray-900 mb-4">Add New Product</h3>
  933.  
  934. <div className="grid grid-cols-3 gap-4 mb-4">
  935. <div>
  936. <label className="block text-sm font-medium text-gray-700 mb-1">Product Number *</label>
  937. <input
  938. type="text"
  939. value={newProduct.id}
  940. onChange={(e) => setNewProduct({...newProduct, id: e.target.value})}
  941. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  942. placeholder="PB-BB-XXX-1300-0825"
  943. />
  944. </div>
  945. <div>
  946. <label className="block text-sm font-medium text-gray-700 mb-1">Product Name *</label>
  947. <input
  948. type="text"
  949. value={newProduct.name}
  950. onChange={(e) => setNewProduct({...newProduct, name: e.target.value})}
  951. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  952. placeholder="One Beaver - Mini"
  953. />
  954. </div>
  955. <div>
  956. <label className="block text-sm font-medium text-gray-700 mb-1">Size</label>
  957. <input
  958. type="text"
  959. value={newProduct.size}
  960. onChange={(e) => setNewProduct({...newProduct, size: e.target.value})}
  961. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  962. placeholder="3.5 x 5.5"
  963. />
  964. </div>
  965. </div>
  966.  
  967. <div className="grid grid-cols-2 gap-4 mb-4">
  968. <div>
  969. <label className="block text-sm font-medium text-gray-700 mb-1">Selling Price *</label>
  970. <input
  971. type="number"
  972. step="0.01"
  973. value={newProduct.sellingPrice}
  974. onChange={(e) => setNewProduct({...newProduct, sellingPrice: e.target.value})}
  975. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  976. placeholder="8.00"
  977. />
  978. </div>
  979. <div>
  980. <label className="block text-sm font-medium text-gray-700 mb-1">Time to Make (hours)</label>
  981. <input
  982. type="number"
  983. step="0.5"
  984. value={newProduct.timeToMake}
  985. onChange={(e) => setNewProduct({...newProduct, timeToMake: e.target.value})}
  986. className="w-full px-3 py-2 border border-gray-300 rounded-md"
  987. placeholder="2.0"
  988. />
  989. </div>
  990. </div>
  991.  
  992. <div className="mb-4">
  993. <div className="flex justify-between items-center mb-2">
  994. <label className="block text-sm font-medium text-gray-700">Materials</label>
  995. <select
  996. onChange={(e) => {
  997. if (e.target.value) {
  998. addMaterialToNewProduct(e.target.value);
  999. e.target.value = '';
  1000. }
  1001. }}
  1002. className="px-3 py-1 border border-gray-300 rounded-md text-sm"
  1003. >
  1004. <option value="">+ Add Material</option>
  1005. {materials.map(mat => (
  1006. <option key={mat.id} value={mat.id}>
  1007. {mat.id} - {mat.item} (${mat.cpp.toFixed(3)})
  1008. </option>
  1009. ))}
  1010. </select>
  1011. </div>
  1012.  
  1013. {newProduct.materials.length > 0 && (
  1014. <table className="w-full text-sm border border-gray-200">
  1015. <thead className="bg-gray-50">
  1016. <tr>
  1017. <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Material</th>
  1018. <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Quantity</th>
  1019. <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">CPP</th>
  1020. <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Cost</th>
  1021. <th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Actions</th>
  1022. </tr>
  1023. </thead>
  1024. <tbody>
  1025. {newProduct.materials.map((mat, idx) => (
  1026. <tr key={idx} className="border-t">
  1027. <td className="px-3 py-2">{mat.name}</td>
  1028. <td className="px-3 py-2">
  1029. <input
  1030. type="number"
  1031. step="0.01"
  1032. value={mat.quantity}
  1033. onChange={(e) => updateNewProductMaterial(idx, 'quantity', e.target.value)}
  1034. className="w-20 px-2 py-1 border border-gray-300 rounded"
  1035. />
  1036. </td>
  1037. <td className="px-3 py-2 font-mono">${mat.cpp.toFixed(3)}</td>
  1038. <td className="px-3 py-2 font-semibold">${mat.materialCost.toFixed(3)}</td>
  1039. <td className="px-3 py-2">
  1040. <button
  1041. onClick={() => removeNewProductMaterial(idx)}
  1042. className="px-2 py-1 bg-red-600 text-white rounded text-xs hover:bg-red-700"
  1043. >
  1044. Remove
  1045. </button>
  1046. </td>
  1047. </tr>
  1048. ))}
  1049. <tr className="border-t bg-gray-50 font-semibold">
  1050. <td className="px-3 py-2" colSpan="3">Total Cost</td>
  1051. <td className="px-3 py-2">${newProduct.materials.reduce((sum, m) => sum + m.materialCost, 0).toFixed(2)}</td>
  1052. <td></td>
  1053. </tr>
  1054. </tbody>
  1055. </table>
  1056. )}
  1057. </div>
  1058.  
  1059. <div className="flex gap-3">
  1060. <button
  1061. onClick={addProduct}
  1062. className="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700"
  1063. >
  1064. Add Product
  1065. </button>
  1066. <button
  1067. onClick={() => {
  1068. setShowAddProduct(false);
  1069. setNewProduct({ id: '', name: '', size: '', sellingPrice: '', timeToMake: '', materials: [] });
  1070. }}
  1071. className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md hover:bg-gray-400"
  1072. >
  1073. Cancel
  1074. </button>
  1075. </div>
  1076. </div>
  1077. )}
  1078.  
  1079. <div className="space-y-3">
  1080. {Object.entries(productMaterials).map(([productId, product]) => (
  1081. <div key={productId} className="bg-white rounded-lg border border-gray-200">
  1082. <div
  1083. className="px-6 py-4 bg-gray-50 border-b border-gray-200 flex justify-between items-center cursor-pointer hover:bg-gray-100"
  1084. onClick={() => setExpandedProducts({...expandedProducts, [productId]: !expandedProducts[productId]})}
  1085. >
  1086. <div className="flex items-center gap-3">
  1087. <span className="text-lg">{expandedProducts[productId] ? '▼' : '▶'}</span>
  1088. <div>
  1089. <h3 className="text-lg font-semibold text-gray-900">{product.name}</h3>
  1090. <p className="text-sm text-gray-600">
  1091. {product.size} | <span className="font-mono">{productId}</span> | Time to Make: {product.timeToMake}hrs
  1092. </p>
  1093. </div>
  1094. </div>
  1095. <div className="flex items-center gap-6">
  1096. <div className="text-right">
  1097. <div className="text-sm text-gray-600">Selling Price</div>
  1098. <div className="text-xl font-semibold text-gray-900">${product.sellingPrice.toFixed(2)}</div>
  1099. </div>
  1100. <div className="text-right">
  1101. <div className="text-sm text-gray-600">Total Cost</div>
  1102. <div className="text-xl font-semibold text-gray-900">${product.totalCost.toFixed(2)}</div>
  1103. </div>
  1104. <div className="text-right">
  1105. <div className="text-sm text-gray-600">Profit</div>
  1106. <div className="text-xl font-semibold text-green-600">${product.profit.toFixed(2)}</div>
  1107. </div>
  1108. <div className="text-right">
  1109. <div className="text-sm text-gray-600">Margin</div>
  1110. <div className={`text-xl font-semibold px-3 py-1 rounded ${
  1111. product.profitMargin >= 70 ? 'bg-green-100 text-green-800' :
  1112. product.profitMargin >= 60 ? 'bg-green-50 text-green-700' : 'bg-yellow-50 text-yellow-700'
  1113. }`}>
  1114. {product.profitMargin.toFixed(1)}%
  1115. </div>
  1116. </div>
  1117. <div className="flex gap-2">
  1118. <select
  1119. onChange={(e) => {
  1120. e.stopPropagation();
  1121. if (e.target.value) {
  1122. addMaterialToProduct(productId, e.target.value);
  1123. e.target.value = '';
  1124. }
  1125. }}
  1126. className="px-3 py-1 border border-green-600 bg-white text-green-700 rounded text-sm hover:bg-green-50"
  1127. onClick={(e) => e.stopPropagation()}
  1128. >
  1129. <option value="">➕ Add Material</option>
  1130. {materials.filter(mat => !product.materials.some(pm => pm.id === mat.id)).map(mat => (
  1131. <option key={mat.id} value={mat.id}>
  1132. {mat.id} - {mat.item} (${mat.cpp.toFixed(3)})
  1133. </option>
  1134. ))}
  1135. </select>
  1136. <button
  1137. onClick={(e) => {
  1138. e.stopPropagation();
  1139. deleteProduct(productId);
  1140. }}
  1141. className="px-3 py-1 bg-red-600 text-white rounded text-sm hover:bg-red-700"
  1142. >
  1143. Delete
  1144. </button>
  1145. </div>
  1146. </div>
  1147. </div>
  1148.  
  1149. {expandedProducts[productId] && (
  1150. <div className="p-6">
  1151. <h4 className="text-md font-semibold text-gray-900 mb-3">Materials Breakdown</h4>
  1152. <table className="w-full text-sm">
  1153. <thead className="bg-gray-50">
  1154. <tr>
  1155. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Material ID</th>
  1156. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Material</th>
  1157. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Quantity</th>
  1158. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">CPP</th>
  1159. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Material Cost</th>
  1160. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">% of Total</th>
  1161. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
  1162. </tr>
  1163. </thead>
  1164. <tbody className="divide-y divide-gray-200">
  1165. {product.materials.map((material, idx) => (
  1166. <tr key={idx}>
  1167. <td className="px-4 py-2 font-mono text-xs">{material.id}</td>
  1168. <td className="px-4 py-2">{material.name}</td>
  1169. <td className="px-4 py-2">
  1170. <input
  1171. type="number"
  1172. step="0.01"
  1173. value={material.quantity}
  1174. onChange={(e) => updateProductMaterial(productId, idx, 'quantity', e.target.value)}
  1175. className="w-24 px-2 py-1 border border-gray-300 rounded"
  1176. />
  1177. </td>
  1178. <td className="px-4 py-2 font-mono">${material.cpp.toFixed(3)}</td>
  1179. <td className="px-4 py-2 font-semibold">${material.materialCost.toFixed(3)}</td>
  1180. <td className="px-4 py-2">
  1181. {((material.materialCost / product.totalCost) * 100).toFixed(1)}%
  1182. </td>
  1183. <td className="px-4 py-2">
  1184. <button
  1185. onClick={() => removeMaterialFromProduct(productId, idx)}
  1186. className="px-2 py-1 bg-red-600 text-white rounded text-xs hover:bg-red-700"
  1187. >
  1188. Remove
  1189. </button>
  1190. </td>
  1191. </tr>
  1192. ))}
  1193. </tbody>
  1194. </table>
  1195. </div>
  1196. )}
  1197. </div>
  1198. ))}
  1199. </div>
  1200. </div>
  1201. )}
  1202.  
  1203. {/* ESTIMATOR TAB */}
  1204. {activeTab === 'estimator' && (
  1205. <div className="space-y-6">
  1206. <div className="bg-amber-50 rounded-lg border border-amber-200 p-6">
  1207. <h2 className="text-lg font-semibold text-gray-900 mb-2">Production Estimator</h2>
  1208. <p className="text-sm text-gray-600">
  1209. Choose a strategy to calculate your optimal production mix, or manually set quantities for each product.
  1210. </p>
  1211. </div>
  1212.  
  1213. <div className="bg-white rounded-lg border border-gray-200 p-6">
  1214. <h3 className="text-md font-semibold text-gray-900 mb-4">Select Strategy</h3>
  1215. <div className="grid grid-cols-3 gap-4">
  1216. <button
  1217. onClick={() => setEstimatorMode('manual')}
  1218. className={`p-4 border-2 rounded-lg text-left ${
  1219. estimatorMode === 'manual' ? 'border-amber-700 bg-amber-50' : 'border-gray-200 hover:border-gray-300'
  1220. }`}
  1221. >
  1222. <div className="font-semibold text-gray-900 mb-1">Manual Quantities</div>
  1223. <div className="text-xs text-gray-600">Set specific quantities for each product in the table below.</div>
  1224. </button>
  1225. <button
  1226. onClick={() => setEstimatorMode('profit-maximizer')}
  1227. className={`p-4 border-2 rounded-lg text-left ${
  1228. estimatorMode === 'profit-maximizer' ? 'border-amber-700 bg-amber-50' : 'border-gray-200 hover:border-gray-300'
  1229. }`}
  1230. >
  1231. <div className="font-semibold text-gray-900 mb-1">Profit Maximizer</div>
  1232. <div className="text-xs text-gray-600">Auto-calculate optimal mix within your time budget.</div>
  1233. </button>
  1234. <button
  1235. onClick={() => setEstimatorMode('premium-basic-ratio')}
  1236. className={`p-4 border-2 rounded-lg text-left ${
  1237. estimatorMode === 'premium-basic-ratio' ? 'border-amber-700 bg-amber-50' : 'border-gray-200 hover:border-gray-300'
  1238. }`}
  1239. >
  1240. <div className="font-semibold text-gray-900 mb-1">Premium/Basic Ratio</div>
  1241. <div className="text-xs text-gray-600">Distribute production by premium vs basic focus.</div>
  1242. </button>
  1243. </div>
  1244. </div>
  1245.  
  1246. {estimatorMode !== 'manual' && (
  1247. <div className="bg-white rounded-lg border border-gray-200 p-6">
  1248. <h3 className="text-md font-semibold text-gray-900 mb-4">Configuration</h3>
  1249. {estimatorMode === 'profit-maximizer' && (
  1250. <div className="mb-4">
  1251. <label className="block text-sm font-medium text-gray-700 mb-2">
  1252. Available Production Time: {productionTime} hours
  1253. </label>
  1254. <input
  1255. type="range"
  1256. min="1"
  1257. max="160"
  1258. value={productionTime}
  1259. onChange={(e) => setProductionTime(parseInt(e.target.value))}
  1260. className="w-full"
  1261. />
  1262. </div>
  1263. )}
  1264. {estimatorMode === 'premium-basic-ratio' && (
  1265. <div className="mb-4">
  1266. <label className="block text-sm font-medium text-gray-700 mb-2">
  1267. Premium: {premiumRatio}% | Basic: {100 - premiumRatio}%
  1268. </label>
  1269. <input
  1270. type="range"
  1271. min="0"
  1272. max="100"
  1273. value={premiumRatio}
  1274. onChange={(e) => setPremiumRatio(parseInt(e.target.value))}
  1275. className="w-full"
  1276. />
  1277. </div>
  1278. )}
  1279. </div>
  1280. )}
  1281.  
  1282. <div className="bg-white rounded-lg border border-gray-200 p-6">
  1283. <h3 className="text-md font-semibold text-gray-900 mb-4">Production Plan</h3>
  1284.  
  1285. <div className="grid grid-cols-4 gap-4 mb-6 p-4 bg-green-50 rounded-lg border border-green-200">
  1286. <div>
  1287. <div className="text-sm text-gray-600">Total Units</div>
  1288. <div className="text-2xl font-semibold text-gray-900">{estimatorResults.totalUnits}</div>
  1289. </div>
  1290. <div>
  1291. <div className="text-sm text-gray-600">Total Revenue</div>
  1292. <div className="text-2xl font-semibold text-blue-600">${estimatorResults.totalRevenue.toFixed(2)}</div>
  1293. </div>
  1294. <div>
  1295. <div className="text-sm text-gray-600">Total Profit</div>
  1296. <div className="text-2xl font-semibold text-green-600">${estimatorResults.totalProfit.toFixed(2)}</div>
  1297. </div>
  1298. <div>
  1299. <div className="text-sm text-gray-600">Time Estimate</div>
  1300. <div className="text-2xl font-semibold text-gray-900">{estimatorResults.timeEstimate.toFixed(1)}hrs</div>
  1301. </div>
  1302. </div>
  1303.  
  1304. <div className="overflow-x-auto">
  1305. <table className="w-full text-sm">
  1306. <thead className="bg-gray-50">
  1307. <tr>
  1308. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Product</th>
  1309. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Size</th>
  1310. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">
  1311. {estimatorMode === 'manual' ? 'Quantity (Edit)' : 'Quantity'}
  1312. </th>
  1313. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Unit Price</th>
  1314. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Revenue</th>
  1315. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Unit Profit</th>
  1316. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Total Profit</th>
  1317. <th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Margin</th>
  1318. </tr>
  1319. </thead>
  1320. <tbody className="divide-y divide-gray-200">
  1321. {estimatorResults.production.map((item, idx) => (
  1322. <tr key={idx}>
  1323. <td className="px-4 py-3 font-semibold">{item.name}</td>
  1324. <td className="px-4 py-3">{item.size}</td>
  1325. <td className="px-4 py-3">
  1326. {estimatorMode === 'manual' ? (
  1327. <input
  1328. type="number"
  1329. min="0"
  1330. value={manualQuantities[item.id] || 0}
  1331. onChange={(e) => setManualQuantities({
  1332. ...manualQuantities,
  1333. [item.id]: parseInt(e.target.value) || 0
  1334. })}
  1335. className="w-20 px-2 py-1 border border-gray-300 rounded font-semibold"
  1336. />
  1337. ) : (
  1338. <span className="font-semibold">{item.units}</span>
  1339. )}
  1340. </td>
  1341. <td className="px-4 py-3">${item.sellingPrice.toFixed(2)}</td>
  1342. <td className="px-4 py-3">${item.productRevenue.toFixed(2)}</td>
  1343. <td className="px-4 py-3 text-green-600">${item.profit.toFixed(2)}</td>
  1344. <td className="px-4 py-3 text-green-600 font-semibold">${item.productProfit.toFixed(2)}</td>
  1345. <td className="px-4 py-3">{item.profitMargin.toFixed(1)}%</td>
  1346. </tr>
  1347. ))}
  1348. </tbody>
  1349. </table>
  1350. </div>
  1351. </div>
  1352. </div>
  1353. )}
  1354.  
  1355. {/* SALES TAB - keeping from previous version */}
  1356. {activeTab === 'sales' && (
  1357. <div className="space-y-6">
  1358. <div className="bg-white rounded-lg border border-gray-200 p-6">
  1359. <h2 className="text-lg font-semibold text-gray-900 mb-4">Record New Sale</h2>
  1360. <div className="grid grid-cols-2 gap-4 mb-4">
  1361. <div>
  1362. <label className="block text-sm font-medium text-gray-700 mb-1">Date *</label>
  1363. <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" />
  1364. </div>
  1365. <div>
  1366. <label className="block text-sm font-medium text-gray-700 mb-1">Location/Venue *</label>
  1367. <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" />
  1368. </div>
  1369. </div>
  1370. <div className="grid grid-cols-3 gap-4 mb-4">
  1371. <div>
  1372. <label className="block text-sm font-medium text-gray-700 mb-1">Product Sold *</label>
  1373. <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">
  1374. <option value="">Select product...</option>
  1375. {products.map(prod => <option key={prod.id} value={prod.name}>{prod.name}</option>)}
  1376. </select>
  1377. </div>
  1378. <div>
  1379. <label className="block text-sm font-medium text-gray-700 mb-1">Units Sold *</label>
  1380. <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" />
  1381. </div>
  1382. <div>
  1383. <label className="block text-sm font-medium text-gray-700 mb-1">Price Per Unit *</label>
  1384. <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" />
  1385. </div>
  1386. </div>
  1387. <button onClick={addSale} className="px-6 py-2 bg-amber-800 text-white rounded-md hover:bg-amber-900">Add Sale Record</button>
  1388. </div>
  1389.  
  1390. <div className="bg-blue-50 rounded-lg border border-blue-200 p-6">
  1391. <h3 className="text-lg font-semibold text-gray-900 mb-4">Sales Summary</h3>
  1392. <div className="grid grid-cols-3 gap-6">
  1393. <div>
  1394. <div className="text-sm text-gray-600">Total Sales</div>
  1395. <div className="text-2xl font-semibold text-gray-900">{sales.length}</div>
  1396. </div>
  1397. <div>
  1398. <div className="text-sm text-gray-600">Total Revenue</div>
  1399. <div className="text-2xl font-semibold text-gray-900">${sales.reduce((sum, s) => sum + s.total, 0).toFixed(2)}</div>
  1400. </div>
  1401. <div>
  1402. <div className="text-sm text-gray-600">Avg Sale Value</div>
  1403. <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>
  1404. </div>
  1405. </div>
  1406. </div>
  1407. </div>
  1408. )}
  1409.  
  1410. {/* EXPENSES TAB - keeping from previous version */}
  1411. {activeTab === 'expenses' && (
  1412. <div className="space-y-6">
  1413. <div className="bg-white rounded-lg border border-gray-200 p-6">
  1414. <h2 className="text-lg font-semibold text-gray-900 mb-4">Record New Expense</h2>
  1415. <div className="grid grid-cols-2 gap-4 mb-4">
  1416. <div>
  1417. <label className="block text-sm font-medium text-gray-700 mb-1">Date *</label>
  1418. <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" />
  1419. </div>
  1420. <div>
  1421. <label className="block text-sm font-medium text-gray-700 mb-1">Supplier/Store *</label>
  1422. <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" />
  1423. </div>
  1424. </div>
  1425. <div className="grid grid-cols-4 gap-4 mb-4">
  1426. <div>
  1427. <label className="block text-sm font-medium text-gray-700 mb-1">Category *</label>
  1428. <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">
  1429. <option value="">Select...</option>
  1430. {expenseCategories.map(cat => <option key={cat} value={cat}>{cat}</option>)}
  1431. </select>
  1432. </div>
  1433. <div>
  1434. <label className="block text-sm font-medium text-gray-700 mb-1">Item Description *</label>
  1435. <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" />
  1436. </div>
  1437. <div>
  1438. <label className="block text-sm font-medium text-gray-700 mb-1">Cost *</label>
  1439. <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" />
  1440. </div>
  1441. <div>
  1442. <label className="block text-sm font-medium text-gray-700 mb-1">Material ID</label>
  1443. <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">
  1444. <option value="">Optional</option>
  1445. {materials.map(mat => <option key={mat.id} value={mat.id}>{mat.id} - {mat.item}</option>)}
  1446. </select>
  1447. </div>
  1448. </div>
  1449. <button onClick={addExpense} className="px-6 py-2 bg-amber-800 text-white rounded-md hover:bg-amber-900">Add Expense</button>
  1450. </div>
  1451.  
  1452. <div className="bg-green-50 rounded-lg border border-green-200 p-6">
  1453. <h3 className="text-lg font-semibold text-gray-900 mb-4">Expense Summary</h3>
  1454. <div className="grid grid-cols-3 gap-6">
  1455. <div>
  1456. <div className="text-sm text-gray-600">Total Expenses</div>
  1457. <div className="text-2xl font-semibold text-gray-900">${expenses.reduce((sum, e) => sum + e.cost, 0).toFixed(2)}</div>
  1458. </div>
  1459. <div>
  1460. <div className="text-sm text-gray-600">Total Transactions</div>
  1461. <div className="text-2xl font-semibold text-gray-900">{expenses.length}</div>
  1462. </div>
  1463. <div>
  1464. <div className="text-sm text-gray-600">Avg Transaction</div>
  1465. <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>
  1466. </div>
  1467. </div>
  1468. </div>
  1469. </div>
  1470. )}
  1471.  
  1472. {/* MARKET RESEARCH TAB - keeping from previous version */}
  1473. {activeTab === 'market-research' && (
  1474. <div className="space-y-6">
  1475. <div className="bg-purple-50 rounded-lg border border-purple-200 p-6">
  1476. <h2 className="text-lg font-semibold text-gray-900 mb-2">Market Research</h2>
  1477. <p className="text-sm text-gray-600">Industry analysis based on major retailers.</p>
  1478. </div>
  1479.  
  1480. <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
  1481. <table className="w-full text-sm">
  1482. <thead className="bg-gray-50">
  1483. <tr>
  1484. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Size Category</th>
  1485. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Your Product</th>
  1486. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Your Price</th>
  1487. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Competitor Avg</th>
  1488. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Price Position</th>
  1489. <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Suggestion</th>
  1490. </tr>
  1491. </thead>
  1492. <tbody className="divide-y divide-gray-200">
  1493. {MARKET_DATA.map((item, idx) => (
  1494. <tr key={idx}>
  1495. <td className="px-6 py-4">{item.sizeCategory}</td>
  1496. <td className="px-6 py-4 font-semibold">{item.yourProduct}</td>
  1497. <td className="px-6 py-4 text-green-600 font-semibold">${item.yourPrice.toFixed(2)}</td>
  1498. <td className="px-6 py-4">${item.competitorAvg.toFixed(2)}</td>
  1499. <td className="px-6 py-4">
  1500. <span className="px-2 py-1 bg-green-100 text-green-800 rounded text-xs font-semibold">{item.pricePosition}</span>
  1501. </td>
  1502. <td className="px-6 py-4">{item.suggestion}</td>
  1503. </tr>
  1504. ))}
  1505. </tbody>
  1506. </table>
  1507. </div>
  1508. </div>
  1509. )}
  1510. </div>
  1511. </div>
  1512. );
  1513. }
  1514.  
  1515. ReactDOM.render(<BusinessTracker />, document.getElementById('root'));
  1516. </script>
  1517. </body>
  1518. </html>
Add Comment
Please, Sign In to add comment