416 lines
13 KiB
Go
416 lines
13 KiB
Go
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
|
|
}
|
|
}
|