diff --git a/inventory-server/src/routes/dashboard.js b/inventory-server/src/routes/dashboard.js index 3899e05..4cb8abc 100644 --- a/inventory-server/src/routes/dashboard.js +++ b/inventory-server/src/routes/dashboard.js @@ -1,36 +1,35 @@ const express = require('express'); const router = express.Router(); -// Get dashboard statistics +// Get dashboard stats router.get('/stats', async (req, res) => { const pool = req.app.locals.pool; try { - const [totalProducts] = await pool.query( - 'SELECT COUNT(*) as count FROM products WHERE visible = true' - ); - - const [lowStockProducts] = await pool.query( - 'SELECT COUNT(*) as count FROM products WHERE stock_quantity <= 10 AND visible = true' - ); - - const [orderStats] = await pool.query(` + const [stats] = await pool.query(` SELECT - COUNT(DISTINCT order_number) as total_orders, - AVG(price * quantity - COALESCE(discount, 0)) as avg_order_value - FROM orders - WHERE canceled = false - AND date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) + COUNT(*) as totalProducts, + COUNT(CASE WHEN stock_quantity <= 5 THEN 1 END) as lowStockProducts, + COALESCE( + (SELECT COUNT(DISTINCT order_number) FROM orders WHERE DATE(date) >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) AND canceled = false), + 0 + ) as totalOrders, + COALESCE( + (SELECT AVG(subtotal) FROM ( + SELECT order_number, SUM(price * quantity) as subtotal + FROM orders + WHERE DATE(date) >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) + AND canceled = false + GROUP BY order_number + ) t), + 0 + ) as averageOrderValue + FROM products + WHERE visible = true `); - - res.json({ - totalProducts: totalProducts[0].count, - lowStockProducts: lowStockProducts[0].count, - totalOrders: orderStats[0].total_orders || 0, - averageOrderValue: orderStats[0].avg_order_value || 0 - }); + res.json(stats[0]); } catch (error) { console.error('Error fetching dashboard stats:', error); - res.status(500).json({ error: 'Failed to fetch dashboard statistics' }); + res.status(500).json({ error: 'Failed to fetch dashboard stats' }); } }); @@ -41,15 +40,17 @@ router.get('/sales-overview', async (req, res) => { const [rows] = await pool.query(` SELECT DATE(date) as date, - SUM(price * quantity - COALESCE(discount, 0)) as total + SUM(price * quantity) as total FROM orders - WHERE canceled = false - AND date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) + WHERE DATE(date) >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) + AND canceled = false GROUP BY DATE(date) ORDER BY date ASC `); - - res.json(rows); + res.json(rows.map(row => ({ + ...row, + total: parseFloat(row.total || 0) + }))); } catch (error) { console.error('Error fetching sales overview:', error); res.status(500).json({ error: 'Failed to fetch sales overview' }); @@ -62,31 +63,35 @@ router.get('/recent-orders', async (req, res) => { try { const [rows] = await pool.query(` SELECT - order_number as id, - customer, - date, - SUM(price * quantity - COALESCE(discount, 0)) as amount - FROM orders - WHERE canceled = false - GROUP BY order_number, customer, date - ORDER BY date DESC + o1.order_number as order_id, + o1.customer as customer_name, + SUM(o2.price * o2.quantity) as total_amount, + o1.date as order_date + FROM orders o1 + JOIN orders o2 ON o1.order_number = o2.order_number + WHERE o1.canceled = false + GROUP BY o1.order_number, o1.customer, o1.date + ORDER BY o1.date DESC LIMIT 5 `); - - res.json(rows); + res.json(rows.map(row => ({ + ...row, + total_amount: parseFloat(row.total_amount || 0), + order_date: row.order_date + }))); } catch (error) { console.error('Error fetching recent orders:', error); res.status(500).json({ error: 'Failed to fetch recent orders' }); } }); -// Get category statistics +// Get category stats router.get('/category-stats', async (req, res) => { const pool = req.app.locals.pool; try { const [rows] = await pool.query(` SELECT - COALESCE(NULLIF(categories, ''), 'Uncategorized') as category, + categories, COUNT(*) as count FROM products WHERE visible = true @@ -94,11 +99,10 @@ router.get('/category-stats', async (req, res) => { ORDER BY count DESC LIMIT 10 `); - res.json(rows); } catch (error) { console.error('Error fetching category stats:', error); - res.status(500).json({ error: 'Failed to fetch category statistics' }); + res.status(500).json({ error: 'Failed to fetch category stats' }); } }); @@ -109,13 +113,12 @@ router.get('/stock-levels', async (req, res) => { const [rows] = await pool.query(` SELECT SUM(CASE WHEN stock_quantity = 0 THEN 1 ELSE 0 END) as outOfStock, - SUM(CASE WHEN stock_quantity > 0 AND stock_quantity <= 10 THEN 1 ELSE 0 END) as lowStock, - SUM(CASE WHEN stock_quantity > 10 AND stock_quantity <= 50 THEN 1 ELSE 0 END) as inStock, - SUM(CASE WHEN stock_quantity > 50 THEN 1 ELSE 0 END) as overStock + SUM(CASE WHEN stock_quantity > 0 AND stock_quantity <= 5 THEN 1 ELSE 0 END) as lowStock, + SUM(CASE WHEN stock_quantity > 5 AND stock_quantity <= 20 THEN 1 ELSE 0 END) as inStock, + SUM(CASE WHEN stock_quantity > 20 THEN 1 ELSE 0 END) as overStock FROM products WHERE visible = true `); - res.json(rows[0]); } catch (error) { console.error('Error fetching stock levels:', error); diff --git a/inventory-server/src/routes/products.js b/inventory-server/src/routes/products.js index 7b4cba3..893a2a5 100755 --- a/inventory-server/src/routes/products.js +++ b/inventory-server/src/routes/products.js @@ -1,21 +1,32 @@ const express = require('express'); const router = express.Router(); -const { v4: uuidv4 } = require('uuid'); -const { importProductsFromCSV } = require('../utils/csvImporter'); const multer = require('multer'); +const { importProductsFromCSV } = require('../utils/csvImporter'); // Configure multer for file uploads const upload = multer({ dest: 'uploads/' }); -// Get all products with their current inventory levels +// Get all products router.get('/', async (req, res) => { const pool = req.app.locals.pool; try { const [rows] = await pool.query(` - SELECT p.*, il.quantity, il.reorder_point, il.reorder_quantity - FROM products p - LEFT JOIN inventory_levels il ON p.id = il.product_id - ORDER BY p.created_at DESC + SELECT + product_id, + title, + SKU, + stock_quantity, + price, + regular_price, + cost_price, + vendor, + brand, + categories, + visible, + managing_stock + FROM products + WHERE visible = true + ORDER BY title ASC `); res.json(rows); } catch (error) { @@ -24,16 +35,14 @@ router.get('/', async (req, res) => { } }); -// Get a single product with its inventory details +// Get a single product router.get('/:id', async (req, res) => { const pool = req.app.locals.pool; try { - const [rows] = await pool.query(` - SELECT p.*, il.quantity, il.reorder_point, il.reorder_quantity - FROM products p - LEFT JOIN inventory_levels il ON p.id = il.product_id - WHERE p.id = ? - `, [req.params.id]); + const [rows] = await pool.query( + 'SELECT * FROM products WHERE product_id = ? AND visible = true', + [req.params.id] + ); if (rows.length === 0) { return res.status(404).json({ error: 'Product not found' }); @@ -45,41 +54,6 @@ router.get('/:id', async (req, res) => { } }); -// Create a new product -router.post('/', async (req, res) => { - const pool = req.app.locals.pool; - const { sku, name, description, category } = req.body; - const id = uuidv4(); - - try { - const connection = await pool.getConnection(); - try { - await connection.beginTransaction(); - - await connection.query( - 'INSERT INTO products (id, sku, name, description, category) VALUES (?, ?, ?, ?, ?)', - [id, sku, name, description, category] - ); - - await connection.query( - 'INSERT INTO inventory_levels (id, product_id, quantity) VALUES (?, ?, 0)', - [uuidv4(), id] - ); - - await connection.commit(); - res.status(201).json({ id, sku, name, description, category }); - } catch (error) { - await connection.rollback(); - throw error; - } finally { - connection.release(); - } - } catch (error) { - console.error('Error creating product:', error); - res.status(500).json({ error: 'Failed to create product' }); - } -}); - // Import products from CSV router.post('/import', upload.single('file'), async (req, res) => { if (!req.file) { @@ -97,41 +71,4 @@ router.post('/import', upload.single('file'), async (req, res) => { } }); -// Update product inventory -router.post('/:id/inventory', async (req, res) => { - const pool = req.app.locals.pool; - const { quantity, type, notes } = req.body; - - try { - const connection = await pool.getConnection(); - try { - await connection.beginTransaction(); - - // Create inventory transaction - await connection.query( - 'INSERT INTO inventory_transactions (id, product_id, transaction_type, quantity, notes) VALUES (?, ?, ?, ?, ?)', - [uuidv4(), req.params.id, type, quantity, notes] - ); - - // Update inventory level - const quantityChange = type === 'sale' ? -quantity : quantity; - await connection.query( - 'UPDATE inventory_levels SET quantity = quantity + ? WHERE product_id = ?', - [quantityChange, req.params.id] - ); - - await connection.commit(); - res.json({ success: true }); - } catch (error) { - await connection.rollback(); - throw error; - } finally { - connection.release(); - } - } catch (error) { - console.error('Error updating inventory:', error); - res.status(500).json({ error: 'Failed to update inventory' }); - } -}); - module.exports = router; \ No newline at end of file diff --git a/inventory-server/src/server.js b/inventory-server/src/server.js index 5e236eb..f2a0e97 100755 --- a/inventory-server/src/server.js +++ b/inventory-server/src/server.js @@ -2,7 +2,7 @@ const path = require('path'); const fs = require('fs'); // Get the absolute path to the .env file -const envPath = path.resolve('/var/www/html/inventory/.env'); +const envPath = path.resolve(process.cwd(), '.env'); console.log('Current working directory:', process.cwd()); console.log('Looking for .env file at:', envPath); console.log('.env file exists:', fs.existsSync(envPath)); @@ -10,6 +10,13 @@ console.log('.env file exists:', fs.existsSync(envPath)); try { require('dotenv').config({ path: envPath }); console.log('.env file loaded successfully'); + console.log('Environment check:', { + NODE_ENV: process.env.NODE_ENV || 'not set', + PORT: process.env.PORT || 'not set', + DB_HOST: process.env.DB_HOST || 'not set', + DB_USER: process.env.DB_USER || 'not set', + DB_NAME: process.env.DB_NAME || 'not set', + }); } catch (error) { console.error('Error loading .env file:', error); } @@ -39,15 +46,18 @@ const dashboardRouter = require('./routes/dashboard'); const app = express(); -// CORS configuration +// CORS configuration - move before route handlers app.use(cors({ origin: ['https://inventory.kent.pw', 'https://www.inventory.kent.pw'], methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'], - credentials: true + credentials: true, + optionsSuccessStatus: 200 // Some legacy browsers (IE11) choke on 204 })); +// Body parser middleware app.use(express.json()); +app.use(express.urlencoded({ extended: true })); // Request logging middleware app.use((req, res, next) => { @@ -61,6 +71,16 @@ app.use((req, res, next) => { next(); }); +// Error handling middleware - move before route handlers +app.use((err, req, res, next) => { + console.error(`[${new Date().toISOString()}] Error:`, err); + res.status(500).json({ + error: process.env.NODE_ENV === 'production' + ? 'An internal server error occurred' + : err.message + }); +}); + // Database connection pool const pool = mysql.createPool({ host: process.env.DB_HOST, @@ -101,16 +121,6 @@ app.get('/health', (req, res) => { }); }); -// Error handling middleware -app.use((err, req, res, next) => { - console.error(`[${new Date().toISOString()}] Error:`, err); - res.status(500).json({ - error: process.env.NODE_ENV === 'production' - ? 'An internal server error occurred' - : err.message - }); -}); - // Handle uncaught exceptions process.on('uncaughtException', (err) => { console.error(`[${new Date().toISOString()}] Uncaught Exception:`, err); diff --git a/inventory/src/components/dashboard/InventoryStats.tsx b/inventory/src/components/dashboard/InventoryStats.tsx index 30f1e8e..fd1e5ec 100644 --- a/inventory/src/components/dashboard/InventoryStats.tsx +++ b/inventory/src/components/dashboard/InventoryStats.tsx @@ -4,7 +4,7 @@ import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from 'recharts'; import config from '../../config'; interface CategoryStats { - category: string; + categories: string; count: number; } @@ -16,7 +16,7 @@ interface StockLevels { } export function InventoryStats() { - const { data: categoryStats } = useQuery({ + const { data: categoryStats, isLoading: categoryLoading, error: categoryError } = useQuery({ queryKey: ['category-stats'], queryFn: async () => { const response = await fetch(`${config.apiUrl}/dashboard/category-stats`); @@ -27,7 +27,7 @@ export function InventoryStats() { }, }); - const { data: stockLevels } = useQuery({ + const { data: stockLevels, isLoading: stockLoading, error: stockError } = useQuery({ queryKey: ['stock-levels'], queryFn: async () => { const response = await fetch(`${config.apiUrl}/dashboard/stock-levels`); @@ -38,6 +38,14 @@ export function InventoryStats() { }, }); + if (categoryLoading || stockLoading) { + return
Loading inventory stats...
; + } + + if (categoryError || stockError) { + return
Error loading inventory stats
; + } + return (
@@ -48,7 +56,7 @@ export function InventoryStats() { ({ + const { data, isLoading, error } = useQuery({ queryKey: ['sales-overview'], queryFn: async () => { const response = await fetch(`${config.apiUrl}/dashboard/sales-overview`); if (!response.ok) { throw new Error('Failed to fetch sales overview'); } - return response.json(); + const rawData = await response.json(); + return rawData.map((item: SalesData) => ({ + ...item, + total: parseFloat(item.total.toString()), + date: new Date(item.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + })); }, }); @@ -23,6 +28,10 @@ export function Overview() { return
Loading chart...
; } + if (error) { + return
Error loading sales overview
; + } + return ( @@ -38,9 +47,12 @@ export function Overview() { fontSize={12} tickLine={false} axisLine={false} - tickFormatter={(value) => `$${value}`} + tickFormatter={(value) => `$${value.toLocaleString()}`} + /> + [`$${value.toLocaleString()}`, 'Sales']} + labelFormatter={(label) => `Date: ${label}`} /> - ({ + const { data: recentOrders, isLoading, error } = useQuery({ queryKey: ['recent-orders'], queryFn: async () => { const response = await fetch(`${config.apiUrl}/dashboard/recent-orders`); if (!response.ok) { throw new Error('Failed to fetch recent orders'); } - return response.json(); + const data = await response.json(); + return data.map((order: RecentOrder) => ({ + ...order, + total_amount: parseFloat(order.total_amount.toString()) + })); }, }); @@ -25,26 +29,35 @@ export function RecentSales() { return
Loading recent sales...
; } + if (error) { + return
Error loading recent sales
; + } + return (
{recentOrders?.map((order) => ( -
+
- {order.customer.split(' ').map(n => n[0]).join('')} + {order.customer_name?.split(' ').map(n => n[0]).join('') || '??'}
-

Order #{order.id}

+

Order #{order.order_id}

- {new Date(order.date).toLocaleDateString()} + {new Date(order.order_date).toLocaleDateString()}

- +${order.amount.toFixed(2)} + ${order.total_amount.toFixed(2)}
))} + {!recentOrders?.length && ( +
+ No recent orders found +
+ )}
); } \ No newline at end of file diff --git a/inventory/src/pages/Dashboard.tsx b/inventory/src/pages/Dashboard.tsx index 8985b99..84f5b19 100644 --- a/inventory/src/pages/Dashboard.tsx +++ b/inventory/src/pages/Dashboard.tsx @@ -13,19 +13,41 @@ interface DashboardStats { averageOrderValue: number; } +interface Order { + order_id: string; + customer_name: string; + total_amount: number; + order_date: string; +} + export function Dashboard() { - const { data: stats, isLoading } = useQuery({ + const { data: stats, isLoading: statsLoading } = useQuery({ queryKey: ['dashboard-stats'], queryFn: async () => { const response = await fetch(`${config.apiUrl}/dashboard/stats`); if (!response.ok) { throw new Error('Failed to fetch dashboard stats'); } + const data = await response.json(); + return { + ...data, + averageOrderValue: parseFloat(data.averageOrderValue) || 0 + }; + }, + }); + + const { data: orders, isLoading: ordersLoading } = useQuery({ + queryKey: ['recent-orders'], + queryFn: async () => { + const response = await fetch(`${config.apiUrl}/dashboard/recent-orders`); + if (!response.ok) { + throw new Error('Failed to fetch recent orders'); + } return response.json(); }, }); - if (isLoading) { + if (statsLoading) { return
Loading dashboard...
; } @@ -49,7 +71,7 @@ export function Dashboard() { -
{stats?.totalProducts}
+
{stats?.totalProducts || 0}
@@ -59,7 +81,7 @@ export function Dashboard() { -
{stats?.lowStockProducts}
+
{stats?.lowStockProducts || 0}
@@ -69,7 +91,7 @@ export function Dashboard() { -
{stats?.totalOrders}
+
{stats?.totalOrders || 0}
@@ -80,7 +102,7 @@ export function Dashboard() {
- ${stats?.averageOrderValue.toFixed(2)} + ${(stats?.averageOrderValue || 0).toFixed(2)}
@@ -107,6 +129,45 @@ export function Dashboard() { + +
+ + + Recent Orders + + + {ordersLoading ? ( +
Loading orders...
+ ) : orders && orders.length > 0 ? ( +
+ {orders.map((order) => ( +
+
+

+ Order #{order.order_id} +

+

+ {order.customer_name} +

+

+ {new Date(order.order_date).toLocaleDateString()} +

+
+
+ ${order.total_amount.toFixed(2)} +
+
+ ))} +
+ ) : ( +
+ No recent orders found +
+ )} +
+
+
+
); diff --git a/inventory/src/pages/Products.tsx b/inventory/src/pages/Products.tsx index 055eb31..7234a9b 100644 --- a/inventory/src/pages/Products.tsx +++ b/inventory/src/pages/Products.tsx @@ -2,14 +2,18 @@ import { useQuery } from '@tanstack/react-query'; import config from '../config'; interface Product { - id: string; - sku: string; - name: string; - description: string | null; - category: string | null; - quantity: number; - reorder_point: number | null; - reorder_quantity: number | null; + product_id: string; + title: string; + SKU: string; + stock_quantity: number; + price: number; + regular_price: number; + cost_price: number; + vendor: string; + brand: string; + categories: string; + visible: boolean; + managing_stock: boolean; } export function Products() { @@ -20,7 +24,13 @@ export function Products() { if (!response.ok) { throw new Error('Network response was not ok'); } - return response.json(); + const data = await response.json(); + return data.map((product: Product) => ({ + ...product, + price: parseFloat(product.price?.toString() || '0'), + regular_price: parseFloat(product.regular_price?.toString() || '0'), + cost_price: parseFloat(product.cost_price?.toString() || '0') + })); }, }); @@ -34,38 +44,29 @@ export function Products() { return (
-
-

Products

- -
+

Products

- - - - - + + + + + {products?.map((product) => ( - - - - - - - + + + + + + + ))} {!products?.length && ( diff --git a/mountremote.command b/mountremote.command new file mode 100755 index 0000000..4e6a61e --- /dev/null +++ b/mountremote.command @@ -0,0 +1,7 @@ +#!/bin/zsh + +#Clear previous mount in case it’s still there +umount ~/Dev/inventory/inventory-server + +#Mount +sshfs matt@dashboard.kent.pw:/var/www/html/inventory -p 22122 ~/Dev/inventory/inventory-server/ \ No newline at end of file
SKUNameCategoryQuantityReorder PointActionsTitleBrandVendorStockPrice
{product.sku}{product.name}{product.category || '-'}{product.quantity}{product.reorder_point || '-'} - -
{product.SKU}{product.title}{product.brand || '-'}{product.vendor || '-'}{product.stock_quantity}${(product.price || 0).toFixed(2)}