From f391b6d85362e6c67096737a0fc965672e771367 Mon Sep 17 00:00:00 2001 From: efrilm Date: Mon, 20 Oct 2025 18:47:29 +0700 Subject: [PATCH] update dashboard analytic --- internal/repository/analytics_repository.go | 117 +++++++++++++++----- internal/service/analytics_service.go | 40 +++---- 2 files changed, 112 insertions(+), 45 deletions(-) diff --git a/internal/repository/analytics_repository.go b/internal/repository/analytics_repository.go index e49fd8d..f31b9aa 100644 --- a/internal/repository/analytics_repository.go +++ b/internal/repository/analytics_repository.go @@ -27,7 +27,7 @@ func (r *AnalyticsRepository) GetLetterSummaryStats(ctx context.Context, startDa // Use department_letter_summary for department-specific stats query := db.Table("department_letter_summary"). Where("department_id = ?", *departmentID) - + if !startDate.IsZero() { query = query.Where("summary_date >= ?", startDate) } @@ -36,13 +36,13 @@ func (r *AnalyticsRepository) GetLetterSummaryStats(ctx context.Context, startDa } var result struct { - TotalIncoming int64 `gorm:"column:total_incoming"` - TotalOutgoing int64 `gorm:"column:total_outgoing"` - PendingOutgoing int64 `gorm:"column:pending_outgoing"` - ApprovedOutgoing int64 `gorm:"column:approved_outgoing"` - RejectedOutgoing int64 `gorm:"column:rejected_outgoing"` - AvgResponseHours float64 `gorm:"column:avg_response_hours"` - CompletionRate float64 `gorm:"column:completion_rate"` + TotalIncoming int64 `gorm:"column:total_incoming"` + TotalOutgoing int64 `gorm:"column:total_outgoing"` + PendingOutgoing int64 `gorm:"column:pending_outgoing"` + ApprovedOutgoing int64 `gorm:"column:approved_outgoing"` + RejectedOutgoing int64 `gorm:"column:rejected_outgoing"` + AvgResponseHours float64 `gorm:"column:avg_response_hours"` + CompletionRate float64 `gorm:"column:completion_rate"` } query.Select(` @@ -66,7 +66,7 @@ func (r *AnalyticsRepository) GetLetterSummaryStats(ctx context.Context, startDa } else if userID == nil && departmentID == nil { // Use letter_summary for overall stats query := db.Table("letter_summary") - + if !startDate.IsZero() { query = query.Where("summary_date >= ?", startDate) } @@ -75,14 +75,14 @@ func (r *AnalyticsRepository) GetLetterSummaryStats(ctx context.Context, startDa } var result struct { - TotalIncoming int64 `gorm:"column:total_incoming"` - TotalOutgoing int64 `gorm:"column:total_outgoing"` - TotalPending int64 `gorm:"column:total_pending"` - TotalApproved int64 `gorm:"column:total_approved"` - TotalRejected int64 `gorm:"column:total_rejected"` - TotalArchived int64 `gorm:"column:total_archived"` - TotalSent int64 `gorm:"column:total_sent"` - AvgProcessing float64 `gorm:"column:avg_processing"` + TotalIncoming int64 `gorm:"column:total_incoming"` + TotalOutgoing int64 `gorm:"column:total_outgoing"` + TotalPending int64 `gorm:"column:total_pending"` + TotalApproved int64 `gorm:"column:total_approved"` + TotalRejected int64 `gorm:"column:total_rejected"` + TotalArchived int64 `gorm:"column:total_archived"` + TotalSent int64 `gorm:"column:total_sent"` + AvgProcessing float64 `gorm:"column:avg_processing"` } query.Select(` @@ -132,39 +132,44 @@ func (r *AnalyticsRepository) GetLetterSummaryStats(ctx context.Context, startDa outgoingQuery = outgoingQuery. Joins("LEFT JOIN letter_outgoing_recipients ON letter_outgoing_recipients.letter_id = letters_outgoing.id"). Where("letter_outgoing_recipients.user_id = ?", *userID) + incomingQuery = incomingQuery. + Joins("LEFT JOIN letter_incoming_recipients ON letter_incoming_recipients.letter_id = letters_incoming.id"). + Where("letter_incoming_recipients.recipient_user_id = ?", *userID) } + fmt.Printf("[DEBUG] userId analitycs: %v\n", userID) + // Count incoming letters var totalIncoming int64 - incomingQuery.Count(&totalIncoming) + incomingQuery.Distinct("letters_incoming.id").Count(&totalIncoming) stats["total_incoming"] = totalIncoming // Count outgoing letters var totalOutgoing int64 - outgoingQuery.Count(&totalOutgoing) + outgoingQuery.Distinct("letters_outgoing.id").Count(&totalOutgoing) stats["total_outgoing"] = totalOutgoing // Count by status - need to clone query for each count var pendingCount, approvedCount, rejectedCount, archivedCount int64 - + db.Table("letters_outgoing").Where("letters_outgoing.deleted_at IS NULL"). Where("letters_outgoing.status = ?", "pending_approval"). Joins("LEFT JOIN letter_outgoing_recipients ON letter_outgoing_recipients.letter_id = letters_outgoing.id"). Where("letter_outgoing_recipients.user_id = ?", *userID). Count(&pendingCount) - + db.Table("letters_outgoing").Where("letters_outgoing.deleted_at IS NULL"). Where("letters_outgoing.status = ?", "approved"). Joins("LEFT JOIN letter_outgoing_recipients ON letter_outgoing_recipients.letter_id = letters_outgoing.id"). Where("letter_outgoing_recipients.user_id = ?", *userID). Count(&approvedCount) - + db.Table("letters_outgoing").Where("letters_outgoing.deleted_at IS NULL"). Where("letters_outgoing.status = ?", "rejected"). Joins("LEFT JOIN letter_outgoing_recipients ON letter_outgoing_recipients.letter_id = letters_outgoing.id"). Where("letter_outgoing_recipients.user_id = ?", *userID). Count(&rejectedCount) - + db.Table("letters_outgoing").Where("letters_outgoing.deleted_at IS NULL"). Where("letters_outgoing.status = ?", "archived"). Joins("LEFT JOIN letter_outgoing_recipients ON letter_outgoing_recipients.letter_id = letters_outgoing.id"). @@ -386,7 +391,7 @@ func (r *AnalyticsRepository) GetDepartmentStats(ctx context.Context, startDate, } fallbackQuery = fmt.Sprintf(fallbackQuery, fallbackDateFilter) - + if err := db.Raw(fallbackQuery).Scan(&results).Error; err != nil { return nil, err } @@ -471,9 +476,9 @@ func (r *AnalyticsRepository) GetMonthlyTrend(ctx context.Context, months int) ( ORDER BY year DESC, month_num DESC LIMIT %d ` - + fallbackQuery = fmt.Sprintf(fallbackQuery, months, months, months) - + if err := db.Raw(fallbackQuery).Scan(&results).Error; err != nil { return nil, err } @@ -713,6 +718,66 @@ func (r *AnalyticsRepository) GetDailyActivity(ctx context.Context, days int) ([ return results, nil } +func (r *AnalyticsRepository) GetDailyActivityByUserID(ctx context.Context, userID *uuid.UUID, days int) ([]map[string]interface{}, error) { + db := DBFromContext(ctx, r.db) + var results []map[string]interface{} + + query := ` + WITH daily_data AS ( + SELECT + DATE(created_at) as date, + TO_CHAR(created_at, 'Day') as day_of_week, + COUNT(CASE WHEN type = 'incoming' THEN 1 END) as incoming_count, + COUNT(CASE WHEN type = 'outgoing' THEN 1 END) as outgoing_count, + 0 as approved_count, + 0 as rejected_count + FROM ( + SELECT li.created_at, 'incoming' as type + FROM letters_incoming li + INNER JOIN letter_incoming_recipients lir ON lir.letter_id = li.id + WHERE li.deleted_at IS NULL AND lir.recipient_user_id = ? + UNION ALL + SELECT lo.created_at, 'outgoing' as type + FROM letters_outgoing lo + INNER JOIN letter_outgoing_recipients lor ON lor.letter_id = lo.id + WHERE lo.deleted_at IS NULL AND lor.user_id = ? + ) combined + WHERE created_at >= CURRENT_DATE - INTERVAL '%d days' + GROUP BY DATE(created_at), TO_CHAR(created_at, 'Day') + ), + approval_data AS ( + SELECT + DATE(acted_at) as date, + COUNT(CASE WHEN status = 'approved' THEN 1 END) as approved_count, + COUNT(CASE WHEN status = 'rejected' THEN 1 END) as rejected_count + FROM letter_outgoing_approvals + WHERE acted_at IS NOT NULL + AND acted_at >= CURRENT_DATE - INTERVAL '%d days' + AND approver_id = ? + GROUP BY DATE(acted_at) + ) + SELECT + d.date, + d.day_of_week, + d.incoming_count, + d.outgoing_count, + COALESCE(a.approved_count, 0) as approved_count, + COALESCE(a.rejected_count, 0) as rejected_count + FROM daily_data d + LEFT JOIN approval_data a ON a.date = d.date + ORDER BY d.date DESC + LIMIT %d + ` + + query = fmt.Sprintf(query, days, days, days) + + if err := db.Raw(query, userID, userID, userID).Scan(&results).Error; err != nil { + return nil, err + } + + return results, nil +} + // GetResponseTimeStats gets response time statistics func (r *AnalyticsRepository) GetResponseTimeStats(ctx context.Context, startDate, endDate time.Time) (map[string]interface{}, error) { db := DBFromContext(ctx, r.db) diff --git a/internal/service/analytics_service.go b/internal/service/analytics_service.go index 6f7cebf..796f73b 100644 --- a/internal/service/analytics_service.go +++ b/internal/service/analytics_service.go @@ -2,6 +2,7 @@ package service import ( "context" + "fmt" "time" "eslogad-be/internal/appcontext" @@ -37,7 +38,7 @@ func (s *AnalyticsServiceImpl) GetDashboard(ctx context.Context, req *contract.A // Default to last 30 days startDate = time.Now().AddDate(0, 0, -30) } - + if req.EndDate != "" { if date, err := time.Parse("2006-01-02", req.EndDate); err == nil { endDate = date.Add(23*time.Hour + 59*time.Minute + 59*time.Second) @@ -48,7 +49,7 @@ func (s *AnalyticsServiceImpl) GetDashboard(ctx context.Context, req *contract.A // Apply user context filters if not admin var userID *uuid.UUID - + appCtx := appcontext.FromGinContext(ctx) if appCtx != nil && appCtx.UserRole != "admin" && appCtx.UserRole != "superadmin" { userID = &appCtx.UserID @@ -61,8 +62,9 @@ func (s *AnalyticsServiceImpl) GetDashboard(ctx context.Context, req *contract.A if err != nil { return nil, err } + fmt.Printf("[DEBUG] summaryData: %v\n", summaryData) response.Summary = s.mapSummaryStats(summaryData) - + // Calculate growth metrics response.Summary.WeekOverWeekGrowth = s.calculateWeekOverWeekGrowth(ctx) response.Summary.MonthOverMonthGrowth = s.calculateMonthOverMonthGrowth(ctx) @@ -99,7 +101,7 @@ func (s *AnalyticsServiceImpl) GetDashboard(ctx context.Context, req *contract.A response.InstitutionStats = s.mapInstitutionStats(instData) // Get daily activity (last 7 days) - dailyData, err := s.analyticsRepo.GetDailyActivity(ctx, 7) + dailyData, err := s.analyticsRepo.GetDailyActivityByUserID(ctx, userID, 7) if err != nil { return nil, err } @@ -143,17 +145,17 @@ func (s *AnalyticsServiceImpl) calculateWeekOverWeekGrowth(ctx context.Context) // Get this week's data thisWeekStart := time.Now().AddDate(0, 0, -int(time.Now().Weekday())) thisWeekEnd := time.Now() - + // Get last week's data lastWeekStart := thisWeekStart.AddDate(0, 0, -7) lastWeekEnd := thisWeekStart.AddDate(0, 0, -1) - + thisWeekData, _ := s.analyticsRepo.GetLetterSummaryStats(ctx, thisWeekStart, thisWeekEnd, nil, nil) lastWeekData, _ := s.analyticsRepo.GetLetterSummaryStats(ctx, lastWeekStart, lastWeekEnd, nil, nil) - + thisWeekTotal := getInt64Value(thisWeekData["total_incoming"]) + getInt64Value(thisWeekData["total_outgoing"]) lastWeekTotal := getInt64Value(lastWeekData["total_incoming"]) + getInt64Value(lastWeekData["total_outgoing"]) - + if lastWeekTotal > 0 { return float64((thisWeekTotal - lastWeekTotal) * 100 / lastWeekTotal) } @@ -166,17 +168,17 @@ func (s *AnalyticsServiceImpl) calculateMonthOverMonthGrowth(ctx context.Context now := time.Now() thisMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) thisMonthEnd := now - + // Get last month's data lastMonthStart := thisMonthStart.AddDate(0, -1, 0) lastMonthEnd := thisMonthStart.AddDate(0, 0, -1) - + thisMonthData, _ := s.analyticsRepo.GetLetterSummaryStats(ctx, thisMonthStart, thisMonthEnd, nil, nil) lastMonthData, _ := s.analyticsRepo.GetLetterSummaryStats(ctx, lastMonthStart, lastMonthEnd, nil, nil) - + thisMonthTotal := getInt64Value(thisMonthData["total_incoming"]) + getInt64Value(thisMonthData["total_outgoing"]) lastMonthTotal := getInt64Value(lastMonthData["total_incoming"]) + getInt64Value(lastMonthData["total_outgoing"]) - + if lastMonthTotal > 0 { return float64((thisMonthTotal - lastMonthTotal) * 100 / lastMonthTotal) } @@ -190,7 +192,7 @@ func (s *AnalyticsServiceImpl) getSimpleDepartmentStats(ctx context.Context, sta if err != nil { return []contract.SimpleDepartmentStats{} } - + result := make([]contract.SimpleDepartmentStats, 0, len(deptData)) for _, item := range deptData { deptIDStr := getStringValue(item["department_id"]) @@ -198,17 +200,17 @@ func (s *AnalyticsServiceImpl) getSimpleDepartmentStats(ctx context.Context, sta if err != nil { continue } - + // Calculate total letter count (incoming + outgoing) letterCount := getInt64Value(item["incoming_count"]) + getInt64Value(item["outgoing_count"]) - + result = append(result, contract.SimpleDepartmentStats{ DepartmentID: deptID, Department: getStringValue(item["department_name"]), LetterCount: letterCount, }) } - + return result } @@ -303,11 +305,11 @@ func (s *AnalyticsServiceImpl) mapInstitutionStats(data []map[string]interface{} OutgoingCount: getInt64Value(item["outgoing_count"]), TotalCount: getInt64Value(item["total_count"]), } - + if lastActivity, ok := item["last_activity"].(time.Time); ok { stat.LastActivity = lastActivity } - + result = append(result, stat) } } @@ -410,4 +412,4 @@ func getFloat64Value(v interface{}) float64 { default: return 0 } -} \ No newline at end of file +}