import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ShareEvent, SharePlatform } from './analytics.entity'; import { Article } from '../entities'; import { TrackShareDto, GetShareStatsDto, ShareStatsResponse, } from './analytics.dto'; @Injectable() export class AnalyticsService { private readonly logger = new Logger(AnalyticsService.name); constructor( @InjectRepository(ShareEvent) private readonly shareEventRepository: Repository, @InjectRepository(Article) private readonly articleRepository: Repository
, ) {} async trackShare(trackShareDto: TrackShareDto): Promise { const shareEvent = this.shareEventRepository.create(trackShareDto); // Also update the article's share counters await this.incrementArticleShareCounter( trackShareDto.articleId, trackShareDto.platform, ); return await this.shareEventRepository.save(shareEvent); } private async incrementArticleShareCounter( articleId: string, platform: SharePlatform, ): Promise { const updateField = this.getShareCounterField(platform); if (!updateField) return; await this.articleRepository .createQueryBuilder() .update(Article) .set({ [updateField]: () => `${updateField} + 1` }) .where('id = :id', { id: articleId }) .execute(); } private getShareCounterField(platform: SharePlatform): string | null { switch (platform) { case 'facebook': return 'facebookShares'; case 'twitter': return 'twitterShares'; case 'instagram': return 'instagramShares'; case 'tiktok': return 'tiktokShares'; case 'telegram': return 'telegramShares'; default: return null; // 'link' shares don't increment counters } } async getShareStats( getShareStatsDto: GetShareStatsDto, ): Promise { const query = this.articleRepository .createQueryBuilder('article') .select([ 'article.id as "articleId"', 'article.title as "articleTitle"', 'article.facebookShares as "facebookShares"', 'article.twitterShares as "twitterShares"', 'article.instagramShares as "instagramShares"', 'article.tiktokShares as "tiktokShares"', 'article.telegramShares as "telegramShares"', 'article.views as "views"', 'article.createdAt as "createdAt"', 'article.updatedAt as "updatedAt"', ]) .addSelect( `(article.facebookShares + article.twitterShares + article.instagramShares + article.tiktokShares + article.telegramShares) as "totalShares"`, ) .addSelect( `CASE WHEN article.views > 0 THEN ROUND( (article.facebookShares + article.twitterShares + article.instagramShares + article.tiktokShares + article.telegramShares)::decimal / article.views * 100, 2 ) ELSE 0 END as "shareRate"`, ); if (getShareStatsDto.articleId) { query.where('article.id = :articleId', { articleId: getShareStatsDto.articleId, }); } if (getShareStatsDto.startDate) { query.andWhere('article.createdAt >= :startDate', { startDate: getShareStatsDto.startDate, }); } if (getShareStatsDto.endDate) { query.andWhere('article.createdAt <= :endDate', { endDate: getShareStatsDto.endDate, }); } query.orderBy('"totalShares"', 'DESC'); const rawResults = await query.getRawMany<{ articleId: string; articleTitle: string; facebookShares: string; twitterShares: string; instagramShares: string; tiktokShares: string; telegramShares: string; views: string; createdAt: string; updatedAt: string; totalShares: string; shareRate: string; }>(); // Get link shares from share_events table const results: ShareStatsResponse[] = []; for (const rawResult of rawResults) { const linkShares = await this.shareEventRepository.count({ where: { articleId: rawResult.articleId, platform: 'link', }, }); const facebookShares = parseInt(rawResult.facebookShares) || 0; const twitterShares = parseInt(rawResult.twitterShares) || 0; const instagramShares = parseInt(rawResult.instagramShares) || 0; const tiktokShares = parseInt(rawResult.tiktokShares) || 0; const telegramShares = parseInt(rawResult.telegramShares) || 0; const views = parseInt(rawResult.views) || 0; const baseTotalShares = facebookShares + twitterShares + instagramShares + tiktokShares + telegramShares; const totalShares = baseTotalShares + linkShares; const shareRate = views > 0 ? parseFloat(((totalShares / views) * 100).toFixed(2)) : 0; results.push({ articleId: rawResult.articleId, articleTitle: rawResult.articleTitle, facebookShares, twitterShares, instagramShares, tiktokShares, telegramShares, linkShares, totalShares, views, shareRate, createdAt: new Date(rawResult.createdAt), updatedAt: new Date(rawResult.updatedAt), }); } return results; } async getTopSharedArticles( limit: number = 10, ): Promise { const stats = await this.getShareStats({}); return stats.slice(0, limit); } async getShareTrends( startDate: Date, endDate: Date, interval: 'day' | 'week' | 'month' = 'day', ): Promise< Array<{ period: string; platform: SharePlatform; count: number }> > { const dateFormat = interval === 'day' ? 'YYYY-MM-DD' : interval === 'week' ? 'YYYY-WW' : 'YYYY-MM'; const query = this.shareEventRepository .createQueryBuilder('share_event') .select([ `TO_CHAR(share_event.createdAt, '${dateFormat}') as period`, 'share_event.platform as platform', 'COUNT(*) as count', ]) .where('share_event.createdAt >= :startDate', { startDate }) .andWhere('share_event.createdAt <= :endDate', { endDate }) .groupBy('period, platform') .orderBy('period', 'ASC'); const rawResults = await query.getRawMany<{ period: string; platform: string; count: string; }>(); // Convert platform strings to SharePlatform type return rawResults.map((result) => ({ period: result.period, platform: result.platform as SharePlatform, count: parseInt(result.count) || 0, })); } async getTotalShareStats(): Promise<{ totalShares: number; facebookShares: number; twitterShares: number; instagramShares: number; tiktokShares: number; telegramShares: number; linkShares: number; }> { interface ArticleStatsRaw { facebookShares: string; twitterShares: string; instagramShares: string; tiktokShares: string; telegramShares: string; } const articleStats = (await this.articleRepository .createQueryBuilder('article') .select([ 'SUM(article.facebookShares) as facebookShares', 'SUM(article.twitterShares) as twitterShares', 'SUM(article.instagramShares) as instagramShares', 'SUM(article.tiktokShares) as tiktokShares', 'SUM(article.telegramShares) as telegramShares', ]) .getRawOne()) as ArticleStatsRaw; const linkShares = await this.shareEventRepository.count({ where: { platform: 'link' }, }); const facebookShares = parseInt(articleStats?.facebookShares || '0') || 0; const twitterShares = parseInt(articleStats?.twitterShares || '0') || 0; const instagramShares = parseInt(articleStats?.instagramShares || '0') || 0; const tiktokShares = parseInt(articleStats?.tiktokShares || '0') || 0; const telegramShares = parseInt(articleStats?.telegramShares || '0') || 0; const totalShares = facebookShares + twitterShares + instagramShares + tiktokShares + telegramShares + linkShares; return { totalShares, facebookShares, twitterShares, instagramShares, tiktokShares, telegramShares, linkShares, }; } }