Initial commit - Lost and Found Revisi

This commit is contained in:
bambang-code1
2025-12-20 00:01:08 +07:00
commit f4c838471c
210 changed files with 30706 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
// internal/repositories/archive_repo.go
package repositories
import (
"errors"
"lost-and-found/internal/models"
"gorm.io/gorm"
)
type ArchiveRepository struct {
db *gorm.DB
}
func NewArchiveRepository(db *gorm.DB) *ArchiveRepository {
return &ArchiveRepository{db: db}
}
// Create creates a new archive record
func (r *ArchiveRepository) Create(archive *models.Archive) error {
return r.db.Create(archive).Error
}
// FindByID finds archive by ID
func (r *ArchiveRepository) FindByID(id uint) (*models.Archive, error) {
var archive models.Archive
err := r.db.Preload("Category").Preload("Claimer").Preload("Claimer.Role").First(&archive, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("archive not found")
}
return nil, err
}
return &archive, nil
}
// FindAll returns all archived items with filters
func (r *ArchiveRepository) FindAll(page, limit int, reason, search string) ([]models.Archive, int64, error) {
var archives []models.Archive
var total int64
query := r.db.Model(&models.Archive{})
// Apply filters
if reason != "" {
query = query.Where("archived_reason = ?", reason)
}
if search != "" {
// GANTI ILIKE MENJADI LIKE
query = query.Where("name LIKE ? OR location LIKE ?", "%"+search+"%", "%"+search+"%")
}
// Count total
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Get paginated results
offset := (page - 1) * limit
err := query.Preload("Category").Preload("Claimer").Preload("Claimer.Role").
Order("archived_at DESC").
Offset(offset).Limit(limit).Find(&archives).Error
if err != nil {
return nil, 0, err
}
return archives, total, nil
}
// FindByItemID finds archive by original item ID
func (r *ArchiveRepository) FindByItemID(itemID uint) (*models.Archive, error) {
var archive models.Archive
err := r.db.Where("item_id = ?", itemID).Preload("Category").Preload("Claimer").First(&archive).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("archive not found")
}
return nil, err
}
return &archive, nil
}
// Delete permanently deletes an archive
func (r *ArchiveRepository) Delete(id uint) error {
return r.db.Unscoped().Delete(&models.Archive{}, id).Error
}
// CountByReason counts archives by reason
func (r *ArchiveRepository) CountByReason(reason string) (int64, error) {
var count int64
err := r.db.Model(&models.Archive{}).Where("archived_reason = ?", reason).Count(&count).Error
return count, err
}
+105
View File
@@ -0,0 +1,105 @@
// internal/repositories/audit_log_repo.go
package repositories
import (
"errors"
"lost-and-found/internal/models"
"gorm.io/gorm"
)
type AuditLogRepository struct {
db *gorm.DB
}
func NewAuditLogRepository(db *gorm.DB) *AuditLogRepository {
return &AuditLogRepository{db: db}
}
// Create creates a new audit log
func (r *AuditLogRepository) Create(log *models.AuditLog) error {
return r.db.Create(log).Error
}
// FindByID finds audit log by ID
func (r *AuditLogRepository) FindByID(id uint) (*models.AuditLog, error) {
var log models.AuditLog
err := r.db.Preload("User").Preload("User.Role").First(&log, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("audit log not found")
}
return nil, err
}
return &log, nil
}
// FindAll returns all audit logs with filters
func (r *AuditLogRepository) FindAll(page, limit int, action, entityType string, userID *uint) ([]models.AuditLog, int64, error) {
var logs []models.AuditLog
var total int64
query := r.db.Model(&models.AuditLog{})
// Apply filters
if action != "" {
query = query.Where("action = ?", action)
}
if entityType != "" {
query = query.Where("entity_type = ?", entityType)
}
if userID != nil {
query = query.Where("user_id = ?", *userID)
}
// Count total
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Get paginated results
offset := (page - 1) * limit
err := query.Preload("User").Preload("User.Role").
Order("created_at DESC").
Offset(offset).Limit(limit).Find(&logs).Error
if err != nil {
return nil, 0, err
}
return logs, total, nil
}
// FindByUser finds audit logs by user
func (r *AuditLogRepository) FindByUser(userID uint, page, limit int) ([]models.AuditLog, int64, error) {
return r.FindAll(page, limit, "", "", &userID)
}
// FindByEntity finds audit logs by entity
func (r *AuditLogRepository) FindByEntity(entityType string, entityID uint, page, limit int) ([]models.AuditLog, int64, error) {
var logs []models.AuditLog
var total int64
query := r.db.Model(&models.AuditLog{}).
Where("entity_type = ? AND entity_id = ?", entityType, entityID)
// Count total
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Get paginated results
offset := (page - 1) * limit
err := query.Preload("User").Preload("User.Role").
Order("created_at DESC").
Offset(offset).Limit(limit).Find(&logs).Error
if err != nil {
return nil, 0, err
}
return logs, total, nil
}
// Log creates a new audit log entry (helper method)
func (r *AuditLogRepository) Log(userID *uint, action, entityType string, entityID *uint, details, ipAddress, userAgent string) error {
return models.CreateAuditLog(r.db, userID, action, entityType, entityID, details, ipAddress, userAgent)
}
+102
View File
@@ -0,0 +1,102 @@
// internal/repositories/category_repo.go
package repositories
import (
"errors"
"lost-and-found/internal/models"
"gorm.io/gorm"
)
type CategoryRepository struct {
db *gorm.DB
}
func NewCategoryRepository(db *gorm.DB) *CategoryRepository {
return &CategoryRepository{db: db}
}
// Create creates a new category
func (r *CategoryRepository) Create(category *models.Category) error {
return r.db.Create(category).Error
}
// FindByID finds category by ID
func (r *CategoryRepository) FindByID(id uint) (*models.Category, error) {
var category models.Category
err := r.db.First(&category, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("category not found")
}
return nil, err
}
return &category, nil
}
// FindBySlug finds category by slug
func (r *CategoryRepository) FindBySlug(slug string) (*models.Category, error) {
var category models.Category
err := r.db.Where("slug = ?", slug).First(&category).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("category not found")
}
return nil, err
}
return &category, nil
}
// FindAll returns all categories
func (r *CategoryRepository) FindAll() ([]models.Category, error) {
var categories []models.Category
err := r.db.Order("name ASC").Find(&categories).Error
return categories, err
}
// Update updates category data
func (r *CategoryRepository) Update(category *models.Category) error {
return r.db.Save(category).Error
}
// Delete soft deletes a category
func (r *CategoryRepository) Delete(id uint) error {
return r.db.Delete(&models.Category{}, id).Error
}
// GetCategoryWithItemCount gets category with item count
func (r *CategoryRepository) GetCategoryWithItemCount(id uint) (*models.Category, int64, error) {
category, err := r.FindByID(id)
if err != nil {
return nil, 0, err
}
var count int64
if err := r.db.Model(&models.Item{}).Where("category_id = ?", id).Count(&count).Error; err != nil {
return nil, 0, err
}
return category, count, nil
}
// GetAllWithItemCount gets all categories with item counts
func (r *CategoryRepository) GetAllWithItemCount() ([]models.CategoryResponse, error) {
var categories []models.Category
if err := r.db.Order("name ASC").Find(&categories).Error; err != nil {
return nil, err
}
var responses []models.CategoryResponse
for _, cat := range categories {
var count int64
if err := r.db.Model(&models.Item{}).Where("category_id = ?", cat.ID).Count(&count).Error; err != nil {
return nil, err
}
response := cat.ToResponse()
response.ItemCount = count
responses = append(responses, response)
}
return responses, nil
}
+40
View File
@@ -0,0 +1,40 @@
package repositories
import (
"lost-and-found/internal/models"
"gorm.io/gorm"
)
type ChatRepository struct {
db *gorm.DB
}
func NewChatRepository(db *gorm.DB) *ChatRepository {
return &ChatRepository{db: db}
}
func (r *ChatRepository) Create(chat *models.ChatMessage) error {
return r.db.Create(chat).Error
}
func (r *ChatRepository) FindByUserID(userID uint, limit int) ([]models.ChatMessage, error) {
var chats []models.ChatMessage
err := r.db.Where("user_id = ?", userID).
Order("created_at DESC").
Limit(limit).
Find(&chats).Error
return chats, err
}
func (r *ChatRepository) GetUserChatHistory(userID uint, limit int) ([]models.ChatMessage, error) {
var chats []models.ChatMessage
err := r.db.Where("user_id = ?", userID).
Order("created_at DESC").
Limit(limit).
Find(&chats).Error
return chats, err
}
func (r *ChatRepository) DeleteUserHistory(userID uint) error {
return r.db.Where("user_id = ?", userID).Delete(&models.ChatMessage{}).Error
}
+166
View File
@@ -0,0 +1,166 @@
// internal/repositories/claim_repo.go
package repositories
import (
"errors"
"lost-and-found/internal/models"
"gorm.io/gorm"
)
type ClaimRepository struct {
db *gorm.DB
}
func NewClaimRepository(db *gorm.DB) *ClaimRepository {
return &ClaimRepository{db: db}
}
// Create creates a new claim
func (r *ClaimRepository) Create(claim *models.Claim) error {
return r.db.Create(claim).Error
}
// FindByID finds claim by ID
func (r *ClaimRepository) FindByID(id uint) (*models.Claim, error) {
var claim models.Claim
err := r.db.
Preload("Item").
Preload("Item.Category").
Preload("Item.CaseClosedBy_User"). // ✅ ADD THIS
Preload("Item.CaseClosedBy_User.Role"). // ✅ ADD THIS
Preload("User").
Preload("User.Role").
Preload("Verifier").
Preload("Verifier.Role").
Preload("Verification").
First(&claim, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("claim not found")
}
return nil, err
}
return &claim, nil
}
// FindAll returns all claims with filters
func (r *ClaimRepository) FindAll(page, limit int, status string, itemID, userID *uint) ([]models.Claim, int64, error) {
var claims []models.Claim
var total int64
query := r.db.Model(&models.Claim{})
// Apply filters... (biarkan kode filter yang ada)
if status != "" {
query = query.Where("status = ?", status)
}
if itemID != nil {
query = query.Where("item_id = ?", *itemID)
}
if userID != nil {
query = query.Where("user_id = ?", *userID)
}
// Count total...
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Get paginated results
offset := (page - 1) * limit
// ✅ PERBAIKAN: Tambahkan Preload yang hilang di sini
err := query.
Preload("Item").
Preload("Item.Category").
Preload("Item.CaseClosedBy_User"). // <--- TAMBAHKAN INI
Preload("Item.CaseClosedBy_User.Role"). // <--- TAMBAHKAN INI
Preload("User").
Preload("User.Role").
Preload("Verifier").
Preload("Verifier.Role").
Preload("Verification").
Order("created_at DESC").
Offset(offset).Limit(limit).Find(&claims).Error
if err != nil {
return nil, 0, err
}
return claims, total, nil
}
// Update updates claim data
func (r *ClaimRepository) Update(claim *models.Claim) error {
return r.db.Save(claim).Error
}
// Delete soft deletes a claim
func (r *ClaimRepository) Delete(id uint) error {
return r.db.Delete(&models.Claim{}, id).Error
}
// CheckExistingClaim checks if user already claimed an item
func (r *ClaimRepository) CheckExistingClaim(userID, itemID uint) (bool, error) {
var count int64
err := r.db.Model(&models.Claim{}).
Where("user_id = ? AND item_id = ? AND status != ?", userID, itemID, models.ClaimStatusRejected).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
// FindByItem finds claims for an item
func (r *ClaimRepository) FindByItem(itemID uint) ([]models.Claim, error) {
var claims []models.Claim
err := r.db.Where("item_id = ?", itemID).
Preload("User").Preload("User.Role").
Preload("Verification").
Order("created_at DESC").Find(&claims).Error
return claims, err
}
// FindByUser finds claims by user
func (r *ClaimRepository) FindByUser(userID uint, page, limit int) ([]models.Claim, int64, error) {
var claims []models.Claim
var total int64
query := r.db.Model(&models.Claim{}).Where("user_id = ?", userID)
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
offset := (page - 1) * limit
err := query.
Preload("Item").
Preload("Item.Category").
Preload("LostItem"). // ← TAMBAHKAN INI!
Preload("User").
Preload("User.Role").
Preload("Verification").
Order("created_at DESC").
Offset(offset).Limit(limit).Find(&claims).Error
if err != nil {
return nil, 0, err
}
return claims, total, nil
}
// CountByStatus counts claims by status
func (r *ClaimRepository) CountByStatus(status string) (int64, error) {
var count int64
err := r.db.Model(&models.Claim{}).Where("status = ?", status).Count(&count).Error
return count, err
}
// FindPendingClaims finds all pending claims
func (r *ClaimRepository) FindPendingClaims(page, limit int) ([]models.Claim, int64, error) {
return r.FindAll(page, limit, models.ClaimStatusPending, nil, nil)
}
func (r *ClaimRepository) CountAll() (int64, error) {
var count int64
err := r.db.Model(&models.Claim{}).Count(&count).Error
return count, err
}
@@ -0,0 +1,67 @@
// internal/repositories/claim_verification_repo.go
package repositories
import (
"errors"
"lost-and-found/internal/models"
"gorm.io/gorm"
)
type ClaimVerificationRepository struct {
db *gorm.DB
}
func NewClaimVerificationRepository(db *gorm.DB) *ClaimVerificationRepository {
return &ClaimVerificationRepository{db: db}
}
// Create creates a new claim verification
func (r *ClaimVerificationRepository) Create(verification *models.ClaimVerification) error {
return r.db.Create(verification).Error
}
// FindByID finds claim verification by ID
func (r *ClaimVerificationRepository) FindByID(id uint) (*models.ClaimVerification, error) {
var verification models.ClaimVerification
err := r.db.Preload("Claim").First(&verification, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("claim verification not found")
}
return nil, err
}
return &verification, nil
}
// FindByClaimID finds claim verification by claim ID
func (r *ClaimVerificationRepository) FindByClaimID(claimID uint) (*models.ClaimVerification, error) {
var verification models.ClaimVerification
err := r.db.Where("claim_id = ?", claimID).Preload("Claim").First(&verification).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil // Return nil if not found (not an error)
}
return nil, err
}
return &verification, nil
}
// Update updates claim verification
func (r *ClaimVerificationRepository) Update(verification *models.ClaimVerification) error {
return r.db.Save(verification).Error
}
// Delete deletes a claim verification
func (r *ClaimVerificationRepository) Delete(id uint) error {
return r.db.Delete(&models.ClaimVerification{}, id).Error
}
// FindHighMatches finds high match verifications (>= 70%)
func (r *ClaimVerificationRepository) FindHighMatches() ([]models.ClaimVerification, error) {
var verifications []models.ClaimVerification
err := r.db.Where("similarity_score >= ?", 70.0).
Preload("Claim").Preload("Claim.Item").Preload("Claim.User").
Order("similarity_score DESC").Find(&verifications).Error
return verifications, err
}
+254
View File
@@ -0,0 +1,254 @@
// internal/repositories/item_repo.go
package repositories
import (
"errors"
"lost-and-found/internal/models"
"time"
"gorm.io/gorm"
)
type ItemRepository struct {
db *gorm.DB
}
func NewItemRepository(db *gorm.DB) *ItemRepository {
return &ItemRepository{db: db}
}
// Create creates a new item
func (r *ItemRepository) Create(item *models.Item) error {
return r.db.Create(item).Error
}
// FindByID finds item by ID
func (r *ItemRepository) FindByID(id uint) (*models.Item, error) {
var item models.Item
err := r.db.
Preload("Category").
Preload("Reporter").
Preload("Reporter.Role").
Preload("CaseClosedBy_User").
Preload("CaseClosedBy_User.Role").
Preload("Claims", "deleted_at IS NULL"). // ✅ TAMBAH INI!
First(&item, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("item not found")
}
return nil, err
}
return &item, nil
}
// ✅ IMPLEMENTASI PROCEDURE 1: Archive Expired Items
// CallArchiveExpiredProcedure memanggil SP sp_archive_expired_items
func (r *ItemRepository) CallArchiveExpiredProcedure() (int, error) {
var archivedCount int
// Menggunakan transaksi untuk eksekusi procedure
err := r.db.Transaction(func(tx *gorm.DB) error {
// 1. Eksekusi Procedure dengan variabel output session MySQL (@count)
if err := tx.Exec("CALL sp_archive_expired_items(@count)").Error; err != nil {
return err
}
// 2. Ambil nilai dari variabel output
// Kita menggunakan Raw SQL karena GORM tidak support OUT param secara native di semua driver
type Result struct {
Count int
}
var res Result
if err := tx.Raw("SELECT @count as count").Scan(&res).Error; err != nil {
return err
}
archivedCount = res.Count
return nil
})
return archivedCount, err
}
// ✅ IMPLEMENTASI PROCEDURE 2: Dashboard Stats
// GetDashboardStatsSP memanggil SP sp_get_dashboard_stats
func (r *ItemRepository) GetDashboardStatsSP() (map[string]int64, error) {
stats := make(map[string]int64)
err := r.db.Transaction(func(tx *gorm.DB) error {
// 1. Eksekusi Procedure dengan 4 variabel output
query := "CALL sp_get_dashboard_stats(@total, @unclaimed, @verified, @pending)"
if err := tx.Exec(query).Error; err != nil {
return err
}
// 2. Select nilai variabel tersebut
type Result struct {
Total int64
Unclaimed int64
Verified int64
Pending int64
}
var res Result
querySelect := "SELECT @total as total, @unclaimed as unclaimed, @verified as verified, @pending as pending"
if err := tx.Raw(querySelect).Scan(&res).Error; err != nil {
return err
}
stats["total_items"] = res.Total
stats["unclaimed_items"] = res.Unclaimed
stats["verified_items"] = res.Verified
stats["pending_claims"] = res.Pending
return nil
})
return stats, err
}
// FindAll returns all items with filters
// internal/repositories/item_repo.go
func (r *ItemRepository) FindAll(page, limit int, status, category, search string) ([]models.Item, int64, error) {
var items []models.Item
var total int64
query := r.db.Model(&models.Item{})
if status != "" {
if status == "!expired" {
query = query.Where("status NOT IN ?", []string{models.ItemStatusExpired, models.ItemStatusCaseClosed})
} else {
query = query.Where("status = ?", status)
}
}
if category != "" {
query = query.Joins("JOIN categories ON categories.id = items.category_id").Where("categories.slug = ?", category)
}
if search != "" {
query = query.Where("name LIKE ? OR description LIKE ?", "%"+search+"%", "%"+search+"%")
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
offset := (page - 1) * limit
// ✅ FIX: Tambahkan Preload Claims untuk hitung status dinamis
err := query.
Preload("Category").
Preload("Reporter").
Preload("Reporter.Role").
Preload("Claims", "deleted_at IS NULL"). // ✅ TAMBAH INI!
Order("date_found DESC").
Offset(offset).
Limit(limit).
Find(&items).Error
if err != nil {
return nil, 0, err
}
return items, total, nil
}
// Update updates item data
func (r *ItemRepository) Update(item *models.Item) error {
return r.db.Save(item).Error
}
// UpdateStatus updates item status
func (r *ItemRepository) UpdateStatus(id uint, status string) error {
return r.db.Model(&models.Item{}).Where("id = ?", id).Update("status", status).Error
}
// Delete soft deletes an item
func (r *ItemRepository) Delete(id uint) error {
return r.db.Delete(&models.Item{}, id).Error
}
// FindExpired finds expired items
func (r *ItemRepository) FindExpired() ([]models.Item, error) {
var items []models.Item
now := time.Now()
err := r.db.Where("expires_at <= ? AND status = ?", now, models.ItemStatusUnclaimed).
Preload("Category").Find(&items).Error
return items, err
}
// ArchiveItem moves item to archive
func (r *ItemRepository) ArchiveItem(item *models.Item, reason string, claimedBy *uint) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Create archive record
archive := models.CreateFromItem(item, reason, claimedBy)
if err := tx.Create(archive).Error; err != nil {
return err
}
// Update item status
if err := tx.Model(item).Updates(map[string]interface{}{
"status": models.ItemStatusExpired,
}).Error; err != nil {
return err
}
return nil
})
}
// CountByStatus counts items by status
func (r *ItemRepository) CountByStatus(status string) (int64, error) {
var count int64
err := r.db.Model(&models.Item{}).Where("status = ?", status).Count(&count).Error
return count, err
}
// FindByReporter finds items by reporter ID
func (r *ItemRepository) FindByReporter(reporterID uint, page, limit int) ([]models.Item, int64, error) {
var items []models.Item
var total int64
query := r.db.Model(&models.Item{}).Where("reporter_id = ?", reporterID)
// Count total
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Get paginated results
offset := (page - 1) * limit
err := query.Preload("Category").Order("date_found DESC").
Offset(offset).Limit(limit).Find(&items).Error
if err != nil {
return nil, 0, err
}
return items, total, nil
}
// SearchForMatching searches items for matching with lost items
func (r *ItemRepository) SearchForMatching(categoryID uint, name, color string) ([]models.Item, error) {
var items []models.Item
query := r.db.Where("status = ? AND category_id = ?", models.ItemStatusUnclaimed, categoryID)
if name != "" {
query = query.Where("name ILIKE ?", "%"+name+"%")
}
err := query.Preload("Category").Order("date_found DESC").Limit(10).Find(&items).Error
return items, err
}
func (r *ItemRepository) CountAll() (int64, error) {
var count int64
err := r.db.Model(&models.Item{}).Count(&count).Error
return count, err
}
+156
View File
@@ -0,0 +1,156 @@
// internal/repositories/lost_item_repo.go
package repositories
import (
"errors"
"lost-and-found/internal/models"
"gorm.io/gorm"
)
type LostItemRepository struct {
db *gorm.DB
}
func NewLostItemRepository(db *gorm.DB) *LostItemRepository {
return &LostItemRepository{db: db}
}
// Create creates a new lost item report
func (r *LostItemRepository) Create(lostItem *models.LostItem) error {
return r.db.Create(lostItem).Error
}
// FindByID finds lost item by ID
func (r *LostItemRepository) FindByID(id uint) (*models.LostItem, error) {
var lostItem models.LostItem
err := r.db.Preload("Category").Preload("User").Preload("User.Role").First(&lostItem, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("lost item not found")
}
return nil, err
}
return &lostItem, nil
}
// ✅ NEW: FindByMatchedItemID (Dibutuhkan untuk VerifyClaim)
// Mencari lost item yang sudah di-match dengan item tertentu
func (r *LostItemRepository) FindByMatchedItemID(itemID uint) (*models.LostItem, error) {
var lostItem models.LostItem
// Asumsi: Anda punya kolom 'matched_item_id' atau logika matching tersimpan
// Jika logika matching ada di tabel 'match_results', query ini mungkin perlu disesuaikan.
// Namun, untuk struktur simple, kita cari berdasarkan relasi match
// Jika Anda menggunakan tabel terpisah (MatchResult), method ini mungkin tidak direct di sini,
// tapi jika LostItem punya field MatchedItemID:
err := r.db.Where("matched_item_id = ?", itemID).First(&lostItem).Error
if err != nil {
return nil, err
}
return &lostItem, nil
}
// FindAll returns all lost items with filters
func (r *LostItemRepository) FindAll(page, limit int, status, category, search string, userID *uint) ([]models.LostItem, int64, error) {
var lostItems []models.LostItem
var total int64
query := r.db.Model(&models.LostItem{})
// Filter by user if specified
if userID != nil {
query = query.Where("user_id = ?", *userID)
}
// Apply filters
if status != "" {
query = query.Where("status = ?", status)
}
if category != "" {
query = query.Joins("JOIN categories ON categories.id = lost_items.category_id").Where("categories.slug = ?", category)
}
if search != "" {
// ✅ FIX: Ganti ILIKE (Postgres) ke LIKE (MySQL Compatible)
// Jika pakai Postgres, ILIKE boleh dipakai. Jika MySQL, harus LIKE.
query = query.Where("name LIKE ? OR description LIKE ?", "%"+search+"%", "%"+search+"%")
}
// Count total
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Get paginated results
offset := (page - 1) * limit
err := query.Preload("Category").Preload("User").Preload("User.Role").
Order("date_lost DESC").
Offset(offset).Limit(limit).Find(&lostItems).Error
if err != nil {
return nil, 0, err
}
return lostItems, total, nil
}
// Update updates lost item data
func (r *LostItemRepository) Update(lostItem *models.LostItem) error {
return r.db.Save(lostItem).Error
}
// UpdateStatus updates lost item status
func (r *LostItemRepository) UpdateStatus(id uint, status string) error {
return r.db.Model(&models.LostItem{}).Where("id = ?", id).Update("status", status).Error
}
// ✅ NEW: UpdateStatusByUserAndCategory (Dibutuhkan untuk CloseCase)
// Mengubah status lost item milik user tertentu di kategori tertentu
// Berguna saat item 'Found' berubah jadi 'Closed'
func (r *LostItemRepository) UpdateStatusByUserAndCategory(userID, categoryID uint, oldStatus, newStatus string) error {
return r.db.Model(&models.LostItem{}).
Where("user_id = ? AND category_id = ? AND status = ?", userID, categoryID, oldStatus).
Update("status", newStatus).Error
}
// Delete soft deletes a lost item
func (r *LostItemRepository) Delete(id uint) error {
return r.db.Delete(&models.LostItem{}, id).Error
}
// FindByUser finds lost items by user ID
func (r *LostItemRepository) FindByUser(userID uint, page, limit int) ([]models.LostItem, int64, error) {
var lostItems []models.LostItem
var total int64
query := r.db.Model(&models.LostItem{}).Where("user_id = ?", userID)
// Count total
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Get paginated results
offset := (page - 1) * limit
err := query.Preload("Category").Order("date_lost DESC").
Offset(offset).Limit(limit).Find(&lostItems).Error
if err != nil {
return nil, 0, err
}
return lostItems, total, nil
}
// CountByStatus counts lost items by status
func (r *LostItemRepository) CountByStatus(status string) (int64, error) {
var count int64
err := r.db.Model(&models.LostItem{}).Where("status = ?", status).Count(&count).Error
return count, err
}
// FindActiveForMatching finds active lost items for matching
func (r *LostItemRepository) FindActiveForMatching(categoryID uint) ([]models.LostItem, error) {
var lostItems []models.LostItem
err := r.db.Where("status = ? AND category_id = ?", models.LostItemStatusActive, categoryID).
Preload("User").Find(&lostItems).Error
return lostItems, err
}
+125
View File
@@ -0,0 +1,125 @@
// internal/repositories/match_result_repo.go
package repositories
import (
"errors"
"lost-and-found/internal/models"
"gorm.io/gorm"
)
type MatchResultRepository struct {
db *gorm.DB
}
func NewMatchResultRepository(db *gorm.DB) *MatchResultRepository {
return &MatchResultRepository{db: db}
}
// Create creates a new match result
func (r *MatchResultRepository) Create(match *models.MatchResult) error {
return r.db.Create(match).Error
}
// FindByID finds match result by ID
func (r *MatchResultRepository) FindByID(id uint) (*models.MatchResult, error) {
var match models.MatchResult
err := r.db.Preload("LostItem").Preload("LostItem.User").
Preload("Item").Preload("Item.Category").
First(&match, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("match result not found")
}
return nil, err
}
return &match, nil
}
// FindAll returns all match results with filters
func (r *MatchResultRepository) FindAll(page, limit int, lostItemID, itemID *uint) ([]models.MatchResult, int64, error) {
var matches []models.MatchResult
var total int64
query := r.db.Model(&models.MatchResult{})
// Apply filters
if lostItemID != nil {
query = query.Where("lost_item_id = ?", *lostItemID)
}
if itemID != nil {
query = query.Where("item_id = ?", *itemID)
}
// Count total
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Get paginated results
offset := (page - 1) * limit
err := query.Preload("LostItem").Preload("LostItem.User").
Preload("Item").Preload("Item.Category").
Order("similarity_score DESC").
Offset(offset).Limit(limit).Find(&matches).Error
if err != nil {
return nil, 0, err
}
return matches, total, nil
}
// FindByLostItem finds match results for a lost item
func (r *MatchResultRepository) FindByLostItem(lostItemID uint) ([]models.MatchResult, error) {
var matches []models.MatchResult
err := r.db.Where("lost_item_id = ?", lostItemID).
Preload("Item").Preload("Item.Category").
Order("similarity_score DESC").Find(&matches).Error
return matches, err
}
// FindByItem finds match results for an item
func (r *MatchResultRepository) FindByItem(itemID uint) ([]models.MatchResult, error) {
var matches []models.MatchResult
err := r.db.Where("item_id = ?", itemID).
Preload("LostItem").Preload("LostItem.User").
Order("similarity_score DESC").Find(&matches).Error
return matches, err
}
// Update updates match result
func (r *MatchResultRepository) Update(match *models.MatchResult) error {
return r.db.Save(match).Error
}
// MarkAsNotified marks match result as notified
func (r *MatchResultRepository) MarkAsNotified(id uint) error {
return r.db.Model(&models.MatchResult{}).Where("id = ?", id).Update("is_notified", true).Error
}
// FindUnnotifiedMatches finds match results that haven't been notified
func (r *MatchResultRepository) FindUnnotifiedMatches() ([]models.MatchResult, error) {
var matches []models.MatchResult
err := r.db.Where("is_notified = ?", false).
Preload("LostItem").Preload("LostItem.User").
Preload("Item").Preload("Item.Category").
Order("matched_at ASC").Find(&matches).Error
return matches, err
}
// Delete deletes a match result
func (r *MatchResultRepository) Delete(id uint) error {
return r.db.Delete(&models.MatchResult{}, id).Error
}
// CheckExistingMatch checks if a match already exists
func (r *MatchResultRepository) CheckExistingMatch(lostItemID, itemID uint) (bool, error) {
var count int64
err := r.db.Model(&models.MatchResult{}).
Where("lost_item_id = ? AND item_id = ?", lostItemID, itemID).
Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
+104
View File
@@ -0,0 +1,104 @@
// internal/repositories/notification_repo.go
package repositories
import (
"errors"
"lost-and-found/internal/models"
"gorm.io/gorm"
)
type NotificationRepository struct {
db *gorm.DB
}
func NewNotificationRepository(db *gorm.DB) *NotificationRepository {
return &NotificationRepository{db: db}
}
// Create creates a new notification
func (r *NotificationRepository) Create(notification *models.Notification) error {
return r.db.Create(notification).Error
}
// FindByID finds notification by ID
func (r *NotificationRepository) FindByID(id uint) (*models.Notification, error) {
var notification models.Notification
err := r.db.Preload("User").First(&notification, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("notification not found")
}
return nil, err
}
return &notification, nil
}
// FindByUser finds notifications for a user
func (r *NotificationRepository) FindByUser(userID uint, page, limit int, onlyUnread bool) ([]models.Notification, int64, error) {
var notifications []models.Notification
var total int64
query := r.db.Model(&models.Notification{}).Where("user_id = ?", userID)
// Filter unread if specified
if onlyUnread {
query = query.Where("is_read = ?", false)
}
// Count total
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Get paginated results
offset := (page - 1) * limit
err := query.Order("created_at DESC").
Offset(offset).Limit(limit).Find(&notifications).Error
if err != nil {
return nil, 0, err
}
return notifications, total, nil
}
// MarkAsRead marks a notification as read
func (r *NotificationRepository) MarkAsRead(id uint) error {
notification, err := r.FindByID(id)
if err != nil {
return err
}
notification.MarkAsRead()
return r.db.Save(notification).Error
}
// MarkAllAsRead marks all notifications for a user as read
func (r *NotificationRepository) MarkAllAsRead(userID uint) error {
return r.db.Model(&models.Notification{}).
Where("user_id = ? AND is_read = ?", userID, false).
Update("is_read", true).Error
}
// Delete deletes a notification
func (r *NotificationRepository) Delete(id uint) error {
return r.db.Delete(&models.Notification{}, id).Error
}
// DeleteAllForUser deletes all notifications for a user
func (r *NotificationRepository) DeleteAllForUser(userID uint) error {
return r.db.Where("user_id = ?", userID).Delete(&models.Notification{}).Error
}
// CountUnread counts unread notifications for a user
func (r *NotificationRepository) CountUnread(userID uint) (int64, error) {
var count int64
err := r.db.Model(&models.Notification{}).
Where("user_id = ? AND is_read = ?", userID, false).
Count(&count).Error
return count, err
}
// Notify creates a notification (helper method)
func (r *NotificationRepository) Notify(userID uint, notifType, title, message, entityType string, entityID *uint) error {
return models.CreateNotification(r.db, userID, notifType, title, message, entityType, entityID)
}
@@ -0,0 +1,93 @@
// internal/repositories/revision_log_repo.go
package repositories
import (
"errors"
"lost-and-found/internal/models"
"gorm.io/gorm"
)
type RevisionLogRepository struct {
db *gorm.DB
}
func NewRevisionLogRepository(db *gorm.DB) *RevisionLogRepository {
return &RevisionLogRepository{db: db}
}
// Create creates a new revision log
func (r *RevisionLogRepository) Create(log *models.RevisionLog) error {
return r.db.Create(log).Error
}
// FindByID finds revision log by ID
func (r *RevisionLogRepository) FindByID(id uint) (*models.RevisionLog, error) {
var log models.RevisionLog
err := r.db.Preload("Item").Preload("User").Preload("User.Role").First(&log, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("revision log not found")
}
return nil, err
}
return &log, nil
}
// FindByItem finds revision logs for an item
func (r *RevisionLogRepository) FindByItem(itemID uint, page, limit int) ([]models.RevisionLog, int64, error) {
var logs []models.RevisionLog
var total int64
query := r.db.Model(&models.RevisionLog{}).Where("item_id = ?", itemID)
// Count total
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Get paginated results
offset := (page - 1) * limit
err := query.Preload("User").Preload("User.Role").
Order("created_at DESC").
Offset(offset).Limit(limit).Find(&logs).Error
if err != nil {
return nil, 0, err
}
return logs, total, nil
}
// FindAll returns all revision logs with filters
func (r *RevisionLogRepository) FindAll(page, limit int, userID *uint) ([]models.RevisionLog, int64, error) {
var logs []models.RevisionLog
var total int64
query := r.db.Model(&models.RevisionLog{})
// Apply filters
if userID != nil {
query = query.Where("user_id = ?", *userID)
}
// Count total
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Get paginated results
offset := (page - 1) * limit
err := query.Preload("Item").Preload("User").Preload("User.Role").
Order("created_at DESC").
Offset(offset).Limit(limit).Find(&logs).Error
if err != nil {
return nil, 0, err
}
return logs, total, nil
}
// Log creates a new revision log entry (helper method)
func (r *RevisionLogRepository) Log(itemID, userID uint, fieldName, oldValue, newValue, reason string) error {
return models.CreateRevisionLog(r.db, itemID, userID, fieldName, oldValue, newValue, reason)
}
+90
View File
@@ -0,0 +1,90 @@
package repositories
import (
"errors"
"lost-and-found/internal/models"
"gorm.io/gorm"
)
type RoleRepository struct {
db *gorm.DB
}
func NewRoleRepository(db *gorm.DB) *RoleRepository {
return &RoleRepository{db: db}
}
// FindAllWithPermissions returns all roles with their permissions (for Role Management)
func (r *RoleRepository) FindAllWithPermissions() ([]models.Role, error) {
var roles []models.Role
err := r.db.Preload("Permissions").Find(&roles).Error
return roles, err
}
// FindAllPermissions gets list of all available permissions
func (r *RoleRepository) FindAllPermissions() ([]models.Permission, error) {
var permissions []models.Permission
err := r.db.Find(&permissions).Error
return permissions, err
}
// Create creates a new role
func (r *RoleRepository) Create(role *models.Role) error {
return r.db.Create(role).Error
}
// FindByID finds role by ID with permissions loaded
func (r *RoleRepository) FindByID(id uint) (*models.Role, error) {
var role models.Role
err := r.db.Preload("Permissions").First(&role, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("role not found")
}
return nil, err
}
return &role, nil
}
// ✅ [PERBAIKAN] Menambahkan kembali method FindByName yang hilang
// FindByName finds role by name (Required by AuthService)
func (r *RoleRepository) FindByName(name string) (*models.Role, error) {
var role models.Role
err := r.db.Where("name = ?", name).First(&role).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("role not found")
}
return nil, err
}
return &role, nil
}
// UpdatePermissions syncs permissions for a role (Update logic)
func (r *RoleRepository) UpdatePermissions(role *models.Role, permissionIDs []uint) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// 1. Update basic info (name/desc)
if err := tx.Save(role).Error; err != nil {
return err
}
// 2. Fetch permission objects based on IDs
var permissions []models.Permission
if len(permissionIDs) > 0 {
if err := tx.Where("id IN ?", permissionIDs).Find(&permissions).Error; err != nil {
return err
}
}
// 3. Replace associations (Hapus yang lama, set yang baru)
return tx.Model(role).Association("Permissions").Replace(permissions)
})
}
// Delete deletes a role
func (r *RoleRepository) Delete(id uint) error {
// Hapus relasi di role_permissions dulu (GORM biasanya handle ini via foreign key constraint, tapi untuk aman bisa manual)
// Kita gunakan Unscoped atau Select clause jika perlu, tapi standard delete object sudah cukup jika constraint DB benar.
return r.db.Select("Permissions").Delete(&models.Role{ID: id}).Error
}
+41
View File
@@ -0,0 +1,41 @@
package repositories
import (
"log"
"gorm.io/gorm"
)
// TransactionManager struct untuk handle manual transaction
type TransactionManager struct {
db *gorm.DB
}
func NewTransactionManager(db *gorm.DB) *TransactionManager {
return &TransactionManager{db: db}
}
// Begin memulai transaksi database
func (m *TransactionManager) Begin() *gorm.DB {
return m.db.Begin()
}
// Rollback membatalkan transaksi jika terjadi error
func (m *TransactionManager) Rollback(tx *gorm.DB) {
// Rollback hanya jika transaksi belum di-commit/rollback
if r := recover(); r != nil {
tx.Rollback()
log.Printf("⚠️ Transaction panicked: %v", r)
} else if tx.Error != nil {
// Jika tx sudah error, rollback
tx.Rollback()
} else {
// Rollback aman
tx.Rollback()
}
}
// Commit menyimpan perubahan permanen ke database
func (m *TransactionManager) Commit(tx *gorm.DB) error {
return tx.Commit().Error
}
+160
View File
@@ -0,0 +1,160 @@
// internal/repositories/user_repo.go - FIXED for ENCRYPTED NRP
package repositories
import (
"errors"
"lost-and-found/internal/models"
"gorm.io/gorm"
)
type UserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) Create(user *models.User) error {
return r.db.Create(user).Error
}
func (r *UserRepository) FindByID(id uint) (*models.User, error) {
var user models.User
// ✅ Tambahkan .Preload("Role.Permissions")
err := r.db.Preload("Role").Preload("Role.Permissions").First(&user, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("user not found")
}
return nil, err
}
return &user, nil
}
func (r *UserRepository) FindByEmail(email string) (*models.User, error) {
var user models.User
// ✅ UPDATE: Preload Role DAN Role.Permissions
err := r.db.
Preload("Role").
Preload("Role.Permissions"). // Memuat daftar hak akses
Where("email = ?", email).
First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("user not found")
}
return nil, err
}
return &user, nil
}
// ✅ FIXED: FindByNRP now accepts ENCRYPTED NRP
func (r *UserRepository) FindByNRP(nrp string) (*models.User, error) {
var user models.User
// ✅ UPDATE: Preload Role DAN Role.Permissions
err := r.db.
Preload("Role").
Preload("Role.Permissions"). // Memuat daftar hak akses
Where("nrp = ?", nrp).
First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil // Return nil jika tidak ditemukan (bukan error sistem)
}
return nil, err
}
return &user, nil
}
func (r *UserRepository) FindAll(page, limit int) ([]models.User, int64, error) {
var users []models.User
var total int64
if err := r.db.Model(&models.User{}).Count(&total).Error; err != nil {
return nil, 0, err
}
offset := (page - 1) * limit
err := r.db.
Preload("Role"). // Cukup Role saja untuk list view
Order("created_at DESC").
Offset(offset).Limit(limit).
Find(&users).Error
if err != nil {
return nil, 0, err
}
return users, total, nil
}
func (r *UserRepository) Update(user *models.User) error {
return r.db.Save(user).Error
}
func (r *UserRepository) UpdateRole(userID, roleID uint) error {
return r.db.Model(&models.User{}).Where("id = ?", userID).Update("role_id", roleID).Error
}
func (r *UserRepository) UpdateStatus(userID uint, status string) error {
return r.db.Model(&models.User{}).Where("id = ?", userID).Update("status", status).Error
}
func (r *UserRepository) Delete(id uint) error {
return r.db.Delete(&models.User{}, id).Error
}
func (r *UserRepository) BlockUser(id uint) error {
return r.UpdateStatus(id, "blocked")
}
func (r *UserRepository) UnblockUser(id uint) error {
return r.UpdateStatus(id, "active")
}
func (r *UserRepository) CountByRole(roleID uint) (int64, error) {
var count int64
err := r.db.Model(&models.User{}).Where("role_id = ?", roleID).Count(&count).Error
return count, err
}
func (r *UserRepository) GetUserStats(userID uint) (map[string]interface{}, error) {
var stats map[string]interface{} = make(map[string]interface{})
// Items reported
var itemCount int64
if err := r.db.Model(&models.Item{}).
Where("reporter_id = ?", userID).Count(&itemCount).Error; err != nil {
return nil, err
}
stats["items_reported"] = itemCount
// Lost items
var lostItemCount int64
if err := r.db.Model(&models.LostItem{}).
Where("user_id = ?", userID).Count(&lostItemCount).Error; err != nil {
return nil, err
}
stats["lost_items_reported"] = lostItemCount
// Claims
var claimCount int64
if err := r.db.Model(&models.Claim{}).
Where("user_id = ?", userID).Count(&claimCount).Error; err != nil {
return nil, err
}
stats["claims_made"] = claimCount
// Approved claims
var approvedClaimCount int64
if err := r.db.Model(&models.Claim{}).
Where("user_id = ? AND status = ?", userID, models.ClaimStatusApproved).
Count(&approvedClaimCount).Error; err != nil {
return nil, err
}
stats["claims_approved"] = approvedClaimCount
return stats, nil
}