dukcapil/internal/service/analytics_service.go
Aditya Siregar aa662a321f Update
2025-09-01 12:06:14 +07:00

413 lines
13 KiB
Go

package service
import (
"context"
"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
}
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.GetMonthlyTrend(ctx, 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.GetDailyActivity(ctx, 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
}
}