diff --git a/docker-compose.deploy.yml b/docker-compose.deploy.yml index 5920bde..727027f 100644 --- a/docker-compose.deploy.yml +++ b/docker-compose.deploy.yml @@ -35,9 +35,12 @@ services: - NODE_ENV=production - PORT=8080 - CLIENT_URL=http://nginx + - MONGODB_URI=mongodb://mongodb:27017/etsy-tracker volumes: - etsy_data:/app/data - etsy_uploads:/app/uploads + depends_on: + - mongodb restart: unless-stopped networks: - etsy-network @@ -48,11 +51,28 @@ services: retries: 3 start_period: 40s + mongodb: + image: mongo:7 + container_name: etsy-mongodb + restart: unless-stopped + volumes: + - etsy_mongodb:/data/db + networks: + - etsy-network + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 20s + volumes: etsy_uploads: driver: local etsy_data: driver: local + etsy_mongodb: + driver: local networks: etsy-network: diff --git a/server/src/index.ts b/server/src/index.ts index 9b80832..d46a79c 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -54,12 +54,9 @@ app.get('/health', (req, res) => { }); }); -// Database connection (temporarily disabled for development) -// mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/etsy-tracker') -// .then(() => console.log('Connected to MongoDB')) -// .catch((error) => console.error('MongoDB connection error:', error)); - -console.log('Running in development mode without MongoDB - using in-memory storage'); +mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/etsy-tracker') + .then(() => console.log('Connected to MongoDB')) + .catch((error) => console.error('MongoDB connection error:', error)); // Routes app.use('/api/auth', authRoutes); diff --git a/server/src/models/Customer.ts b/server/src/models/Customer.ts new file mode 100644 index 0000000..0ed8eff --- /dev/null +++ b/server/src/models/Customer.ts @@ -0,0 +1,44 @@ +import mongoose, { Document, Schema } from 'mongoose'; + +export interface ICustomer extends Document { + name: string; + email: string; + etsyUserId?: string; + address?: { + street1: string; + street2?: string; + city: string; + state: string; + zipCode: string; + country: string; + }; + totalOrders: number; + totalSpent: number; + dateCreated: Date; + dateUpdated: Date; +} + +const CustomerSchema: Schema = new Schema({ + name: { type: String, required: true, trim: true }, + email: { type: String, required: true, trim: true, lowercase: true }, + etsyUserId: { type: String }, + address: { + street1: { type: String }, + street2: { type: String }, + city: { type: String }, + state: { type: String }, + zipCode: { type: String }, + country: { type: String }, + }, + totalOrders: { type: Number, default: 0 }, + totalSpent: { type: Number, default: 0 }, + dateCreated: { type: Date, default: Date.now }, + dateUpdated: { type: Date, default: Date.now }, +}); + +CustomerSchema.pre('save', function (next) { + this.dateUpdated = new Date(); + next(); +}); + +export default mongoose.model('Customer', CustomerSchema); diff --git a/server/src/models/Expense.ts b/server/src/models/Expense.ts new file mode 100644 index 0000000..8990fcf --- /dev/null +++ b/server/src/models/Expense.ts @@ -0,0 +1,34 @@ +import mongoose, { Document, Schema } from 'mongoose'; + +export interface IExpense extends Document { + category: 'materials' | 'shipping' | 'fees' | 'advertising' | 'tools' | 'other'; + description: string; + amount: number; + date: Date; + receiptUrl?: string; + notes?: string; + dateCreated: Date; + dateUpdated: Date; +} + +const ExpenseSchema: Schema = new Schema({ + category: { + type: String, + enum: ['materials', 'shipping', 'fees', 'advertising', 'tools', 'other'], + required: true, + }, + description: { type: String, required: true, trim: true }, + amount: { type: Number, required: true, min: 0 }, + date: { type: Date, required: true }, + receiptUrl: { type: String }, + notes: { type: String }, + dateCreated: { type: Date, default: Date.now }, + dateUpdated: { type: Date, default: Date.now }, +}); + +ExpenseSchema.pre('save', function (next) { + this.dateUpdated = new Date(); + next(); +}); + +export default mongoose.model('Expense', ExpenseSchema); diff --git a/server/src/routes/analytics.ts b/server/src/routes/analytics.ts index a7a81ac..9d0ffa4 100644 --- a/server/src/routes/analytics.ts +++ b/server/src/routes/analytics.ts @@ -1,35 +1,126 @@ -import { Router } from 'express'; +import { Router, Request, Response } from 'express'; +import Order from '../models/Order'; +import Product from '../models/Product'; +import Customer from '../models/Customer'; +import Expense from '../models/Expense'; const router = Router(); -// GET /api/analytics/dashboard -router.get('/dashboard', (req, res) => { - res.json({ - message: 'Dashboard analytics endpoint', - data: { - totalRevenue: 0, - totalOrders: 0, - totalProducts: 0, - totalCustomers: 0, - recentOrders: [], - salesChart: [], +router.get('/dashboard', async (req: Request, res: Response) => { + try { + const [totalOrders, totalProducts, totalCustomers, revenueResult, expenseResult, recentOrders] = await Promise.all([ + Order.countDocuments({ paymentStatus: 'paid' }), + Product.countDocuments({ isActive: true }), + Customer.countDocuments(), + Order.aggregate([ + { $match: { paymentStatus: 'paid' } }, + { $group: { _id: null, total: { $sum: '$total' } } }, + ]), + Expense.aggregate([ + { $group: { _id: null, total: { $sum: '$amount' } } }, + ]), + Order.find({ paymentStatus: 'paid' }) + .populate('customerId', 'name email') + .sort({ dateOrdered: -1 }) + .limit(5), + ]); + + const totalRevenue = revenueResult[0]?.total ?? 0; + const totalExpenses = expenseResult[0]?.total ?? 0; + + const twelveMonthsAgo = new Date(); + twelveMonthsAgo.setMonth(twelveMonthsAgo.getMonth() - 11); + twelveMonthsAgo.setDate(1); + twelveMonthsAgo.setHours(0, 0, 0, 0); + + const salesChart = await Order.aggregate([ + { $match: { paymentStatus: 'paid', dateOrdered: { $gte: twelveMonthsAgo } } }, + { + $group: { + _id: { year: { $year: '$dateOrdered' }, month: { $month: '$dateOrdered' } }, + revenue: { $sum: '$total' }, + orders: { $sum: 1 }, + }, + }, + { $sort: { '_id.year': 1, '_id.month': 1 } }, + ]); + + res.json({ + totalRevenue, + totalExpenses, + totalOrders, + totalProducts, + totalCustomers, + recentOrders, + salesChart, + }); + } catch (err) { + res.status(500).json({ message: 'Failed to fetch dashboard data', error: err }); + } +}); + +router.get('/sales', async (req: Request, res: Response) => { + try { + const { from, to } = req.query; + const match: any = { paymentStatus: 'paid' }; + if (from || to) { + match.dateOrdered = {}; + if (from) match.dateOrdered.$gte = new Date(from as string); + if (to) match.dateOrdered.$lte = new Date(to as string); } - }); + + const data = await Order.aggregate([ + { $match: match }, + { + $group: { + _id: { year: { $year: '$dateOrdered' }, month: { $month: '$dateOrdered' } }, + revenue: { $sum: '$total' }, + orders: { $sum: 1 }, + }, + }, + { $sort: { '_id.year': 1, '_id.month': 1 } }, + ]); + + res.json(data); + } catch (err) { + res.status(500).json({ message: 'Failed to fetch sales data', error: err }); + } }); -// GET /api/analytics/sales -router.get('/sales', (req, res) => { - res.json({ message: 'Sales analytics endpoint - coming soon!' }); +router.get('/products', async (req: Request, res: Response) => { + try { + const topProducts = await Order.aggregate([ + { $match: { paymentStatus: 'paid' } }, + { $unwind: '$items' }, + { + $group: { + _id: '$items.productId', + title: { $first: '$items.title' }, + totalSold: { $sum: '$items.quantity' }, + totalRevenue: { $sum: { $multiply: ['$items.quantity', '$items.price'] } }, + }, + }, + { $sort: { totalRevenue: -1 } }, + { $limit: 10 }, + ]); + + res.json(topProducts); + } catch (err) { + res.status(500).json({ message: 'Failed to fetch product analytics', error: err }); + } }); -// GET /api/analytics/products -router.get('/products', (req, res) => { - res.json({ message: 'Product analytics endpoint - coming soon!' }); +router.get('/customers', async (req: Request, res: Response) => { + try { + const topCustomers = await Customer.find() + .sort({ totalSpent: -1 }) + .limit(10) + .select('name email totalOrders totalSpent'); + + res.json(topCustomers); + } catch (err) { + res.status(500).json({ message: 'Failed to fetch customer analytics', error: err }); + } }); -// GET /api/analytics/customers -router.get('/customers', (req, res) => { - res.json({ message: 'Customer analytics endpoint - coming soon!' }); -}); - -export default router; \ No newline at end of file +export default router; diff --git a/server/src/routes/customers.ts b/server/src/routes/customers.ts index 5c7818a..664de29 100644 --- a/server/src/routes/customers.ts +++ b/server/src/routes/customers.ts @@ -1,25 +1,50 @@ -import { Router } from 'express'; +import { Router, Request, Response } from 'express'; +import Customer from '../models/Customer'; const router = Router(); -// GET /api/customers -router.get('/', (req, res) => { - res.json({ message: 'Get customers endpoint - coming soon!', customers: [] }); +router.get('/', async (req: Request, res: Response) => { + try { + const { page = 1, limit = 20 } = req.query; + const customers = await Customer.find() + .sort({ dateCreated: -1 }) + .limit(Number(limit)) + .skip((Number(page) - 1) * Number(limit)); + const total = await Customer.countDocuments(); + res.json({ customers, total, page: Number(page), limit: Number(limit) }); + } catch (err) { + res.status(500).json({ message: 'Failed to fetch customers', error: err }); + } }); -// POST /api/customers -router.post('/', (req, res) => { - res.json({ message: 'Create customer endpoint - coming soon!' }); +router.post('/', async (req: Request, res: Response) => { + try { + const customer = new Customer(req.body); + await customer.save(); + res.status(201).json(customer); + } catch (err) { + res.status(400).json({ message: 'Failed to create customer', error: err }); + } }); -// GET /api/customers/:id -router.get('/:id', (req, res) => { - res.json({ message: 'Get customer by ID endpoint - coming soon!' }); +router.get('/:id', async (req: Request, res: Response) => { + try { + const customer = await Customer.findById(req.params.id); + if (!customer) return res.status(404).json({ message: 'Customer not found' }); + res.json(customer); + } catch (err) { + res.status(500).json({ message: 'Failed to fetch customer', error: err }); + } }); -// PUT /api/customers/:id -router.put('/:id', (req, res) => { - res.json({ message: 'Update customer endpoint - coming soon!' }); +router.put('/:id', async (req: Request, res: Response) => { + try { + const customer = await Customer.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true }); + if (!customer) return res.status(404).json({ message: 'Customer not found' }); + res.json(customer); + } catch (err) { + res.status(400).json({ message: 'Failed to update customer', error: err }); + } }); -export default router; \ No newline at end of file +export default router; diff --git a/server/src/routes/expenses.ts b/server/src/routes/expenses.ts index 8a577c2..252f4ca 100644 --- a/server/src/routes/expenses.ts +++ b/server/src/routes/expenses.ts @@ -1,30 +1,64 @@ -import { Router } from 'express'; +import { Router, Request, Response } from 'express'; +import Expense from '../models/Expense'; const router = Router(); -// GET /api/expenses -router.get('/', (req, res) => { - res.json({ message: 'Get expenses endpoint - coming soon!', expenses: [] }); +router.get('/', async (req: Request, res: Response) => { + try { + const { page = 1, limit = 20, category } = req.query; + const filter: any = {}; + if (category) filter.category = category; + + const expenses = await Expense.find(filter) + .sort({ date: -1 }) + .limit(Number(limit)) + .skip((Number(page) - 1) * Number(limit)); + const total = await Expense.countDocuments(filter); + + res.json({ expenses, total, page: Number(page), limit: Number(limit) }); + } catch (err) { + res.status(500).json({ message: 'Failed to fetch expenses', error: err }); + } }); -// POST /api/expenses -router.post('/', (req, res) => { - res.json({ message: 'Create expense endpoint - coming soon!' }); +router.post('/', async (req: Request, res: Response) => { + try { + const expense = new Expense(req.body); + await expense.save(); + res.status(201).json(expense); + } catch (err) { + res.status(400).json({ message: 'Failed to create expense', error: err }); + } }); -// GET /api/expenses/:id -router.get('/:id', (req, res) => { - res.json({ message: 'Get expense by ID endpoint - coming soon!' }); +router.get('/:id', async (req: Request, res: Response) => { + try { + const expense = await Expense.findById(req.params.id); + if (!expense) return res.status(404).json({ message: 'Expense not found' }); + res.json(expense); + } catch (err) { + res.status(500).json({ message: 'Failed to fetch expense', error: err }); + } }); -// PUT /api/expenses/:id -router.put('/:id', (req, res) => { - res.json({ message: 'Update expense endpoint - coming soon!' }); +router.put('/:id', async (req: Request, res: Response) => { + try { + const expense = await Expense.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true }); + if (!expense) return res.status(404).json({ message: 'Expense not found' }); + res.json(expense); + } catch (err) { + res.status(400).json({ message: 'Failed to update expense', error: err }); + } }); -// DELETE /api/expenses/:id -router.delete('/:id', (req, res) => { - res.json({ message: 'Delete expense endpoint - coming soon!' }); +router.delete('/:id', async (req: Request, res: Response) => { + try { + const expense = await Expense.findByIdAndDelete(req.params.id); + if (!expense) return res.status(404).json({ message: 'Expense not found' }); + res.json({ message: 'Expense deleted' }); + } catch (err) { + res.status(500).json({ message: 'Failed to delete expense', error: err }); + } }); -export default router; \ No newline at end of file +export default router; diff --git a/server/src/routes/orders.ts b/server/src/routes/orders.ts index cbc1aa2..e7eb909 100644 --- a/server/src/routes/orders.ts +++ b/server/src/routes/orders.ts @@ -1,25 +1,62 @@ -import { Router } from 'express'; +import { Router, Request, Response } from 'express'; +import Order from '../models/Order'; +import Customer from '../models/Customer'; const router = Router(); -// GET /api/orders -router.get('/', (req, res) => { - res.json({ message: 'Get orders endpoint - coming soon!', orders: [] }); +router.get('/', async (req: Request, res: Response) => { + try { + const { page = 1, limit = 20, status, paymentStatus } = req.query; + const filter: any = {}; + if (status) filter.status = status; + if (paymentStatus) filter.paymentStatus = paymentStatus; + + const orders = await Order.find(filter) + .populate('customerId', 'name email') + .sort({ dateOrdered: -1 }) + .limit(Number(limit)) + .skip((Number(page) - 1) * Number(limit)); + const total = await Order.countDocuments(filter); + + res.json({ orders, total, page: Number(page), limit: Number(limit) }); + } catch (err) { + res.status(500).json({ message: 'Failed to fetch orders', error: err }); + } }); -// POST /api/orders -router.post('/', (req, res) => { - res.json({ message: 'Create order endpoint - coming soon!' }); +router.post('/', async (req: Request, res: Response) => { + try { + const order = new Order(req.body); + await order.save(); + + await Customer.findByIdAndUpdate(order.customerId, { + $inc: { totalOrders: 1, totalSpent: order.total }, + }); + + res.status(201).json(order); + } catch (err) { + res.status(400).json({ message: 'Failed to create order', error: err }); + } }); -// GET /api/orders/:id -router.get('/:id', (req, res) => { - res.json({ message: 'Get order by ID endpoint - coming soon!' }); +router.get('/:id', async (req: Request, res: Response) => { + try { + const order = await Order.findById(req.params.id).populate('customerId', 'name email'); + if (!order) return res.status(404).json({ message: 'Order not found' }); + res.json(order); + } catch (err) { + res.status(500).json({ message: 'Failed to fetch order', error: err }); + } }); -// PUT /api/orders/:id -router.put('/:id', (req, res) => { - res.json({ message: 'Update order endpoint - coming soon!' }); +router.put('/:id', async (req: Request, res: Response) => { + try { + const order = await Order.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true }); + if (!order) return res.status(404).json({ message: 'Order not found' }); + res.json(order); + } catch (err) { + res.status(400).json({ message: 'Failed to update order', error: err }); + } }); -export default router; \ No newline at end of file +export default router; diff --git a/server/src/routes/products.ts b/server/src/routes/products.ts index 481cd1b..98361a7 100644 --- a/server/src/routes/products.ts +++ b/server/src/routes/products.ts @@ -1,30 +1,65 @@ -import { Router } from 'express'; +import { Router, Request, Response } from 'express'; +import Product from '../models/Product'; const router = Router(); -// GET /api/products -router.get('/', (req, res) => { - res.json({ message: 'Get products endpoint - coming soon!', products: [] }); +router.get('/', async (req: Request, res: Response) => { + try { + const { page = 1, limit = 20, category, active } = req.query; + const filter: any = {}; + if (category) filter.category = category; + if (active !== undefined) filter.isActive = active === 'true'; + + const products = await Product.find(filter) + .sort({ dateCreated: -1 }) + .limit(Number(limit)) + .skip((Number(page) - 1) * Number(limit)); + const total = await Product.countDocuments(filter); + + res.json({ products, total, page: Number(page), limit: Number(limit) }); + } catch (err) { + res.status(500).json({ message: 'Failed to fetch products', error: err }); + } }); -// POST /api/products -router.post('/', (req, res) => { - res.json({ message: 'Create product endpoint - coming soon!' }); +router.post('/', async (req: Request, res: Response) => { + try { + const product = new Product(req.body); + await product.save(); + res.status(201).json(product); + } catch (err) { + res.status(400).json({ message: 'Failed to create product', error: err }); + } }); -// GET /api/products/:id -router.get('/:id', (req, res) => { - res.json({ message: 'Get product by ID endpoint - coming soon!' }); +router.get('/:id', async (req: Request, res: Response) => { + try { + const product = await Product.findById(req.params.id); + if (!product) return res.status(404).json({ message: 'Product not found' }); + res.json(product); + } catch (err) { + res.status(500).json({ message: 'Failed to fetch product', error: err }); + } }); -// PUT /api/products/:id -router.put('/:id', (req, res) => { - res.json({ message: 'Update product endpoint - coming soon!' }); +router.put('/:id', async (req: Request, res: Response) => { + try { + const product = await Product.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true }); + if (!product) return res.status(404).json({ message: 'Product not found' }); + res.json(product); + } catch (err) { + res.status(400).json({ message: 'Failed to update product', error: err }); + } }); -// DELETE /api/products/:id -router.delete('/:id', (req, res) => { - res.json({ message: 'Delete product endpoint - coming soon!' }); +router.delete('/:id', async (req: Request, res: Response) => { + try { + const product = await Product.findByIdAndDelete(req.params.id); + if (!product) return res.status(404).json({ message: 'Product not found' }); + res.json({ message: 'Product deleted' }); + } catch (err) { + res.status(500).json({ message: 'Failed to delete product', error: err }); + } }); -export default router; \ No newline at end of file +export default router;