Initial commit - Lost and Found Revisi
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
// internal/models/archive.go
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Archive represents an archived item
|
||||
type Archive struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ItemID uint `gorm:"not null;uniqueIndex" json:"item_id"`
|
||||
Name string `gorm:"type:varchar(100);not null" json:"name"`
|
||||
CategoryID uint `gorm:"not null" json:"category_id"`
|
||||
Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
||||
PhotoURL string `gorm:"type:varchar(255)" json:"photo_url"`
|
||||
Location string `gorm:"type:varchar(200)" json:"location"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
DateFound time.Time `json:"date_found"`
|
||||
Status string `gorm:"type:varchar(50)" json:"status"`
|
||||
ReporterName string `gorm:"type:varchar(100)" json:"reporter_name"`
|
||||
ReporterContact string `gorm:"type:varchar(50)" json:"reporter_contact"`
|
||||
ArchivedReason string `gorm:"type:varchar(100)" json:"archived_reason"`
|
||||
ClaimedBy *uint `json:"claimed_by"`
|
||||
Claimer *User `gorm:"foreignKey:ClaimedBy" json:"claimer,omitempty"`
|
||||
|
||||
// ✅ NEW FIELDS
|
||||
BeritaAcaraNo string `gorm:"type:varchar(100)" json:"berita_acara_no"`
|
||||
BuktiSerahTerima string `gorm:"type:varchar(255)" json:"bukti_serah_terima"`
|
||||
|
||||
ArchivedAt time.Time `json:"archived_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// TableName specifies the table name for Archive model
|
||||
func (Archive) TableName() string {
|
||||
return "archives"
|
||||
}
|
||||
|
||||
// Archive reason constants
|
||||
const (
|
||||
ArchiveReasonExpired = "expired"
|
||||
ArchiveReasonCaseClosed = "case_closed"
|
||||
)
|
||||
|
||||
// BeforeCreate hook
|
||||
func (a *Archive) BeforeCreate(tx *gorm.DB) error {
|
||||
if a.ArchivedAt.IsZero() {
|
||||
a.ArchivedAt = time.Now()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ArchiveResponse represents archive data for API responses
|
||||
type ArchiveResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ItemID uint `json:"item_id"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
PhotoURL string `json:"photo_url"`
|
||||
Location string `json:"location"`
|
||||
DateFound time.Time `json:"date_found"`
|
||||
Status string `json:"status"`
|
||||
ArchivedReason string `json:"archived_reason"`
|
||||
ClaimedBy string `json:"claimed_by,omitempty"`
|
||||
ArchivedAt time.Time `json:"archived_at"`
|
||||
}
|
||||
|
||||
// ToResponse converts Archive to ArchiveResponse
|
||||
func (a *Archive) ToResponse() ArchiveResponse {
|
||||
categoryName := ""
|
||||
if a.Category.ID != 0 {
|
||||
categoryName = a.Category.Name
|
||||
}
|
||||
|
||||
claimedByName := ""
|
||||
if a.Claimer != nil && a.Claimer.ID != 0 {
|
||||
claimedByName = a.Claimer.Name
|
||||
}
|
||||
|
||||
return ArchiveResponse{
|
||||
ID: a.ID,
|
||||
ItemID: a.ItemID,
|
||||
Name: a.Name,
|
||||
Category: categoryName,
|
||||
PhotoURL: a.PhotoURL,
|
||||
Location: a.Location,
|
||||
DateFound: a.DateFound,
|
||||
Status: a.Status,
|
||||
ArchivedReason: a.ArchivedReason,
|
||||
ClaimedBy: claimedByName,
|
||||
ArchivedAt: a.ArchivedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateFromItem creates an Archive from an Item
|
||||
func CreateFromItem(item *Item, reason string, claimedBy *uint) *Archive {
|
||||
return &Archive{
|
||||
ItemID: item.ID,
|
||||
Name: item.Name,
|
||||
CategoryID: item.CategoryID,
|
||||
PhotoURL: item.PhotoURL,
|
||||
Location: item.Location,
|
||||
Description: item.Description,
|
||||
DateFound: item.DateFound,
|
||||
Status: item.Status,
|
||||
ReporterName: item.ReporterName,
|
||||
ReporterContact: item.ReporterContact,
|
||||
ArchivedReason: reason,
|
||||
ClaimedBy: claimedBy,
|
||||
ArchivedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// internal/models/audit_log.go
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AuditLog represents an audit log entry
|
||||
type AuditLog struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID *uint `json:"user_id"` // Nullable for system actions
|
||||
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Action string `gorm:"type:varchar(50);not null" json:"action"` // create, update, delete, verify, login, etc.
|
||||
EntityType string `gorm:"type:varchar(50)" json:"entity_type"` // item, claim, user, etc.
|
||||
EntityID *uint `json:"entity_id"`
|
||||
Details string `gorm:"type:text" json:"details"`
|
||||
IPAddress string `gorm:"type:varchar(50)" json:"ip_address"`
|
||||
UserAgent string `gorm:"type:varchar(255)" json:"user_agent"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// TableName specifies the table name for AuditLog model
|
||||
func (AuditLog) TableName() string {
|
||||
return "audit_logs"
|
||||
}
|
||||
|
||||
// Action constants
|
||||
const (
|
||||
ActionCreate = "create"
|
||||
ActionUpdate = "update"
|
||||
ActionDelete = "delete"
|
||||
ActionVerify = "verify"
|
||||
ActionLogin = "login"
|
||||
ActionLogout = "logout"
|
||||
ActionBlock = "block"
|
||||
ActionUnblock = "unblock"
|
||||
ActionApprove = "approve"
|
||||
ActionReject = "reject"
|
||||
ActionExport = "export"
|
||||
)
|
||||
|
||||
// Entity type constants
|
||||
const (
|
||||
EntityItem = "item"
|
||||
EntityLostItem = "lost_item"
|
||||
EntityClaim = "claim"
|
||||
EntityUser = "user"
|
||||
EntityCategory = "category"
|
||||
EntityArchive = "archive"
|
||||
)
|
||||
|
||||
// AuditLogResponse represents audit log data for API responses
|
||||
type AuditLogResponse struct {
|
||||
ID uint `json:"id"`
|
||||
UserName string `json:"user_name,omitempty"`
|
||||
Action string `json:"action"`
|
||||
EntityType string `json:"entity_type"`
|
||||
EntityID *uint `json:"entity_id"`
|
||||
Details string `json:"details"`
|
||||
IPAddress string `json:"ip_address"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ToResponse converts AuditLog to AuditLogResponse
|
||||
func (a *AuditLog) ToResponse() AuditLogResponse {
|
||||
userName := "System"
|
||||
if a.User != nil && a.User.ID != 0 {
|
||||
userName = a.User.Name
|
||||
}
|
||||
|
||||
return AuditLogResponse{
|
||||
ID: a.ID,
|
||||
UserName: userName,
|
||||
Action: a.Action,
|
||||
EntityType: a.EntityType,
|
||||
EntityID: a.EntityID,
|
||||
Details: a.Details,
|
||||
IPAddress: a.IPAddress,
|
||||
CreatedAt: a.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateAuditLog creates a new audit log entry
|
||||
func CreateAuditLog(db *gorm.DB, userID *uint, action, entityType string, entityID *uint, details, ipAddress, userAgent string) error {
|
||||
log := &AuditLog{
|
||||
UserID: userID,
|
||||
Action: action,
|
||||
EntityType: entityType,
|
||||
EntityID: entityID,
|
||||
Details: details,
|
||||
IPAddress: ipAddress,
|
||||
UserAgent: userAgent,
|
||||
}
|
||||
|
||||
return db.Create(log).Error
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// internal/models/category.go
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Category represents an item category
|
||||
type Category struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"type:varchar(100);not null" json:"name"`
|
||||
Slug string `gorm:"type:varchar(100);uniqueIndex;not null" json:"slug"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
// Relationships
|
||||
Items []Item `gorm:"foreignKey:CategoryID" json:"items,omitempty"`
|
||||
LostItems []LostItem `gorm:"foreignKey:CategoryID" json:"lost_items,omitempty"`
|
||||
}
|
||||
|
||||
// TableName specifies the table name for Category model
|
||||
func (Category) TableName() string {
|
||||
return "categories"
|
||||
}
|
||||
|
||||
// CategoryResponse represents category data for API responses
|
||||
type CategoryResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
ItemCount int64 `json:"item_count,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ToResponse converts Category to CategoryResponse
|
||||
func (c *Category) ToResponse() CategoryResponse {
|
||||
return CategoryResponse{
|
||||
ID: c.ID,
|
||||
Name: c.Name,
|
||||
Slug: c.Slug,
|
||||
Description: c.Description,
|
||||
CreatedAt: c.CreatedAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ChatMessage struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"not null" json:"user_id"`
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Message string `gorm:"type:text;not null" json:"message"`
|
||||
Response string `gorm:"type:text;not null" json:"response"`
|
||||
ContextData string `gorm:"type:json" json:"context_data"`
|
||||
Intent string `gorm:"type:varchar(50)" json:"intent"`
|
||||
ConfidenceScore float64 `gorm:"type:decimal(5,2);default:0.00" json:"confidence_score"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func (ChatMessage) TableName() string {
|
||||
return "chat_messages"
|
||||
}
|
||||
|
||||
// Intent types
|
||||
const (
|
||||
IntentSearchItem = "search_item"
|
||||
IntentReportLost = "report_lost"
|
||||
IntentClaimHelp = "claim_help"
|
||||
IntentGeneral = "general"
|
||||
IntentRecommendItem = "recommend_item"
|
||||
)
|
||||
|
||||
type ChatMessageResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Message string `json:"message"`
|
||||
Response string `json:"response"`
|
||||
Intent string `json:"intent"`
|
||||
ConfidenceScore float64 `json:"confidence_score"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func (c *ChatMessage) ToResponse() ChatMessageResponse {
|
||||
return ChatMessageResponse{
|
||||
ID: c.ID,
|
||||
Message: c.Message,
|
||||
Response: c.Response,
|
||||
Intent: c.Intent,
|
||||
ConfidenceScore: c.ConfidenceScore,
|
||||
CreatedAt: c.CreatedAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Claim represents a claim for a found item or a direct claim on a lost item
|
||||
type Claim struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
|
||||
// ✅ UBAH: Jadi Pointer (*uint) agar bisa NULL
|
||||
ItemID *uint `json:"item_id"`
|
||||
// ✅ UBAH: Jadi Pointer (*Item) untuk handling relasi opsional
|
||||
Item *Item `gorm:"foreignKey:ItemID" json:"item,omitempty"`
|
||||
|
||||
// ✅ BARU: Relasi ke LostItem (untuk Direct Claim)
|
||||
LostItemID *uint `json:"lost_item_id"`
|
||||
LostItem *LostItem `gorm:"foreignKey:LostItemID" json:"lost_item,omitempty"`
|
||||
|
||||
UserID uint `gorm:"not null" json:"user_id"` // Ini adalah Finder (Penemu)
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
|
||||
Description string `gorm:"type:text;not null" json:"description"`
|
||||
ProofURL string `gorm:"type:varchar(255)" json:"proof_url"`
|
||||
Contact string `gorm:"type:varchar(50);not null" json:"contact"`
|
||||
Status string `gorm:"type:varchar(50);default:'pending'" json:"status"`
|
||||
Notes string `gorm:"type:text" json:"notes"`
|
||||
VerifiedAt *time.Time `json:"verified_at"`
|
||||
VerifiedBy *uint `json:"verified_by"`
|
||||
Verifier *User `gorm:"foreignKey:VerifiedBy" json:"verifier,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
// Relationships
|
||||
Verification *ClaimVerification `gorm:"foreignKey:ClaimID" json:"verification,omitempty"`
|
||||
}
|
||||
|
||||
func (Claim) TableName() string {
|
||||
return "claims"
|
||||
}
|
||||
|
||||
// Claim status constants
|
||||
const (
|
||||
ClaimStatusPending = "pending"
|
||||
ClaimStatusApproved = "approved"
|
||||
ClaimStatusRejected = "rejected"
|
||||
ClaimStatusWaitingOwner = "waiting_owner"
|
||||
ClaimStatusVerified = "verified"
|
||||
)
|
||||
|
||||
func (c *Claim) BeforeCreate(tx *gorm.DB) error {
|
||||
if c.Status == "" {
|
||||
c.Status = ClaimStatusPending
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper methods status check
|
||||
func (c *Claim) IsPending() bool { return c.Status == ClaimStatusPending }
|
||||
func (c *Claim) IsApproved() bool { return c.Status == ClaimStatusApproved }
|
||||
func (c *Claim) IsRejected() bool { return c.Status == ClaimStatusRejected }
|
||||
func (c *Claim) IsWaitingOwner() bool { return c.Status == ClaimStatusWaitingOwner }
|
||||
func (c *Claim) IsVerified() bool { return c.Status == ClaimStatusVerified }
|
||||
|
||||
// Approve approves the claim
|
||||
func (c *Claim) Approve(verifierID uint, notes string) {
|
||||
c.Status = ClaimStatusApproved
|
||||
c.VerifiedBy = &verifierID
|
||||
now := time.Now()
|
||||
c.VerifiedAt = &now
|
||||
c.Notes = notes
|
||||
}
|
||||
|
||||
// Reject rejects the claim
|
||||
func (c *Claim) Reject(verifierID uint, notes string) {
|
||||
c.Status = ClaimStatusRejected
|
||||
c.VerifiedBy = &verifierID
|
||||
now := time.Now()
|
||||
c.VerifiedAt = &now
|
||||
c.Notes = notes
|
||||
}
|
||||
|
||||
// ClaimResponse represents claim data for API responses
|
||||
type ClaimResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ItemID uint `json:"item_id"` // 0 jika Direct Claim
|
||||
LostItemID uint `json:"lost_item_id"` // ✅ BARU: 0 jika Regular Claim
|
||||
LostItemUserID *uint `json:"lost_item_user_id"`
|
||||
ItemName string `json:"item_name"`
|
||||
UserID uint `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
Description string `json:"description"`
|
||||
ItemSecretDetails string `json:"item_secret_details"`
|
||||
ProofURL string `json:"proof_url"`
|
||||
Contact string `json:"contact"`
|
||||
Status string `json:"status"`
|
||||
Notes string `json:"notes"`
|
||||
MatchPercentage *float64 `json:"match_percentage,omitempty"`
|
||||
VerifiedAt *time.Time `json:"verified_at"`
|
||||
VerifiedBy *uint `json:"verified_by"`
|
||||
VerifierName string `json:"verifier_name,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
// Administrative Fields
|
||||
BeritaAcaraNo string `json:"berita_acara_no,omitempty"`
|
||||
BuktiSerahTerima string `json:"bukti_serah_terima,omitempty"`
|
||||
CaseClosedAt *time.Time `json:"case_closed_at,omitempty"`
|
||||
ReporterName string `json:"reporter_name"`
|
||||
CaseClosedByName string `json:"case_closed_by_name,omitempty"`
|
||||
Type string `json:"type"` // ✅ BARU: "regular" atau "direct"
|
||||
}
|
||||
|
||||
// ToResponse converts Claim to ClaimResponse with SAFETY CHECKS
|
||||
func (c *Claim) ToResponse() ClaimResponse {
|
||||
// 1. Initialize Default Values
|
||||
var itemID, lostItemID uint
|
||||
itemName := "Unknown Item"
|
||||
itemSecret := ""
|
||||
beritaAcara := ""
|
||||
buktiSerahTerima := ""
|
||||
var caseClosedAt *time.Time
|
||||
caseClosedByName := ""
|
||||
reporterName := ""
|
||||
claimType := "unknown"
|
||||
|
||||
// 2. LOGIC PENTING: Tentukan sumber data (Item vs LostItem)
|
||||
|
||||
// KASUS A: REGULAR CLAIM (Dari Barang Temuan)
|
||||
if c.Item != nil {
|
||||
claimType = "regular"
|
||||
itemID = c.Item.ID
|
||||
itemName = c.Item.Name
|
||||
itemSecret = c.Item.SecretDetails
|
||||
|
||||
// Admin details hanya ada di Item Temuan
|
||||
beritaAcara = c.Item.BeritaAcaraNo
|
||||
buktiSerahTerima = c.Item.BuktiSerahTerima
|
||||
caseClosedAt = c.Item.CaseClosedAt
|
||||
reporterName = c.Item.ReporterName
|
||||
|
||||
if c.Item.CaseClosedBy_User != nil {
|
||||
caseClosedByName = c.Item.CaseClosedBy_User.Name
|
||||
}
|
||||
} else if c.LostItem != nil {
|
||||
// KASUS B: DIRECT CLAIM (Dari Barang Hilang)
|
||||
claimType = "direct"
|
||||
lostItemID = c.LostItem.ID
|
||||
itemName = fmt.Sprintf("[DICARI] %s", c.LostItem.Name)
|
||||
itemSecret = c.LostItem.Description // Gunakan deskripsi lost item sebagai rahasia
|
||||
|
||||
// Untuk direct claim, ReporterName adalah si User (Finder) yang membuat claim ini
|
||||
if c.User.ID != 0 {
|
||||
reporterName = c.User.Name
|
||||
}
|
||||
}
|
||||
|
||||
// 3. User Info (Claimant)
|
||||
userName := ""
|
||||
if c.User.ID != 0 {
|
||||
userName = c.User.Name
|
||||
}
|
||||
|
||||
// 4. Verifier Info
|
||||
verifierName := ""
|
||||
if c.Verifier != nil {
|
||||
verifierName = c.Verifier.Name
|
||||
}
|
||||
|
||||
// 5. Match Info
|
||||
var matchPercentage *float64
|
||||
if c.Verification != nil {
|
||||
matchPercentage = &c.Verification.SimilarityScore
|
||||
}
|
||||
|
||||
return ClaimResponse{
|
||||
ID: c.ID,
|
||||
ItemID: itemID,
|
||||
LostItemID: lostItemID, // ✅ Field Baru
|
||||
ItemName: itemName,
|
||||
UserID: c.UserID,
|
||||
UserName: userName,
|
||||
Description: c.Description,
|
||||
ItemSecretDetails: itemSecret,
|
||||
ProofURL: c.ProofURL,
|
||||
Contact: c.Contact,
|
||||
Status: c.Status,
|
||||
Notes: c.Notes,
|
||||
MatchPercentage: matchPercentage,
|
||||
VerifiedAt: c.VerifiedAt,
|
||||
VerifiedBy: c.VerifiedBy,
|
||||
VerifierName: verifierName,
|
||||
CreatedAt: c.CreatedAt,
|
||||
|
||||
BeritaAcaraNo: beritaAcara,
|
||||
BuktiSerahTerima: buktiSerahTerima,
|
||||
CaseClosedAt: caseClosedAt,
|
||||
CaseClosedByName: caseClosedByName,
|
||||
ReporterName: reporterName,
|
||||
Type: claimType, // ✅ Field Baru
|
||||
}
|
||||
}
|
||||
|
||||
// ClaimDetailResponse includes item description for verification
|
||||
type ClaimDetailResponse struct {
|
||||
ClaimResponse
|
||||
ItemDescription string `json:"item_description"`
|
||||
ItemSecretDetails string `json:"item_secret_details"`
|
||||
}
|
||||
|
||||
// ToDetailResponse converts Claim to ClaimDetailResponse
|
||||
func (c *Claim) ToDetailResponse() ClaimDetailResponse {
|
||||
baseResponse := c.ToResponse()
|
||||
|
||||
itemDescription := ""
|
||||
itemSecretDetails := ""
|
||||
|
||||
// ✅ Logic Aman: Cek Item dulu, kalau nil cek LostItem
|
||||
if c.Item != nil {
|
||||
itemDescription = c.Item.Description
|
||||
itemSecretDetails = c.Item.SecretDetails
|
||||
} else if c.LostItem != nil {
|
||||
itemDescription = c.LostItem.Description
|
||||
itemSecretDetails = c.LostItem.Description // Fallback
|
||||
}
|
||||
|
||||
return ClaimDetailResponse{
|
||||
ClaimResponse: baseResponse,
|
||||
ItemDescription: itemDescription,
|
||||
ItemSecretDetails: itemSecretDetails,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// internal/models/claim_verification.go
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ClaimVerification represents verification data for a claim
|
||||
type ClaimVerification struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ClaimID uint `gorm:"not null;uniqueIndex" json:"claim_id"`
|
||||
Claim Claim `gorm:"foreignKey:ClaimID" json:"claim,omitempty"`
|
||||
SimilarityScore float64 `gorm:"type:decimal(5,2);default:0" json:"similarity_score"` // Percentage match (0-100)
|
||||
MatchedKeywords string `gorm:"type:text" json:"matched_keywords"` // Keywords that matched
|
||||
VerificationNotes string `gorm:"type:text" json:"verification_notes"` // Manager's notes
|
||||
IsAutoMatched bool `gorm:"default:false" json:"is_auto_matched"` // Was it auto-matched?
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// TableName specifies the table name for ClaimVerification model
|
||||
func (ClaimVerification) TableName() string {
|
||||
return "claim_verifications"
|
||||
}
|
||||
|
||||
// IsHighMatch checks if similarity score is high (>= 70%)
|
||||
func (cv *ClaimVerification) IsHighMatch() bool {
|
||||
return cv.SimilarityScore >= 70.0
|
||||
}
|
||||
|
||||
// IsMediumMatch checks if similarity score is medium (50-69%)
|
||||
func (cv *ClaimVerification) IsMediumMatch() bool {
|
||||
return cv.SimilarityScore >= 50.0 && cv.SimilarityScore < 70.0
|
||||
}
|
||||
|
||||
// IsLowMatch checks if similarity score is low (< 50%)
|
||||
func (cv *ClaimVerification) IsLowMatch() bool {
|
||||
return cv.SimilarityScore < 50.0
|
||||
}
|
||||
|
||||
// GetMatchLevel returns the match level as string
|
||||
func (cv *ClaimVerification) GetMatchLevel() string {
|
||||
if cv.IsHighMatch() {
|
||||
return "high"
|
||||
} else if cv.IsMediumMatch() {
|
||||
return "medium"
|
||||
}
|
||||
return "low"
|
||||
}
|
||||
|
||||
// ClaimVerificationResponse represents verification data for API responses
|
||||
type ClaimVerificationResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ClaimID uint `json:"claim_id"`
|
||||
SimilarityScore float64 `json:"similarity_score"`
|
||||
MatchLevel string `json:"match_level"`
|
||||
MatchedKeywords string `json:"matched_keywords"`
|
||||
VerificationNotes string `json:"verification_notes"`
|
||||
IsAutoMatched bool `json:"is_auto_matched"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ToResponse converts ClaimVerification to ClaimVerificationResponse
|
||||
func (cv *ClaimVerification) ToResponse() ClaimVerificationResponse {
|
||||
return ClaimVerificationResponse{
|
||||
ID: cv.ID,
|
||||
ClaimID: cv.ClaimID,
|
||||
SimilarityScore: cv.SimilarityScore,
|
||||
MatchLevel: cv.GetMatchLevel(),
|
||||
MatchedKeywords: cv.MatchedKeywords,
|
||||
VerificationNotes: cv.VerificationNotes,
|
||||
IsAutoMatched: cv.IsAutoMatched,
|
||||
CreatedAt: cv.CreatedAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
// internal/models/item.go - FIXED VERSION dengan SecretDetails
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Item represents a found item
|
||||
type Item struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"type:varchar(100);not null" json:"name"`
|
||||
CategoryID uint `gorm:"not null" json:"category_id"`
|
||||
Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
||||
PhotoURL string `gorm:"type:varchar(255)" json:"photo_url"`
|
||||
Location string `gorm:"type:varchar(200);not null" json:"location"`
|
||||
Description string `gorm:"type:text;not null" json:"description"`
|
||||
SecretDetails string `gorm:"type:text;column:secret_details" json:"secret_details,omitempty"`
|
||||
DateFound time.Time `gorm:"not null" json:"date_found"`
|
||||
Status string `gorm:"type:varchar(50);default:'unclaimed'" json:"status"`
|
||||
ReporterID uint `gorm:"not null" json:"reporter_id"`
|
||||
Reporter User `gorm:"foreignKey:ReporterID" json:"reporter,omitempty"`
|
||||
ReporterName string `gorm:"type:varchar(100);not null" json:"reporter_name"`
|
||||
ReporterContact string `gorm:"type:varchar(50);not null" json:"reporter_contact"`
|
||||
ExpiresAt *time.Time `json:"expires_at"`
|
||||
|
||||
// ✅ NEW FIELDS FOR CASE CLOSE
|
||||
BeritaAcaraNo string `gorm:"type:varchar(100)" json:"berita_acara_no"`
|
||||
BuktiSerahTerima string `gorm:"type:varchar(255)" json:"bukti_serah_terima"`
|
||||
CaseClosedAt *time.Time `json:"case_closed_at"`
|
||||
CaseClosedBy *uint `json:"case_closed_by"`
|
||||
CaseClosedBy_User *User `gorm:"foreignKey:CaseClosedBy" json:"case_closed_by_user,omitempty"`
|
||||
CaseClosedNotes string `gorm:"type:text" json:"case_closed_notes"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
// Relationships
|
||||
Claims []Claim `gorm:"foreignKey:ItemID" json:"claims,omitempty"`
|
||||
MatchResults []MatchResult `gorm:"foreignKey:ItemID" json:"match_results,omitempty"`
|
||||
RevisionLogs []RevisionLog `gorm:"foreignKey:ItemID" json:"revision_logs,omitempty"`
|
||||
}
|
||||
|
||||
// TableName specifies the table name for Item model
|
||||
func (Item) TableName() string {
|
||||
return "items"
|
||||
}
|
||||
|
||||
// Status constants
|
||||
const (
|
||||
ItemStatusUnclaimed = "unclaimed"
|
||||
ItemStatusPendingClaim = "pending_claim"
|
||||
ItemStatusVerified = "verified"
|
||||
ItemStatusCaseClosed = "case_closed"
|
||||
ItemStatusExpired = "expired"
|
||||
)
|
||||
|
||||
// BeforeCreate hook to set expiration date
|
||||
func (i *Item) BeforeCreate(tx *gorm.DB) error {
|
||||
if i.Status == "" {
|
||||
i.Status = ItemStatusUnclaimed
|
||||
}
|
||||
|
||||
if i.ExpiresAt == nil {
|
||||
expiresAt := i.DateFound.AddDate(0, 0, 90)
|
||||
i.ExpiresAt = &expiresAt
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsExpired checks if item has expired
|
||||
func (i *Item) IsExpired() bool {
|
||||
if i.ExpiresAt == nil {
|
||||
return false
|
||||
}
|
||||
return time.Now().After(*i.ExpiresAt)
|
||||
}
|
||||
|
||||
// CanBeClaimed checks if item can be claimed
|
||||
func (i *Item) CanBeClaimed() bool {
|
||||
return i.Status == ItemStatusUnclaimed && !i.IsExpired()
|
||||
}
|
||||
|
||||
// CanBeEdited checks if item can be edited
|
||||
func (i *Item) CanBeEdited() bool {
|
||||
return i.Status != ItemStatusCaseClosed && i.Status != ItemStatusExpired
|
||||
}
|
||||
|
||||
// ItemPublicResponse represents item data for public view (without sensitive info)
|
||||
type ItemPublicResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
PhotoURL string `json:"photo_url"`
|
||||
Location string `json:"location"`
|
||||
DateFound time.Time `json:"date_found"`
|
||||
Status string `json:"status"`
|
||||
ReporterID uint `json:"reporter_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func (i *Item) GetDisplayStatus() string {
|
||||
// Jika case closed atau expired, return as-is
|
||||
if i.Status == ItemStatusCaseClosed || i.Status == ItemStatusExpired {
|
||||
return i.Status
|
||||
}
|
||||
|
||||
// ✅ LOGIKA BARU: Check claims
|
||||
if len(i.Claims) > 0 {
|
||||
// Check apakah ada claim yang approved/completed
|
||||
for _, claim := range i.Claims {
|
||||
if claim.Status == ClaimStatusApproved || claim.Status == ClaimStatusVerified {
|
||||
return ItemStatusVerified // atau "completed"
|
||||
}
|
||||
if claim.Status == ClaimStatusPending || claim.Status == ClaimStatusWaitingOwner {
|
||||
return ItemStatusPendingClaim // "Sedang Diklaim"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default: return status dari database
|
||||
return i.Status
|
||||
}
|
||||
|
||||
// ToPublicResponse converts Item to ItemPublicResponse (hides description, reporter details)
|
||||
func (i *Item) ToPublicResponse() ItemPublicResponse {
|
||||
categoryName := ""
|
||||
if i.Category.ID != 0 {
|
||||
categoryName = i.Category.Name
|
||||
}
|
||||
|
||||
return ItemPublicResponse{
|
||||
ID: i.ID,
|
||||
Name: i.Name,
|
||||
Category: categoryName,
|
||||
PhotoURL: i.PhotoURL,
|
||||
Location: i.Location,
|
||||
DateFound: i.DateFound,
|
||||
Status: i.GetDisplayStatus(), // ✅ GANTI JADI GetDisplayStatus()
|
||||
ReporterID: i.ReporterID,
|
||||
CreatedAt: i.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
type ItemDetailResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
PhotoURL string `json:"photo_url"`
|
||||
Location string `json:"location"`
|
||||
Description string `json:"description"`
|
||||
SecretDetails string `json:"secret_details"`
|
||||
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"`
|
||||
|
||||
// ✅ NEW FIELDS
|
||||
BeritaAcaraNo string `json:"berita_acara_no,omitempty"`
|
||||
BuktiSerahTerima string `json:"bukti_serah_terima,omitempty"`
|
||||
CaseClosedAt *time.Time `json:"case_closed_at,omitempty"`
|
||||
CaseClosedByName string `json:"case_closed_by_name,omitempty"`
|
||||
CaseClosedNotes string `json:"case_closed_notes,omitempty"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ✅ FIXED ToDetailResponse - MAPPING LENGKAP
|
||||
func (i *Item) ToDetailResponse() ItemDetailResponse {
|
||||
categoryName := ""
|
||||
if i.Category.ID != 0 {
|
||||
categoryName = i.Category.Name
|
||||
}
|
||||
|
||||
reporterName := i.ReporterName
|
||||
if reporterName == "" && i.Reporter.ID != 0 {
|
||||
reporterName = i.Reporter.Name
|
||||
}
|
||||
|
||||
reporterContact := i.ReporterContact
|
||||
if reporterContact == "" && i.Reporter.ID != 0 {
|
||||
reporterContact = i.Reporter.Phone
|
||||
}
|
||||
|
||||
caseClosedByName := ""
|
||||
if i.CaseClosedBy_User != nil && i.CaseClosedBy_User.ID != 0 {
|
||||
caseClosedByName = i.CaseClosedBy_User.Name
|
||||
}
|
||||
|
||||
return ItemDetailResponse{
|
||||
ID: i.ID,
|
||||
Name: i.Name,
|
||||
Category: categoryName,
|
||||
PhotoURL: i.PhotoURL,
|
||||
Location: i.Location,
|
||||
Description: i.Description,
|
||||
SecretDetails: i.SecretDetails,
|
||||
DateFound: i.DateFound,
|
||||
Status: i.GetDisplayStatus(), // ✅ GANTI JADI GetDisplayStatus()
|
||||
ReporterName: reporterName,
|
||||
ReporterContact: reporterContact,
|
||||
ExpiresAt: i.ExpiresAt,
|
||||
BeritaAcaraNo: i.BeritaAcaraNo,
|
||||
BuktiSerahTerima: i.BuktiSerahTerima,
|
||||
CaseClosedAt: i.CaseClosedAt,
|
||||
CaseClosedByName: caseClosedByName,
|
||||
CaseClosedNotes: i.CaseClosedNotes,
|
||||
CreatedAt: i.CreatedAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type LostItem struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"not null" json:"user_id"`
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Name string `gorm:"type:varchar(100);not null" json:"name"`
|
||||
CategoryID uint `gorm:"not null" json:"category_id"`
|
||||
Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
||||
Color string `gorm:"type:varchar(50)" json:"color"`
|
||||
Location string `gorm:"type:varchar(200)" json:"location"`
|
||||
Description string `gorm:"type:text;not null" json:"description"`
|
||||
DateLost time.Time `gorm:"not null" json:"date_lost"`
|
||||
Status string `gorm:"type:varchar(50);default:'active'" json:"status"`
|
||||
MatchedAt *time.Time `json:"matched_at"`
|
||||
|
||||
// NEW: Direct claim fields
|
||||
DirectClaimID *uint `json:"direct_claim_id"`
|
||||
DirectClaim *Claim `gorm:"foreignKey:DirectClaimID" json:"direct_claim,omitempty"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
MatchResults []MatchResult `gorm:"foreignKey:LostItemID" json:"match_results,omitempty"`
|
||||
}
|
||||
|
||||
func (LostItem) TableName() string {
|
||||
return "lost_items"
|
||||
}
|
||||
|
||||
const (
|
||||
LostItemStatusActive = "active"
|
||||
LostItemStatusFound = "found"
|
||||
LostItemStatusExpired = "expired"
|
||||
LostItemStatusClosed = "closed"
|
||||
LostItemStatusClaimed = "claimed" // NEW: Status ketika ada yang klaim langsung ke owner
|
||||
LostItemStatusCompleted = "completed" // NEW: Status ketika owner confirm sudah terima barang
|
||||
)
|
||||
|
||||
func (l *LostItem) BeforeCreate(tx *gorm.DB) error {
|
||||
if l.Status == "" {
|
||||
l.Status = LostItemStatusActive
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *LostItem) IsActive() bool {
|
||||
return l.Status == LostItemStatusActive
|
||||
}
|
||||
|
||||
func (l *LostItem) IsClaimed() bool {
|
||||
return l.Status == LostItemStatusClaimed
|
||||
}
|
||||
|
||||
type LostItemResponse struct {
|
||||
ID uint `json:"id"`
|
||||
UserID uint `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
Name string `json:"name"`
|
||||
CategoryID uint `json:"category_id"` // ✅ TAMBAHKAN INI
|
||||
Category string `json:"category"`
|
||||
Color string `json:"color"`
|
||||
Location string `json:"location"`
|
||||
Description string `json:"description"`
|
||||
DateLost time.Time `json:"date_lost"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DirectClaimID *uint `json:"direct_claim_id,omitempty"`
|
||||
DirectClaimStatus string `json:"direct_claim_status,omitempty"`
|
||||
DirectClaim *ClaimResponse `json:"direct_claim,omitempty"`
|
||||
}
|
||||
|
||||
func (l *LostItem) ToResponse() LostItemResponse {
|
||||
userName := ""
|
||||
if l.User.ID != 0 {
|
||||
userName = l.User.Name
|
||||
}
|
||||
categoryName := ""
|
||||
if l.Category.ID != 0 {
|
||||
categoryName = l.Category.Name
|
||||
}
|
||||
directClaimStatus := ""
|
||||
var directClaimResp *ClaimResponse
|
||||
if l.DirectClaim != nil {
|
||||
directClaimStatus = l.DirectClaim.Status
|
||||
resp := l.DirectClaim.ToResponse()
|
||||
directClaimResp = &resp
|
||||
}
|
||||
return LostItemResponse{
|
||||
ID: l.ID,
|
||||
UserID: l.UserID,
|
||||
UserName: userName,
|
||||
Name: l.Name,
|
||||
CategoryID: l.CategoryID, // ✅ TAMBAHKAN INI
|
||||
Category: categoryName,
|
||||
Color: l.Color,
|
||||
Location: l.Location,
|
||||
Description: l.Description,
|
||||
DateLost: l.DateLost,
|
||||
Status: l.Status,
|
||||
CreatedAt: l.CreatedAt,
|
||||
DirectClaimID: l.DirectClaimID,
|
||||
DirectClaimStatus: directClaimStatus,
|
||||
DirectClaim: directClaimResp,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// internal/models/match_result.go
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// MatchResult represents auto-matching result between lost item and found item
|
||||
type MatchResult struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
LostItemID uint `gorm:"not null" json:"lost_item_id"`
|
||||
LostItem LostItem `gorm:"foreignKey:LostItemID" json:"lost_item,omitempty"`
|
||||
ItemID uint `gorm:"not null" json:"item_id"`
|
||||
Item Item `gorm:"foreignKey:ItemID" json:"item,omitempty"`
|
||||
SimilarityScore float64 `gorm:"type:decimal(5,2)" json:"similarity_score"` // Percentage match (0-100)
|
||||
MatchedFields string `gorm:"type:text" json:"matched_fields"` // JSON of matched fields
|
||||
MatchedAt time.Time `gorm:"not null" json:"matched_at"`
|
||||
IsNotified bool `gorm:"default:false" json:"is_notified"` // Was user notified?
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// TableName specifies the table name for MatchResult model
|
||||
func (MatchResult) TableName() string {
|
||||
return "match_results"
|
||||
}
|
||||
|
||||
// BeforeCreate hook
|
||||
func (mr *MatchResult) BeforeCreate(tx *gorm.DB) error {
|
||||
if mr.MatchedAt.IsZero() {
|
||||
mr.MatchedAt = time.Now()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsHighMatch checks if similarity score is high (>= 70%)
|
||||
func (mr *MatchResult) IsHighMatch() bool {
|
||||
return mr.SimilarityScore >= 70.0
|
||||
}
|
||||
|
||||
// IsMediumMatch checks if similarity score is medium (50-69%)
|
||||
func (mr *MatchResult) IsMediumMatch() bool {
|
||||
return mr.SimilarityScore >= 50.0 && mr.SimilarityScore < 70.0
|
||||
}
|
||||
|
||||
// IsLowMatch checks if similarity score is low (< 50%)
|
||||
func (mr *MatchResult) IsLowMatch() bool {
|
||||
return mr.SimilarityScore < 50.0
|
||||
}
|
||||
|
||||
// GetMatchLevel returns the match level as string
|
||||
func (mr *MatchResult) GetMatchLevel() string {
|
||||
if mr.IsHighMatch() {
|
||||
return "high"
|
||||
} else if mr.IsMediumMatch() {
|
||||
return "medium"
|
||||
}
|
||||
return "low"
|
||||
}
|
||||
|
||||
// MatchResultResponse represents match result data for API responses
|
||||
type MatchResultResponse struct {
|
||||
ID uint `json:"id"`
|
||||
LostItemID uint `json:"lost_item_id"`
|
||||
LostItemName string `json:"lost_item_name"`
|
||||
ItemID uint `json:"item_id"`
|
||||
ItemName string `json:"item_name"`
|
||||
ItemPhotoURL string `json:"item_photo_url"`
|
||||
ItemLocation string `json:"item_location"`
|
||||
ItemDateFound time.Time `json:"item_date_found"`
|
||||
ItemStatus string `json:"item_status"`
|
||||
SimilarityScore float64 `json:"similarity_score"`
|
||||
MatchLevel string `json:"match_level"`
|
||||
MatchedFields string `json:"matched_fields"`
|
||||
MatchedAt time.Time `json:"matched_at"`
|
||||
IsNotified bool `json:"is_notified"`
|
||||
}
|
||||
|
||||
// ToResponse converts MatchResult to MatchResultResponse
|
||||
func (mr *MatchResult) ToResponse() MatchResultResponse {
|
||||
lostItemName := ""
|
||||
if mr.LostItem.ID != 0 {
|
||||
lostItemName = mr.LostItem.Name
|
||||
}
|
||||
|
||||
itemName := ""
|
||||
itemPhotoURL := ""
|
||||
itemLocation := ""
|
||||
itemDateFound := time.Time{}
|
||||
itemStatus := ""
|
||||
if mr.Item.ID != 0 {
|
||||
itemName = mr.Item.Name
|
||||
itemPhotoURL = mr.Item.PhotoURL
|
||||
itemLocation = mr.Item.Location
|
||||
itemDateFound = mr.Item.DateFound
|
||||
itemStatus = mr.Item.Status
|
||||
}
|
||||
|
||||
return MatchResultResponse{
|
||||
ID: mr.ID,
|
||||
LostItemID: mr.LostItemID,
|
||||
LostItemName: lostItemName,
|
||||
ItemID: mr.ItemID,
|
||||
ItemName: itemName,
|
||||
ItemPhotoURL: itemPhotoURL,
|
||||
ItemLocation: itemLocation,
|
||||
ItemDateFound: itemDateFound,
|
||||
ItemStatus: itemStatus,
|
||||
SimilarityScore: mr.SimilarityScore,
|
||||
MatchLevel: mr.GetMatchLevel(),
|
||||
MatchedFields: mr.MatchedFields,
|
||||
MatchedAt: mr.MatchedAt,
|
||||
IsNotified: mr.IsNotified,
|
||||
}
|
||||
}
|
||||
|
||||
// ItemMatchResponse represents simplified item data for matching display
|
||||
type ItemMatchResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
PhotoURL string `json:"photo_url"`
|
||||
Location string `json:"location"`
|
||||
DateFound time.Time `json:"date_found"`
|
||||
Status string `json:"status"`
|
||||
Similarity float64 `json:"similarity"`
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// internal/models/notification.go
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Notification represents a notification for a user
|
||||
type Notification struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"not null" json:"user_id"`
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
Type string `gorm:"type:varchar(50);not null" json:"type"` // match_found, claim_approved, claim_rejected, item_expired, etc.
|
||||
Title string `gorm:"type:varchar(200);not null" json:"title"`
|
||||
Message string `gorm:"type:text;not null" json:"message"`
|
||||
EntityType string `gorm:"type:varchar(50)" json:"entity_type"` // item, claim, match, etc.
|
||||
EntityID *uint `json:"entity_id"`
|
||||
IsRead bool `gorm:"default:false" json:"is_read"`
|
||||
ReadAt *time.Time `json:"read_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// TableName specifies the table name for Notification model
|
||||
func (Notification) TableName() string {
|
||||
return "notifications"
|
||||
}
|
||||
|
||||
// Notification type constants
|
||||
const (
|
||||
NotificationMatchFound = "match_found"
|
||||
NotificationClaimApproved = "claim_approved"
|
||||
NotificationClaimRejected = "claim_rejected"
|
||||
NotificationItemExpired = "item_expired"
|
||||
NotificationNewClaim = "new_claim"
|
||||
NotificationItemReturned = "item_returned"
|
||||
)
|
||||
|
||||
// MarkAsRead marks the notification as read
|
||||
func (n *Notification) MarkAsRead() {
|
||||
n.IsRead = true
|
||||
now := time.Now()
|
||||
n.ReadAt = &now
|
||||
}
|
||||
|
||||
// NotificationResponse represents notification data for API responses
|
||||
type NotificationResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
EntityType string `json:"entity_type"`
|
||||
EntityID *uint `json:"entity_id"`
|
||||
IsRead bool `json:"is_read"`
|
||||
ReadAt *time.Time `json:"read_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ToResponse converts Notification to NotificationResponse
|
||||
func (n *Notification) ToResponse() NotificationResponse {
|
||||
return NotificationResponse{
|
||||
ID: n.ID,
|
||||
Type: n.Type,
|
||||
Title: n.Title,
|
||||
Message: n.Message,
|
||||
EntityType: n.EntityType,
|
||||
EntityID: n.EntityID,
|
||||
IsRead: n.IsRead,
|
||||
ReadAt: n.ReadAt,
|
||||
CreatedAt: n.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateNotification creates a new notification
|
||||
func CreateNotification(db *gorm.DB, userID uint, notifType, title, message, entityType string, entityID *uint) error {
|
||||
notification := &Notification{
|
||||
UserID: userID,
|
||||
Type: notifType,
|
||||
Title: title,
|
||||
Message: message,
|
||||
EntityType: entityType,
|
||||
EntityID: entityID,
|
||||
}
|
||||
|
||||
return db.Create(notification).Error
|
||||
}
|
||||
|
||||
// CreateMatchNotification creates a notification for match found
|
||||
func CreateMatchNotification(db *gorm.DB, userID uint, itemName string, matchID uint) error {
|
||||
return CreateNotification(
|
||||
db,
|
||||
userID,
|
||||
NotificationMatchFound,
|
||||
"Barang yang Mirip Ditemukan!",
|
||||
"Kami menemukan barang yang mirip dengan laporan kehilangan Anda: "+itemName,
|
||||
"match",
|
||||
&matchID,
|
||||
)
|
||||
}
|
||||
|
||||
// CreateClaimApprovedNotification creates a notification for approved claim
|
||||
func CreateClaimApprovedNotification(db *gorm.DB, userID uint, itemName string, claimID uint) error {
|
||||
return CreateNotification(
|
||||
db,
|
||||
userID,
|
||||
NotificationClaimApproved,
|
||||
"Klaim Disetujui!",
|
||||
"Klaim Anda untuk barang '"+itemName+"' telah disetujui. Silakan ambil barang di tempat yang ditentukan.",
|
||||
"claim",
|
||||
&claimID,
|
||||
)
|
||||
}
|
||||
|
||||
// CreateClaimRejectedNotification creates a notification for rejected claim
|
||||
func CreateClaimRejectedNotification(db *gorm.DB, userID uint, itemName, reason string, claimID uint) error {
|
||||
return CreateNotification(
|
||||
db,
|
||||
userID,
|
||||
NotificationClaimRejected,
|
||||
"Klaim Ditolak",
|
||||
"Klaim Anda untuk barang '"+itemName+"' ditolak. Alasan: "+reason,
|
||||
"claim",
|
||||
&claimID,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Permission struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Slug string `gorm:"unique;not null" json:"slug"`
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Description string `json:"description"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// internal/models/revision_log.go
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// RevisionLog represents revision history for item edits
|
||||
type RevisionLog struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
ItemID uint `gorm:"not null" json:"item_id"`
|
||||
Item Item `gorm:"foreignKey:ItemID" json:"item,omitempty"`
|
||||
UserID uint `gorm:"not null" json:"user_id"` // Who made the edit
|
||||
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
FieldName string `gorm:"type:varchar(50);not null" json:"field_name"` // Which field was edited
|
||||
OldValue string `gorm:"type:text" json:"old_value"`
|
||||
NewValue string `gorm:"type:text" json:"new_value"`
|
||||
Reason string `gorm:"type:text" json:"reason"` // Why was it edited
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// TableName specifies the table name for RevisionLog model
|
||||
func (RevisionLog) TableName() string {
|
||||
return "revision_logs"
|
||||
}
|
||||
|
||||
// RevisionLogResponse represents revision log data for API responses
|
||||
type RevisionLogResponse struct {
|
||||
ID uint `json:"id"`
|
||||
ItemID uint `json:"item_id"`
|
||||
UserName string `json:"user_name"`
|
||||
FieldName string `json:"field_name"`
|
||||
OldValue string `json:"old_value"`
|
||||
NewValue string `json:"new_value"`
|
||||
Reason string `json:"reason"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ToResponse converts RevisionLog to RevisionLogResponse
|
||||
func (rl *RevisionLog) ToResponse() RevisionLogResponse {
|
||||
userName := ""
|
||||
if rl.User.ID != 0 {
|
||||
userName = rl.User.Name
|
||||
}
|
||||
|
||||
return RevisionLogResponse{
|
||||
ID: rl.ID,
|
||||
ItemID: rl.ItemID,
|
||||
UserName: userName,
|
||||
FieldName: rl.FieldName,
|
||||
OldValue: rl.OldValue,
|
||||
NewValue: rl.NewValue,
|
||||
Reason: rl.Reason,
|
||||
CreatedAt: rl.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateRevisionLog creates a new revision log entry
|
||||
func CreateRevisionLog(db *gorm.DB, itemID, userID uint, fieldName, oldValue, newValue, reason string) error {
|
||||
log := &RevisionLog{
|
||||
ItemID: itemID,
|
||||
UserID: userID,
|
||||
FieldName: fieldName,
|
||||
OldValue: oldValue,
|
||||
NewValue: newValue,
|
||||
Reason: reason,
|
||||
}
|
||||
|
||||
return db.Create(log).Error
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// internal/models/role.go
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Role represents a user role in the system
|
||||
type Role struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"type:varchar(50);uniqueIndex;not null" json:"name"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
|
||||
// ✅ Tambahkan relasi Permissions
|
||||
Permissions []Permission `gorm:"many2many:role_permissions;" json:"permissions,omitempty"`
|
||||
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Users []User `gorm:"foreignKey:RoleID" json:"users,omitempty"`
|
||||
}
|
||||
|
||||
// TableName specifies the table name for Role model
|
||||
func (Role) TableName() string {
|
||||
return "roles"
|
||||
}
|
||||
|
||||
// Role constants
|
||||
const (
|
||||
RoleAdmin = "admin"
|
||||
RoleManager = "manager"
|
||||
RoleUser = "user"
|
||||
)
|
||||
|
||||
// GetRoleID returns the ID for a given role name
|
||||
func GetRoleID(db *gorm.DB, roleName string) (uint, error) {
|
||||
var role Role
|
||||
if err := db.Where("name = ?", roleName).First(&role).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return role.ID, nil
|
||||
}
|
||||
|
||||
// IsValidRole checks if a role name is valid
|
||||
func IsValidRole(roleName string) bool {
|
||||
validRoles := []string{RoleAdmin, RoleManager, RoleUser}
|
||||
for _, r := range validRoles {
|
||||
if r == roleName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
// internal/models/user.go - FIXED VERSION
|
||||
package models
|
||||
|
||||
import (
|
||||
"lost-and-found/internal/utils"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// User represents a user in the system
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"type:varchar(100);not null" json:"name"`
|
||||
Email string `gorm:"type:varchar(100);uniqueIndex;not null" json:"email"`
|
||||
Password string `gorm:"type:varchar(255);not null" json:"-"`
|
||||
NRP string `gorm:"type:varchar(20)" json:"nrp"` // ✅ Plain text
|
||||
Phone string `gorm:"type:varchar(20)" json:"phone"` // ✅ Plain text
|
||||
RoleID uint `gorm:"not null;default:3" json:"role_id"`
|
||||
Role Role `gorm:"foreignKey:RoleID" json:"role,omitempty"`
|
||||
Status string `gorm:"type:varchar(20);default:'active'" json:"status"`
|
||||
LastLogin *time.Time `json:"last_login"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// TableName specifies the table name
|
||||
func (User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
// User status constants
|
||||
const (
|
||||
UserStatusActive = "active"
|
||||
UserStatusBlocked = "blocked"
|
||||
)
|
||||
|
||||
// IsActive checks if user is active
|
||||
func (u *User) IsActive() bool {
|
||||
return u.Status == UserStatusActive
|
||||
}
|
||||
|
||||
// IsBlocked checks if user is blocked
|
||||
func (u *User) IsBlocked() bool {
|
||||
return u.Status == UserStatusBlocked
|
||||
}
|
||||
|
||||
// IsAdmin checks if user is admin
|
||||
func (u *User) IsAdmin() bool {
|
||||
return u.Role.Name == "admin"
|
||||
}
|
||||
|
||||
// IsManager checks if user is manager
|
||||
func (u *User) IsManager() bool {
|
||||
return u.Role.Name == "manager"
|
||||
}
|
||||
|
||||
func (u *User) HasPermission(permissionSlug string) bool {
|
||||
// Jika Role atau Permissions belum di-load, return false (fail safe)
|
||||
if u.Role.ID == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Admin (Role ID 1) biasanya bypass semua check, tapi sebaiknya tetap cek list
|
||||
// untuk konsistensi database.
|
||||
|
||||
for _, perm := range u.Role.Permissions {
|
||||
if perm.Slug == permissionSlug {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsUser checks if user is regular user
|
||||
func (u *User) IsUser() bool {
|
||||
return u.Role.Name == "user"
|
||||
}
|
||||
|
||||
// UserResponse represents user data for API responses
|
||||
type UserResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
NRP string `json:"nrp,omitempty"` // ✅ Langsung dari database
|
||||
Phone string `json:"phone,omitempty"` // ✅ Langsung dari database
|
||||
Role string `json:"role"`
|
||||
Status string `json:"status"`
|
||||
LastLogin *time.Time `json:"last_login,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ✅ ToResponse converts User to UserResponse dengan DEKRIPSI
|
||||
func (u *User) ToResponse() UserResponse {
|
||||
response := UserResponse{
|
||||
ID: u.ID,
|
||||
Name: u.Name,
|
||||
Email: u.Email,
|
||||
NRP: u.NRP, // ✅ Langsung assign
|
||||
Phone: u.Phone, // ✅ Langsung assign
|
||||
Status: u.Status,
|
||||
LastLogin: u.LastLogin,
|
||||
CreatedAt: u.CreatedAt,
|
||||
}
|
||||
|
||||
// Set role name
|
||||
if u.Role.ID != 0 {
|
||||
response.Role = u.Role.Name
|
||||
}
|
||||
|
||||
// ✅ DEKRIPSI NRP
|
||||
if u.NRP != "" {
|
||||
decryptedNRP, err := utils.DecryptString(u.NRP)
|
||||
if err == nil {
|
||||
response.NRP = decryptedNRP
|
||||
} else {
|
||||
// Jika dekripsi gagal, kembalikan nilai asli (untuk backward compatibility)
|
||||
response.NRP = u.NRP
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ DEKRIPSI Phone
|
||||
if u.Phone != "" {
|
||||
decryptedPhone, err := utils.DecryptString(u.Phone)
|
||||
if err == nil {
|
||||
response.Phone = decryptedPhone
|
||||
} else {
|
||||
// Jika dekripsi gagal, kembalikan nilai asli (untuk backward compatibility)
|
||||
response.Phone = u.Phone
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
// ✅ ToPublicResponse - untuk public access (hide sensitive data)
|
||||
func (u *User) ToPublicResponse() UserResponse {
|
||||
return UserResponse{
|
||||
ID: u.ID,
|
||||
Name: u.Name,
|
||||
Role: u.Role.Name,
|
||||
Status: u.Status,
|
||||
CreatedAt: u.CreatedAt,
|
||||
// NRP & Phone tidak disertakan untuk security
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user