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
+116
View File
@@ -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(),
}
}
+99
View File
@@ -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
}
+49
View File
@@ -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,
}
}
+50
View File
@@ -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,
}
}
+235
View File
@@ -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,
}
}
+78
View File
@@ -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,
}
}
+214
View File
@@ -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,
}
}
+111
View File
@@ -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,
}
}
+129
View File
@@ -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"`
}
+128
View File
@@ -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,
)
}
+12
View File
@@ -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"`
}
+73
View File
@@ -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
}
+56
View File
@@ -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
}
+147
View File
@@ -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
}
}