// internal/services/dashboard_service.go package services import ( "context" "time" "gorm.io/gorm" "lost-and-found/internal/repositories" ) // ✅ KRITERIA BASDAT: Menggunakan VIEWS dari enhancement.sql type DashboardService struct { db *gorm.DB itemRepo *repositories.ItemRepository // ✅ Tambahkan ini } func NewDashboardService(db *gorm.DB) *DashboardService { return &DashboardService{ db: db, itemRepo: repositories.NewItemRepository(db), // ✅ Inisialisasi } } // DashboardStats menggunakan VIEW vw_dashboard_stats type DashboardStats struct { TotalUnclaimed int64 `json:"total_unclaimed"` TotalVerified int64 `json:"total_verified"` TotalLostReports int64 `json:"total_lost_reports"` PendingClaims int64 `json:"pending_claims"` UnnotifiedMatches int64 `json:"unnotified_matches"` } // GetDashboardStats - ✅ MENGGUNAKAN VIEW func (s *DashboardService) GetDashboardStats() (*DashboardStats, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() var stats DashboardStats // ✅ QUERY VIEW vw_dashboard_stats (dari enhancement.sql) err := s.db.WithContext(ctx). Table("vw_dashboard_stats"). Select("*"). Scan(&stats).Error if err != nil { return nil, err } return &stats, nil } // ItemDetail menggunakan VIEW vw_items_detail type ItemDetail struct { ID uint `json:"id"` Name string `json:"name"` CategoryName string `json:"category_name"` CategorySlug string `json:"category_slug"` PhotoURL string `json:"photo_url"` Location string `json:"location"` DateFound time.Time `json:"date_found"` Status string `json:"status"` ReporterName string `json:"reporter_name"` ReporterContact string `json:"reporter_contact"` ExpiresAt *time.Time `json:"expires_at"` ReporterUserName string `json:"reporter_user_name"` ReporterEmail string `json:"reporter_email"` DaysUntilExpire int `json:"days_until_expire"` CreatedAt time.Time `json:"created_at"` } // GetItemsWithDetails - ✅ MENGGUNAKAN VIEW func (s *DashboardService) GetItemsWithDetails(page, limit int, status string) ([]ItemDetail, int64, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() var items []ItemDetail var total int64 query := s.db.WithContext(ctx).Table("vw_items_detail") if status != "" { query = query.Where("status = ?", status) } // Count if err := query.Count(&total).Error; err != nil { return nil, 0, err } // Paginate offset := (page - 1) * limit err := query.Offset(offset).Limit(limit).Scan(&items).Error if err != nil { return nil, 0, err } return items, total, nil } // ClaimDetail menggunakan VIEW vw_claims_detail type ClaimDetail struct { ID uint `json:"id"` Status string `json:"status"` ItemName string `json:"item_name"` CategoryName string `json:"category_name"` ClaimantName string `json:"claimant_name"` ClaimantEmail string `json:"claimant_email"` ClaimantPhone string `json:"claimant_phone"` ClaimDescription string `json:"claim_description"` Contact string `json:"contact"` SimilarityScore *float64 `json:"similarity_score"` VerifiedAt *time.Time `json:"verified_at"` VerifiedByName string `json:"verified_by_name"` Notes string `json:"notes"` CreatedAt time.Time `json:"created_at"` } // GetClaimsWithDetails - ✅ MENGGUNAKAN VIEW func (s *DashboardService) GetClaimsWithDetails(status string) ([]ClaimDetail, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() var claims []ClaimDetail query := s.db.WithContext(ctx).Table("vw_claims_detail") if status != "" { query = query.Where("status = ?", status) } err := query.Order("created_at DESC").Scan(&claims).Error if err != nil { return nil, err } return claims, nil } // MatchDetail menggunakan VIEW vw_match_results_detail type MatchDetail struct { ID uint `json:"id"` LostItemName string `json:"lost_item_name"` LostByUserID uint `json:"lost_by_user_id"` LostByUserName string `json:"lost_by_user_name"` LostByEmail string `json:"lost_by_email"` FoundItemName string `json:"found_item_name"` FoundByName string `json:"found_by_name"` SimilarityScore float64 `json:"similarity_score"` IsNotified bool `json:"is_notified"` MatchedAt time.Time `json:"matched_at"` FoundItemID uint `json:"found_item_id"` LostItemID uint `json:"lost_item_id"` } // GetMatchesWithDetails - ✅ MENGGUNAKAN VIEW func (s *DashboardService) GetMatchesWithDetails(minScore float64) ([]MatchDetail, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() var matches []MatchDetail query := s.db.WithContext(ctx).Table("vw_match_results_detail") if minScore > 0 { query = query.Where("similarity_score >= ?", minScore) } err := query.Limit(100).Scan(&matches).Error if err != nil { return nil, err } return matches, nil } // CategoryStats menggunakan VIEW vw_category_stats type CategoryStats struct { ID uint `json:"id"` Name string `json:"name"` Slug string `json:"slug"` TotalItems int `json:"total_items"` UnclaimedItems int `json:"unclaimed_items"` VerifiedItems int `json:"verified_items"` TotalLostReports int `json:"total_lost_reports"` } // GetCategoryStats - ✅ MENGGUNAKAN VIEW func (s *DashboardService) GetCategoryStats() ([]CategoryStats, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() var stats []CategoryStats err := s.db.WithContext(ctx). Table("vw_category_stats"). Order("total_items DESC"). Scan(&stats).Error if err != nil { return nil, err } return stats, nil } // UserActivity menggunakan VIEW vw_user_activity type UserActivity struct { ID uint `json:"id"` Name string `json:"name"` Email string `json:"email"` RoleName string `json:"role_name"` ItemsReported int `json:"items_reported"` LostItemsReported int `json:"lost_items_reported"` ClaimsMade int `json:"claims_made"` ClaimsApproved int `json:"claims_approved"` MemberSince time.Time `json:"member_since"` } // GetUserActivity - ✅ MENGGUNAKAN VIEW func (s *DashboardService) GetUserActivity(limit int) ([]UserActivity, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() var activities []UserActivity err := s.db.WithContext(ctx). Table("vw_user_activity"). Order("items_reported DESC"). Limit(limit). Scan(&activities).Error if err != nil { return nil, err } return activities, nil } func (s *DashboardService) GetStatsFromSP() (map[string]int64, error) { return s.itemRepo.GetDashboardStatsSP() } // RecentActivity menggunakan VIEW vw_recent_activities type RecentActivity struct { ID uint `json:"id"` Action string `json:"action"` EntityType string `json:"entity_type"` EntityID *uint `json:"entity_id"` Details string `json:"details"` UserName string `json:"user_name"` UserEmail string `json:"user_email"` UserRole string `json:"user_role"` IPAddress string `json:"ip_address"` CreatedAt time.Time `json:"created_at"` } // GetRecentActivities - ✅ MENGGUNAKAN VIEW (limited to 100) func (s *DashboardService) GetRecentActivities() ([]RecentActivity, error) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() var activities []RecentActivity err := s.db.WithContext(ctx). Table("vw_recent_activities"). Scan(&activities).Error if err != nil { return nil, err } return activities, nil }