update dashboard analytic

This commit is contained in:
efrilm 2025-10-20 18:47:29 +07:00
parent ea980f8cf7
commit f391b6d853
2 changed files with 112 additions and 45 deletions

View File

@ -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)

View File

@ -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
}
}
}