package service import ( "context" "fmt" "time" "eslogad-be/internal/appcontext" "eslogad-be/internal/contract" "eslogad-be/internal/repository" "github.com/google/uuid" ) type AnalyticsService interface { GetDashboard(ctx context.Context, req *contract.AnalyticsDashboardRequest) (*contract.AnalyticsDashboardResponse, error) GetLetterVolume(ctx context.Context) (*contract.LetterVolumeByTypeResponse, error) } type AnalyticsServiceImpl struct { analyticsRepo *repository.AnalyticsRepository } func NewAnalyticsService(analyticsRepo *repository.AnalyticsRepository) *AnalyticsServiceImpl { return &AnalyticsServiceImpl{ analyticsRepo: analyticsRepo, } } func (s *AnalyticsServiceImpl) GetDashboard(ctx context.Context, req *contract.AnalyticsDashboardRequest) (*contract.AnalyticsDashboardResponse, error) { // Parse dates var startDate, endDate time.Time if req.StartDate != "" { if date, err := time.Parse("2006-01-02", req.StartDate); err == nil { startDate = date } } else { // 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) } } else { endDate = time.Now() } // 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 } response := &contract.AnalyticsDashboardResponse{} // Get summary statistics - don't filter by department for overall stats summaryData, err := s.analyticsRepo.GetLetterSummaryStats(ctx, startDate, endDate, userID, nil) 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) // Get priority distribution priorityData, err := s.analyticsRepo.GetPriorityDistribution(ctx, startDate, endDate) if err != nil { return nil, err } response.PriorityDistribution = s.mapPriorityDistribution(priorityData) // Get department statistics deptData, err := s.analyticsRepo.GetDepartmentStats(ctx, startDate, endDate) if err != nil { return nil, err } response.DepartmentStats = s.mapDepartmentStats(deptData) // Get monthly trend (last 12 months) monthlyData, err := s.analyticsRepo.GetMonthlyTrendByUserID(ctx, userID, 12) if err != nil { return nil, err } response.MonthlyTrend = s.mapMonthlyTrend(monthlyData) // Get simplified department stats (departments_stats) response.DepartmentsStats = s.getSimpleDepartmentStats(ctx, startDate, endDate) // Get institution statistics instData, err := s.analyticsRepo.GetInstitutionStats(ctx, startDate, endDate) if err != nil { return nil, err } response.InstitutionStats = s.mapInstitutionStats(instData) // Get daily activity (last 7 days) dailyData, err := s.analyticsRepo.GetDailyActivityByUserID(ctx, userID, 7) if err != nil { return nil, err } response.DailyActivity = s.mapDailyActivity(dailyData) return response, nil } func (s *AnalyticsServiceImpl) GetLetterVolume(ctx context.Context) (*contract.LetterVolumeByTypeResponse, error) { // This would be implemented with specific queries for volume metrics // For now, returning a placeholder return &contract.LetterVolumeByTypeResponse{ Incoming: contract.IncomingLetterVolume{ Today: 0, ThisWeek: 0, ThisMonth: 0, ThisYear: 0, Total: 0, }, Outgoing: contract.OutgoingLetterVolume{ Today: 0, ThisWeek: 0, ThisMonth: 0, ThisYear: 0, Total: 0, }, }, nil } // Helper functions to map repository data to contract types func (s *AnalyticsServiceImpl) mapSummaryStats(data map[string]interface{}) contract.LetterSummaryStats { return contract.LetterSummaryStats{ TotalIncoming: getInt64Value(data["total_incoming"]), TotalOutgoing: getInt64Value(data["total_outgoing"]), } } // calculateWeekOverWeekGrowth calculates the week over week growth rate func (s *AnalyticsServiceImpl) calculateWeekOverWeekGrowth(ctx context.Context) float64 { // 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) } return 0 } // calculateMonthOverMonthGrowth calculates the month over month growth rate func (s *AnalyticsServiceImpl) calculateMonthOverMonthGrowth(ctx context.Context) float64 { // Get this month's data 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) } return 0 } // getSimpleDepartmentStats gets simplified department statistics func (s *AnalyticsServiceImpl) getSimpleDepartmentStats(ctx context.Context, startDate, endDate time.Time) []contract.SimpleDepartmentStats { // Get department stats with letter counts deptData, err := s.analyticsRepo.GetDepartmentStats(ctx, startDate, endDate) if err != nil { return []contract.SimpleDepartmentStats{} } result := make([]contract.SimpleDepartmentStats, 0, len(deptData)) for _, item := range deptData { deptIDStr := getStringValue(item["department_id"]) deptID, err := uuid.Parse(deptIDStr) 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 } func (s *AnalyticsServiceImpl) mapStatusDistribution(data []map[string]interface{}) []contract.StatusDistribution { result := make([]contract.StatusDistribution, 0, len(data)) for _, item := range data { result = append(result, contract.StatusDistribution{ Status: getStringValue(item["status"]), Count: getInt64Value(item["count"]), Percentage: getFloat64Value(item["percentage"]), Type: getStringValue(item["type"]), }) } return result } func (s *AnalyticsServiceImpl) mapPriorityDistribution(data []map[string]interface{}) []contract.PriorityDistribution { result := make([]contract.PriorityDistribution, 0, len(data)) for _, item := range data { result = append(result, contract.PriorityDistribution{ PriorityID: getStringValue(item["priority_id"]), PriorityName: getStringValue(item["priority_name"]), Level: getIntValue(item["level"]), Count: getInt64Value(item["count"]), Percentage: getFloat64Value(item["percentage"]), AvgResponseTime: getFloat64Value(item["avg_response_time"]), }) } return result } func (s *AnalyticsServiceImpl) mapDepartmentStats(data []map[string]interface{}) []contract.DepartmentStats { result := make([]contract.DepartmentStats, 0, len(data)) for _, item := range data { if deptID, err := uuid.Parse(getStringValue(item["department_id"])); err == nil { result = append(result, contract.DepartmentStats{ DepartmentID: deptID, DepartmentName: getStringValue(item["department_name"]), DepartmentCode: getStringValue(item["department_code"]), IncomingCount: getInt64Value(item["incoming_count"]), OutgoingCount: getInt64Value(item["outgoing_count"]), PendingCount: getInt64Value(item["pending_count"]), AvgResponseTime: getFloat64Value(item["avg_response_time"]), CompletionRate: getFloat64Value(item["completion_rate"]), }) } } return result } func (s *AnalyticsServiceImpl) mapMonthlyTrend(data []map[string]interface{}) []contract.MonthlyTrend { result := make([]contract.MonthlyTrend, 0, len(data)) for _, item := range data { result = append(result, contract.MonthlyTrend{ Month: getStringValue(item["month"]), Year: getIntValue(item["year"]), IncomingCount: getInt64Value(item["incoming_count"]), OutgoingCount: getInt64Value(item["outgoing_count"]), TotalCount: getInt64Value(item["total_count"]), GrowthRate: getFloat64Value(item["growth_rate"]), }) } return result } func (s *AnalyticsServiceImpl) mapTopUsers(data []map[string]interface{}) []contract.TopUserStats { result := make([]contract.TopUserStats, 0, len(data)) for _, item := range data { if userID, err := uuid.Parse(getStringValue(item["user_id"])); err == nil { result = append(result, contract.TopUserStats{ UserID: userID, UserName: getStringValue(item["user_name"]), UserEmail: getStringValue(item["user_email"]), Department: getStringValue(item["department"]), LetterCount: getInt64Value(item["letter_count"]), AvgResponseTime: getFloat64Value(item["avg_response_time"]), }) } } return result } func (s *AnalyticsServiceImpl) mapInstitutionStats(data []map[string]interface{}) []contract.InstitutionStats { result := make([]contract.InstitutionStats, 0, len(data)) for _, item := range data { if instID, err := uuid.Parse(getStringValue(item["institution_id"])); err == nil { stat := contract.InstitutionStats{ InstitutionID: instID, InstitutionName: getStringValue(item["institution_name"]), InstitutionType: getStringValue(item["institution_type"]), IncomingCount: getInt64Value(item["incoming_count"]), 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) } } return result } func (s *AnalyticsServiceImpl) mapApprovalMetrics(data map[string]interface{}) contract.ApprovalMetrics { return contract.ApprovalMetrics{ TotalSubmitted: getInt64Value(data["total_submitted"]), TotalApproved: getInt64Value(data["total_approved"]), TotalRejected: getInt64Value(data["total_rejected"]), TotalPending: getInt64Value(data["total_pending"]), ApprovalRate: getFloat64Value(data["approval_rate"]), RejectionRate: getFloat64Value(data["rejection_rate"]), AvgApprovalTime: getFloat64Value(data["avg_approval_time"]), AvgApprovalSteps: getFloat64Value(data["avg_approval_steps"]), } } func (s *AnalyticsServiceImpl) mapResponseTimeStats(data map[string]interface{}) contract.ResponseTimeStats { return contract.ResponseTimeStats{ MinResponseTime: getFloat64Value(data["min_response_time"]), MaxResponseTime: getFloat64Value(data["max_response_time"]), AvgResponseTime: getFloat64Value(data["avg_response_time"]), MedianResponseTime: getFloat64Value(data["median_response_time"]), P95ResponseTime: getFloat64Value(data["p95_response_time"]), P99ResponseTime: getFloat64Value(data["p99_response_time"]), } } func (s *AnalyticsServiceImpl) mapDailyActivity(data []map[string]interface{}) []contract.DailyActivity { result := make([]contract.DailyActivity, 0, len(data)) for _, item := range data { activity := contract.DailyActivity{ Date: "", // Leave date empty as per requirement DayOfWeek: getStringValue(item["day_of_week"]), IncomingCount: getInt64Value(item["incoming_count"]), OutgoingCount: getInt64Value(item["outgoing_count"]), } result = append(result, activity) } return result } // Helper functions to safely extract values from interface{} func getStringValue(v interface{}) string { if v == nil { return "" } if str, ok := v.(string); ok { return str } return "" } func getInt64Value(v interface{}) int64 { if v == nil { return 0 } switch val := v.(type) { case int64: return val case float64: return int64(val) case int: return int64(val) default: return 0 } } func getIntValue(v interface{}) int { if v == nil { return 0 } switch val := v.(type) { case int: return val case int64: return int(val) case float64: return int(val) default: return 0 } } func getFloat64Value(v interface{}) float64 { if v == nil { return 0 } switch val := v.(type) { case float64: return val case int64: return float64(val) case int: return float64(val) default: return 0 } }