diff --git a/frontend/web/src/components/admin/dashboard/CustomerInsights.tsx b/frontend/web/src/components/admin/dashboard/CustomerInsights.tsx index 299d7ec..b2f4b82 100644 --- a/frontend/web/src/components/admin/dashboard/CustomerInsights.tsx +++ b/frontend/web/src/components/admin/dashboard/CustomerInsights.tsx @@ -48,6 +48,8 @@ export default function CustomerInsights() { ); } + console.log(data); + if (!data) return null; const stats = [ diff --git a/services/checkout-service/src/analytics/analytics.controller.ts b/services/checkout-service/src/analytics/analytics.controller.ts index e311afd..66e9bd2 100644 --- a/services/checkout-service/src/analytics/analytics.controller.ts +++ b/services/checkout-service/src/analytics/analytics.controller.ts @@ -4,6 +4,8 @@ import { AnalyticsService } from './analytics.service.js'; export class AnalyticsController { constructor(private analyticsService: AnalyticsService) {} + // SALES OVERVIEW + getOverview = async (req: Request, res: Response) => { try { const data = await this.analyticsService.getOverview(); @@ -18,6 +20,7 @@ export class AnalyticsController { try { const days = Number(req.query.range) || 30; const data = await this.analyticsService.getRevenueChart(days); + res.json(data); } catch (error) { console.error(error); @@ -25,6 +28,18 @@ export class AnalyticsController { } }; + getGrowthStats = async (req: Request, res: Response) => { + try { + const data = await this.analyticsService.getGrowthStats(); + res.json(data); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Failed to fetch growth stats' }); + } + }; + + // CART ANALYTICS + getCartStats = async (req: Request, res: Response) => { try { const data = await this.analyticsService.getCartStats(); @@ -35,6 +50,8 @@ export class AnalyticsController { } }; + // PRODUCT ANALYTICS + // TOP SELLING PRODUCTS getTopProducts = async (req: Request, res: Response) => { try { @@ -77,6 +94,7 @@ export class AnalyticsController { trackProductView = async (req: Request, res: Response) => { try { const { productId } = req.body; + await this.analyticsService.trackProductView(productId); res.json({ success: true }); @@ -86,6 +104,8 @@ export class AnalyticsController { } }; + // FUNNEL ANALYTICS + getFunnelStats = async (req: Request, res: Response) => { try { const data = await this.analyticsService.getFunnelStats(); @@ -96,6 +116,8 @@ export class AnalyticsController { } }; + // DASHBOARD SUMMARY + getDashboardSummary = async (req: Request, res: Response) => { try { const data = await this.analyticsService.getDashboardSummary(); @@ -106,9 +128,55 @@ export class AnalyticsController { } }; - getGrowthStats = async (req: Request, res: Response) => { - const data = await this.analyticsService.getGrowthStats(); + // USER ANALYTICS + + getUserAnalytics = async (req: Request, res: Response) => { + try { + const { userId } = req.params; + + const data = await this.analyticsService.getUserAnalytics(userId); + + res.json(data); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Failed to fetch user analytics' }); + } + }; - res.json(data); + getTopCustomers = async (req: Request, res: Response) => { + try { + const limit = Number(req.query.limit) || 10; + + const data = await this.analyticsService.getTopCustomers(limit); + + res.json(data); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Failed to fetch top customers' }); + } + }; + + getMostActiveCustomers = async (req: Request, res: Response) => { + try { + const limit = Number(req.query.limit) || 10; + + const data = await this.analyticsService.getMostActiveCustomers(limit); + + res.json(data); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Failed to fetch active customers' }); + } + }; + + getUserOverview = async (req: Request, res: Response) => { + try { + const data = await this.analyticsService.getUserOverview(); + + res.json(data); + } catch (error) { + console.error(error); + res.status(500).json({ message: 'Failed to fetch user overview' }); + } }; } diff --git a/services/checkout-service/src/analytics/analytics.routes.ts b/services/checkout-service/src/analytics/analytics.routes.ts index 718ce4c..8d31956 100644 --- a/services/checkout-service/src/analytics/analytics.routes.ts +++ b/services/checkout-service/src/analytics/analytics.routes.ts @@ -7,15 +7,39 @@ const router = Router(); const analyticsService = new AnalyticsService(); const controller = new AnalyticsController(analyticsService); +/* ============================= + SALES ANALYTICS +============================= */ + router.get('/overview', controller.getOverview); router.get('/revenue', controller.getRevenueChart); +router.get('/growth', controller.getGrowthStats); + +//DASHBOARD SUMMARY + +router.get('/dashboard', controller.getDashboardSummary); + +//CART ANALYTICS + router.get('/cart', controller.getCartStats); + +//PRODUCT ANALYTICS + router.get('/products/top', controller.getTopProducts); router.get('/products/views', controller.getMostViewedProducts); router.get('/products/revenue', controller.getTopRevenueProducts); + router.post('/product-view', controller.trackProductView); + +//FUNNEL ANALYTICS + router.get('/funnel', controller.getFunnelStats); -router.get('/dashboard', controller.getDashboardSummary); -router.get('/growth', controller.getGrowthStats); + +//USER ANALYTICS + +router.get('/users/top', controller.getTopCustomers); +router.get('/users/active', controller.getMostActiveCustomers); +router.get('/users/overview', controller.getUserOverview); +router.get('/users/:userId', controller.getUserAnalytics); export default router; diff --git a/services/checkout-service/src/analytics/analytics.service.ts b/services/checkout-service/src/analytics/analytics.service.ts index 11df4a2..b87271d 100644 --- a/services/checkout-service/src/analytics/analytics.service.ts +++ b/services/checkout-service/src/analytics/analytics.service.ts @@ -3,6 +3,7 @@ import { CartStats } from './models/cartStats.model.js'; import { FunnelStats } from './models/funnelStats.model.js'; import { ProductStats } from './models/productStats.model.js'; import { SalesDaily } from './models/salesDaily.model.js'; +import { UserStats } from './models/userAnalytics.model.js'; export class AnalyticsService { //ORDER CREATED @@ -10,19 +11,8 @@ export class AnalyticsService { async trackOrder(order: Order): Promise { const date = new Date(order.createdAt).toISOString().split('T')[0]; - await SalesDaily.updateOne( - { date }, - { - $inc: { - revenue: order.totalAmount, - orders: 1, - }, - }, - { upsert: true }, - ); - - for (const item of order.items) { - await ProductStats.updateOne( + const productUpdates = order.items.map((item) => + ProductStats.updateOne( { productId: item.productId }, { $inc: { @@ -31,22 +21,49 @@ export class AnalyticsService { }, }, { upsert: true }, - ); - } - - await CartStats.updateOne( - { date }, - { - $inc: { cartsConverted: 1 }, - }, - { upsert: true }, + ), ); - await FunnelStats.updateOne( - { date }, - { $inc: { ordersCompleted: 1 } }, - { upsert: true }, - ); + await Promise.all([ + SalesDaily.updateOne( + { date }, + { + $inc: { + revenue: order.totalAmount, + orders: 1, + }, + }, + { upsert: true }, + ), + + CartStats.updateOne( + { date }, + { $inc: { cartsConverted: 1 } }, + { upsert: true }, + ), + + FunnelStats.updateOne( + { date }, + { $inc: { ordersCompleted: 1 } }, + { upsert: true }, + ), + + UserStats.updateOne( + { userId: order.userId }, + { + $inc: { + totalOrders: 1, + totalSpent: order.totalAmount, + }, + $set: { + lastOrderAt: new Date(order.createdAt), + }, + }, + { upsert: true }, + ), + + ...productUpdates, + ]); } //REFUND CREATED @@ -349,4 +366,48 @@ export class AnalyticsService { orderGrowth, }; } + + async getUserAnalytics(userId: string) { + return UserStats.findOne({ userId }); + } + + async getTopCustomers(limit = 10) { + return UserStats.find().sort({ totalSpent: -1 }).limit(limit); + } + + async getMostActiveCustomers(limit = 10) { + return UserStats.find().sort({ totalOrders: -1 }).limit(limit); + } + + async getUserOverview() { + const stats = await UserStats.aggregate([ + { + $group: { + _id: null, + totalCustomers: { $sum: 1 }, + totalOrders: { $sum: '$totalOrders' }, + totalRevenue: { $sum: '$totalSpent' }, + }, + }, + ]); + + return ( + stats[0] ?? { + totalCustomers: 0, + totalOrders: 0, + totalRevenue: 0, + } + ); + } + + async getCustomerGrowth() { + return UserStats.aggregate([ + { + $group: { + _id: '$createdAt', + users: { $sum: 1 }, + }, + }, + ]); + } } diff --git a/services/checkout-service/src/analytics/models/userAnalytics.model.ts b/services/checkout-service/src/analytics/models/userAnalytics.model.ts new file mode 100644 index 0000000..d2ebc0b --- /dev/null +++ b/services/checkout-service/src/analytics/models/userAnalytics.model.ts @@ -0,0 +1,68 @@ +import mongoose, { Schema, Document } from 'mongoose'; + +export interface MonthlyStat { + month: string; + orders: number; + spent: number; +} + +export interface IUserStats extends Document { + userId: mongoose.Types.ObjectId; + totalOrders: number; + totalSpent: number; + lastOrderAt?: Date; + monthlyStats: MonthlyStat[]; +} + +const MonthlyStatSchema = new Schema( + { + month: { + type: String, + required: true, + }, + orders: { + type: Number, + default: 0, + }, + spent: { + type: Number, + default: 0, + }, + }, + { _id: false }, +); + +const UserStatsSchema = new Schema( + { + userId: { + type: Schema.Types.ObjectId, + required: true, + unique: true, + index: true, + }, + + totalOrders: { + type: Number, + default: 0, + }, + + totalSpent: { + type: Number, + default: 0, + }, + + lastOrderAt: { + type: Date, + }, + + monthlyStats: [MonthlyStatSchema], + }, + { + timestamps: true, + }, +); + +export const UserStats = mongoose.model( + 'UserStats', + UserStatsSchema, +); diff --git a/services/user-service/src/repositories/user.repository.ts b/services/user-service/src/repositories/user.repository.ts index be6e88d..f450baf 100644 --- a/services/user-service/src/repositories/user.repository.ts +++ b/services/user-service/src/repositories/user.repository.ts @@ -11,6 +11,17 @@ type AdminUserFilter = { }>; }; +type AnalyticsCustomer = { + userId: string; + totalOrders: number; + totalSpent: number; +}; + +type UserBasic = { + _id: Types.ObjectId; + name: string; +}; + export class UserRepository { async findByEmail(email: string): Promise { return await User.findOne({ email }); //isDeleted: false @@ -108,25 +119,14 @@ export class UserRepository { const oneWeekAgo = new Date(); oneWeekAgo.setDate(now.getDate() - 7); - // Total customers - const totalCustomers = await User.countDocuments({ - role: 'user', - }); + // BASIC CUSTOMER STATS + const totalCustomers = await User.countDocuments({ role: 'user' }); - // New customers (last 7 days) const newCustomers = await User.countDocuments({ role: 'user', createdAt: { $gte: oneWeekAgo }, }); - // VIP customers (example: you can adjust logic) - // Here assuming users with more than 5 orders OR add vip flag later - const vipCustomers = await User.countDocuments({ - role: 'user', - isActive: true, - }); - - // Retention calculation (basic example) const activeUsers = await User.countDocuments({ role: 'user', isActive: true, @@ -136,28 +136,64 @@ export class UserRepository { ? Math.round((activeUsers / totalCustomers) * 100) : 0; - // Top 5 latest customers (temporary until you connect orders) - const topCustomers = await User.find({ role: 'user' }) - .sort({ createdAt: -1 }) - .limit(5) - .select('name email') - .lean(); + let analyticsCustomers: AnalyticsCustomer[] = []; + + try { + const analyticsRes = await fetch( + 'http://localhost:4003/api/v1/analytics/users/top?limit=5', + ); + + if (analyticsRes.ok) { + const response: unknown = await analyticsRes.json(); + + if (Array.isArray(response)) { + analyticsCustomers = response as AnalyticsCustomer[]; + } else if ( + typeof response === 'object' && + response !== null && + 'data' in response + ) { + analyticsCustomers = (response as { data: AnalyticsCustomer[] }).data; + } + } + } catch (error) { + console.error('Analytics service unavailable:', error); + } + + const userIds = analyticsCustomers.map((c) => c.userId); + + let users: UserBasic[] = []; + + if (userIds.length > 0) { + users = await User.find({ _id: { $in: userIds } }) + .select('_id name') + .lean(); + } + + const userMap = new Map( + users.map((u) => [String(u._id), u]), + ); + + const topCustomers = analyticsCustomers.map((stat) => { + const user = userMap.get(String(stat.userId)); + + return { + _id: stat.userId, + name: user?.name ?? 'Unknown', + totalOrders: stat.totalOrders ?? 0, + totalSpent: stat.totalSpent ?? 0, + }; + }); return { newCustomers, - vipCustomers, + vipCustomers: topCustomers.filter((c) => c.totalSpent > 5000).length, totalCustomers, retentionRate, - retentionGrowth: 2.4, // static for now - topCustomers: topCustomers.map((user) => ({ - _id: user._id, - name: user.name, - totalOrders: 0, // update later when you connect orders - totalSpent: 0, - })), + retentionGrowth: 2.4, + topCustomers, }; } - // async deleteUser(userId: string): Promise { // return User.findOneAndUpdate( // { _id: userId, isDeleted: false },