Basdat/internal/controllers/admin_controller.go
2025-12-20 00:01:08 +07:00

303 lines
10 KiB
Go

// internal/controllers/admin_controller.go
package controllers
import (
"lost-and-found/internal/repositories"
"lost-and-found/internal/services"
"lost-and-found/internal/utils"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type AdminController struct {
DB *gorm.DB // ✅ DITAMBAHKAN: Untuk akses transaksi manual (Begin/Commit)
userService *services.UserService
itemRepo *repositories.ItemRepository
claimRepo *repositories.ClaimRepository
archiveService *services.ArchiveService // ✅ Service Archive
auditService *services.AuditService
dashboardService *services.DashboardService
itemService *services.ItemService
}
func NewAdminController(db *gorm.DB) *AdminController {
return &AdminController{
DB: db, // ✅ Simpan koneksi DB
userService: services.NewUserService(db),
itemRepo: repositories.NewItemRepository(db),
claimRepo: repositories.NewClaimRepository(db),
archiveService: services.NewArchiveService(db),
auditService: services.NewAuditService(db),
dashboardService: services.NewDashboardService(db),
itemService: services.NewItemService(db),
}
}
// GetDashboardStats - ✅ MENGGUNAKAN VIEW vw_dashboard_stats
// GET /api/admin/dashboard
func (c *AdminController) GetDashboardStats(ctx *gin.Context) {
// 1. Ambil stats dasar dari View
dashStats, err := c.dashboardService.GetDashboardStats()
if err != nil {
utils.ErrorResponse(ctx, http.StatusInternalServerError, "Failed to get dashboard stats", err.Error())
return
}
// 2. Hitung Total User
allUsers, totalUsers, _ := c.userService.GetAllUsers(1, 10000)
// 3. Hitung Total Items
totalItems, _ := c.itemRepo.CountAll()
// 4. Hitung Total Claims
totalClaims, _ := c.claimRepo.CountAll()
// 5. Ambil Statistik Arsip
archiveStats, err := c.archiveService.GetArchiveStats()
var totalArchives int64 = 0
// Type assertion yang aman
if err == nil {
if val, ok := archiveStats["total"]; ok {
switch v := val.(type) {
case int:
totalArchives = int64(v)
case int64:
totalArchives = v
case float64:
totalArchives = int64(v)
}
}
}
// 6. Ambil Category Stats
categoryStats, _ := c.dashboardService.GetCategoryStats()
_, totalAuditLogs, _ := c.auditService.GetAllAuditLogs(1, 1, "", "", nil)
// 7. Susun Response
stats := map[string]interface{}{
"items": map[string]interface{}{
"total": totalItems,
"unclaimed": dashStats.TotalUnclaimed,
"verified": dashStats.TotalVerified,
},
"claims": map[string]interface{}{
"total": totalClaims,
"pending": dashStats.PendingClaims,
},
"archives": map[string]interface{}{
"total": totalArchives,
},
"lost_items": map[string]interface{}{
"total": dashStats.TotalLostReports,
},
"matches": map[string]interface{}{
"unnotified": dashStats.UnnotifiedMatches,
},
"users": map[string]interface{}{
"total": totalUsers,
"count": len(allUsers),
},
"categories": categoryStats,
"audit_logs": map[string]interface{}{
"total": totalAuditLogs,
},
}
utils.SuccessResponse(ctx, http.StatusOK, "Dashboard stats retrieved", stats)
}
// GetAuditLogs gets audit logs (admin only)
// GET /api/admin/audit-logs
func (c *AdminController) GetAuditLogs(ctx *gin.Context) {
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "20"))
action := ctx.Query("action")
entityType := ctx.Query("entity_type")
var userID *uint
if userIDStr := ctx.Query("user_id"); userIDStr != "" {
id, _ := strconv.ParseUint(userIDStr, 10, 32)
userID = new(uint)
*userID = uint(id)
}
logs, total, err := c.auditService.GetAllAuditLogs(page, limit, action, entityType, userID)
if err != nil {
utils.ErrorResponse(ctx, http.StatusInternalServerError, "Failed to get audit logs", err.Error())
return
}
utils.SendPaginatedResponse(ctx, http.StatusOK, "Audit logs retrieved", logs, total, page, limit)
}
// GetItemsDetail - Get Items with Details (PAKAI VIEW)
// GET /api/admin/items-detail
func (c *AdminController) GetItemsDetail(ctx *gin.Context) {
page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10"))
status := ctx.Query("status")
items, total, err := c.dashboardService.GetItemsWithDetails(page, limit, status)
if err != nil {
utils.ErrorResponse(ctx, http.StatusInternalServerError, "Failed to get items detail", err.Error())
return
}
utils.SendPaginatedResponse(ctx, http.StatusOK, "Items detail retrieved", items, total, page, limit)
}
// =========================================================================
// OPSI 1: ARSIP VIA STORED PROCEDURE (Database Logic)
// =========================================================================
// TriggerAutoArchive manually triggers the cleanup procedure
// POST /api/admin/archive/trigger
func (c *AdminController) TriggerAutoArchive(ctx *gin.Context) {
ipAddress := ctx.ClientIP()
userAgent := ctx.Request.UserAgent()
// Memanggil service yang menjalankan RAW SQL "CALL sp_archive_expired_items()"
count, err := c.itemService.RunAutoArchive(ipAddress, userAgent)
if err != nil {
utils.ErrorResponse(ctx, http.StatusInternalServerError, "Failed to run archive procedure", err.Error())
return
}
utils.SuccessResponse(ctx, http.StatusOK, "Auto-archive completed", gin.H{
"archived_count": count,
"method": "Stored Procedure (Database)",
})
}
// =========================================================================
// OPSI 2: ARSIP (Golang Logic - Begin/Commit)
// =========================================================================
// ArchiveExpired melakukan arsip menggunakan Begin/Commit/Rollback di Golang
// POST /api/admin/archive/manual-transaction
func (c *AdminController) ArchiveExpiredItemsManual(ctx *gin.Context) {
// 1. BEGIN: Memulai Transaksi
tx := c.DB.Begin()
if tx.Error != nil {
utils.ErrorResponse(ctx, http.StatusInternalServerError, "Failed to start transaction", tx.Error.Error())
return
}
// Safety: Defer Rollback (Jaga-jaga jika aplikasi crash/panic)
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
copyQuery := `
INSERT INTO archives (item_id, name, description, category, found_location, found_date, image_url, finder_id)
SELECT id, name, description, category, found_location, found_date, image_url, user_id
FROM items
WHERE expires_at < NOW() AND status = 'unclaimed'
`
if err := tx.Exec(copyQuery).Error; err != nil {
tx.Rollback() // ❌ ROLLBACK JIKA GAGAL COPY
utils.ErrorResponse(ctx, http.StatusInternalServerError, "Transaction Failed: Could not copy to archives", err.Error())
return
}
// 3. LANGKAH 2: Update status barang di tabel items
updateQuery := `
UPDATE items
SET status = 'expired'
WHERE expires_at < NOW() AND status = 'unclaimed'
`
result := tx.Exec(updateQuery)
if result.Error != nil {
tx.Rollback() // ❌ ROLLBACK JIKA GAGAL UPDATE (Data di archives juga akan batal masuk)
utils.ErrorResponse(ctx, http.StatusInternalServerError, "Transaction Failed: Could not update item status", result.Error.Error())
return
}
// Hitung berapa baris yang berubah
itemsArchived := result.RowsAffected
// 4. COMMIT: Simpan Perubahan Permanen
if err := tx.Commit().Error; err != nil {
utils.ErrorResponse(ctx, http.StatusInternalServerError, "Transaction Failed: Could not commit changes", err.Error())
return
}
// Sukses
utils.SuccessResponse(ctx, http.StatusOK, "Manual Archive Transaction Successful", gin.H{
"archived_count": itemsArchived,
"method": "Go Transaction (Begin/Commit/Rollback)",
})
}
// GetFastDashboardStats uses Stored Procedure instead of View/Count
// GET /api/admin/dashboard/fast
func (c *AdminController) GetFastDashboardStats(ctx *gin.Context) {
stats, err := c.dashboardService.GetStatsFromSP()
if err != nil {
utils.ErrorResponse(ctx, http.StatusInternalServerError, "Failed to get stats from SP", err.Error())
return
}
utils.SuccessResponse(ctx, http.StatusOK, "Dashboard stats (SP) retrieved", stats)
}
// GetClaimsDetail - Get Claims with Details (PAKAI VIEW)
// GET /api/admin/claims-detail
func (c *AdminController) GetClaimsDetail(ctx *gin.Context) {
status := ctx.Query("status")
claims, err := c.dashboardService.GetClaimsWithDetails(status)
if err != nil {
utils.ErrorResponse(ctx, http.StatusInternalServerError, "Failed to get claims detail", err.Error())
return
}
utils.SuccessResponse(ctx, http.StatusOK, "Claims detail retrieved", claims)
}
// GetMatchesDetail - Get Match Results with Details (PAKAI VIEW)
// GET /api/admin/matches-detail
func (c *AdminController) GetMatchesDetail(ctx *gin.Context) {
minScore, _ := strconv.ParseFloat(ctx.DefaultQuery("min_score", "0"), 64)
matches, err := c.dashboardService.GetMatchesWithDetails(minScore)
if err != nil {
utils.ErrorResponse(ctx, http.StatusInternalServerError, "Failed to get matches detail", err.Error())
return
}
utils.SuccessResponse(ctx, http.StatusOK, "Matches detail retrieved", matches)
}
// GetUserActivity - Get User Activity (PAKAI VIEW)
// GET /api/admin/user-activity
func (c *AdminController) GetUserActivity(ctx *gin.Context) {
limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "20"))
activities, err := c.dashboardService.GetUserActivity(limit)
if err != nil {
utils.ErrorResponse(ctx, http.StatusInternalServerError, "Failed to get user activity", err.Error())
return
}
utils.SuccessResponse(ctx, http.StatusOK, "User activity retrieved", activities)
}
// GetRecentActivities - Get Recent Activities (PAKAI VIEW vw_recent_activities)
// GET /api/admin/recent-activities
func (c *AdminController) GetRecentActivities(ctx *gin.Context) {
activities, err := c.dashboardService.GetRecentActivities()
if err != nil {
utils.ErrorResponse(ctx, http.StatusInternalServerError, "Failed to get recent activities", err.Error())
return
}
utils.SuccessResponse(ctx, http.StatusOK, "Recent activities retrieved", activities)
}