303 lines
10 KiB
Go
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)
|
|
} |