package controllers import ( "fmt" "net/http" "strconv" "time" "service-warungpos-go/config" "service-warungpos-go/models" "service-warungpos-go/services" "github.com/gin-gonic/gin" ) // DTOs & Custom Structs type CompanyResponse struct { ID uint `json:"id"` Name string `json:"name"` BankAccountNumber string `json:"bank_account_number"` BankName string `json:"bank_name"` Address string `json:"address"` Email string `json:"email"` Phone string `json:"phone"` LogoURL string `json:"logo_url"` IsActive bool `json:"is_active"` FeePercentage float64 `json:"fee_percentage"` CashFeeRules string `json:"cash_fee_rules"` QrisFeeRules string `json:"qris_fee_rules"` QrisFeePercentage float64 `json:"qris_fee_percentage"` QrisFeeFixed int `json:"qris_fee_fixed"` QrisFeeMin int `json:"qris_fee_min"` QrisMinTransaction int `json:"qris_min_transaction"` AllowQrisBelowMin bool `json:"allow_qris_below_min"` ChargeFeeBeforeDiscount bool `json:"charge_fee_before_discount"` WalletBalance float64 `json:"wallet_balance"` WalletLimit float64 `json:"wallet_limit"` EnableMember bool `json:"enable_member"` EnableVoucher bool `json:"enable_voucher"` EnableMultiBranch bool `json:"enable_multi_branch"` MaxBranches int `json:"max_branches"` EnableMultiKasir bool `json:"enable_multi_kasir"` MaxCashiers int `json:"max_cashiers"` EnforceSingleSession bool `json:"enforce_single_session"` ApprovedAt *time.Time `json:"approved_at"` OwnerVerified *bool `json:"owner_verified,omitempty"` OwnerName string `json:"owner_name,omitempty"` OwnerEmail string `json:"owner_email,omitempty"` OwnerPhone string `json:"owner_phone,omitempty"` PointRules []models.PointRule `json:"point_rules"` CreatedAt time.Time `json:"created_at"` } type CompanyCreateRequest struct { Name string `json:"name" binding:"required"` BankAccountNumber string `json:"bank_account_number"` BankName string `json:"bank_name"` Address string `json:"address"` Email string `json:"email"` Phone string `json:"phone"` LogoURL string `json:"logo_url"` FeePercentage float64 `json:"fee_percentage"` TripayMerchantCode string `json:"tripay_merchant_code"` TripayAPIKey string `json:"tripay_api_key"` TripayPrivateKey string `json:"tripay_private_key"` TripayProxyURL string `json:"tripay_proxy_url"` } type PointRuleInput struct { MinAmount int `json:"min_amount" binding:"required"` PointReward int `json:"point_reward" binding:"required"` } type UpdateCompanyPointsRequest struct { PointRules []PointRuleInput `json:"point_rules"` } type BranchCreateRequest struct { Name string `json:"name" binding:"required"` Address string `json:"address"` Phone string `json:"phone"` IsPointEnabled *bool `json:"is_point_enabled"` } type BranchResponse struct { ID uint `json:"id"` CompanyID uint `json:"company_id"` Name string `json:"name"` Address string `json:"address"` Phone string `json:"phone"` IsActive bool `json:"is_active"` CreatedAt time.Time `json:"created_at"` } type BranchProductResponse struct { ID uint `json:"id"` Name string `json:"name"` Price int `json:"price"` CategoryID *uint `json:"category_id"` ImageURL string `json:"image_url"` IsAvailable bool `json:"is_available"` } // ==================== COMPANY CONTROLLER ==================== func ListCompanies(c *gin.Context) { limitStr := c.Query("limit") skipStr := c.Query("skip") searchQuery := c.Query("search") query := config.DB.Model(&models.Company{}). Select("id, name, bank_account_number, bank_name, address, email, phone, logo_url, is_active, fee_percentage, cash_fee_rules, qris_fee_rules, qris_fee_percentage, qris_fee_fixed, qris_fee_min, qris_min_transaction, allow_qris_below_min, charge_fee_before_discount, wallet_balance, wallet_limit, enable_member, enable_voucher, enable_multi_branch, max_branches, enable_multi_kasir, max_cashiers, enforce_single_session, approved_at, created_at") if searchQuery != "" { query = query.Where("name ILIKE ? OR email ILIKE ? OR phone ILIKE ? OR address ILIKE ?", "%"+searchQuery+"%", "%"+searchQuery+"%", "%"+searchQuery+"%", "%"+searchQuery+"%") } if limitStr != "" { if limit, err := strconv.Atoi(limitStr); err == nil { query = query.Limit(limit) } } if skipStr != "" { if skip, err := strconv.Atoi(skipStr); err == nil { query = query.Offset(skip) } } var companies []models.Company if err := query.Preload("PointRules").Find(&companies).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mengambil data perusahaan"}) return } if len(companies) == 0 { c.JSON(http.StatusOK, []CompanyResponse{}) return } var companyIDs []uint for _, comp := range companies { companyIDs = append(companyIDs, comp.ID) } // Ambil semua owner user dalam 1 query (hanya kolom yang diperlukan) var owners []models.User config.DB.Select("id, name, email, phone, company_id"). Where("company_id IN ? AND role = ?", companyIDs, "owner"). Find(&owners) // Map company_id -> owner ownerMap := make(map[uint]models.User) var ownerPhones []string for _, o := range owners { if o.CompanyID != nil { ownerMap[*o.CompanyID] = o if o.Phone != "" { ownerPhones = append(ownerPhones, o.Phone) } } } // Ambil status verifikasi OTP semua owner dalam 1 query otpMap := make(map[string]bool) if len(ownerPhones) > 0 { var otps []models.OTP config.DB.Select("phone_number"). Where("phone_number IN ? AND is_verified = ?", ownerPhones, true). Find(&otps) for _, otp := range otps { otpMap[otp.PhoneNumber] = true } } resp := []CompanyResponse{} for _, comp := range companies { owner, hasOwner := ownerMap[comp.ID] var ownerVerified *bool var ownerName, ownerEmail, ownerPhone string if hasOwner { ownerName = owner.Name ownerEmail = owner.Email ownerPhone = owner.Phone if owner.Phone != "" { isVerified := otpMap[owner.Phone] ownerVerified = &isVerified } } resp = append(resp, CompanyResponse{ ID: comp.ID, Name: comp.Name, BankAccountNumber: comp.BankAccountNumber, BankName: comp.BankName, Address: comp.Address, Email: comp.Email, Phone: comp.Phone, LogoURL: comp.LogoURL, IsActive: comp.IsActive, FeePercentage: comp.FeePercentage, ApprovedAt: comp.ApprovedAt, OwnerVerified: ownerVerified, OwnerName: ownerName, OwnerEmail: ownerEmail, OwnerPhone: ownerPhone, PointRules: comp.PointRules, CashFeeRules: comp.CashFeeRules, QrisFeeRules: comp.QrisFeeRules, QrisFeePercentage: comp.QrisFeePercentage, QrisFeeFixed: comp.QrisFeeFixed, QrisFeeMin: comp.QrisFeeMin, QrisMinTransaction: comp.QrisMinTransaction, AllowQrisBelowMin: comp.AllowQrisBelowMin, ChargeFeeBeforeDiscount: comp.ChargeFeeBeforeDiscount, WalletBalance: comp.WalletBalance, WalletLimit: comp.WalletLimit, EnableMember: comp.EnableMember, EnableVoucher: comp.EnableVoucher, EnableMultiBranch: comp.EnableMultiBranch, MaxBranches: comp.MaxBranches, EnableMultiKasir: comp.EnableMultiKasir, MaxCashiers: comp.MaxCashiers, EnforceSingleSession: comp.EnforceSingleSession, CreatedAt: comp.CreatedAt, }) } c.JSON(http.StatusOK, resp) } func GetMyCompany(c *gin.Context) { userVal, _ := c.Get("user") user := userVal.(*models.User) if user.CompanyID == nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Tidak terhubung ke perusahaan"}) return } var company models.Company if err := config.DB.Preload("PointRules").First(&company, *user.CompanyID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Perusahaan tidak ditemukan"}) return } c.JSON(http.StatusOK, company) } func UpdateMyCompanyPoints(c *gin.Context) { userVal, exists := c.Get("user") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"detail": "Unauthorized"}) return } user := userVal.(*models.User) if user.CompanyID == nil || user.Role != "owner" { c.JSON(http.StatusForbidden, gin.H{"detail": "Hanya owner yang bisa mengatur poin"}) return } var req UpdateCompanyPointsRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": "Data tidak valid"}) return } // Hapus point rules yang lama config.DB.Where("company_id = ?", *user.CompanyID).Delete(&models.PointRule{}) // Masukkan point rules yang baru var newRules []models.PointRule for _, r := range req.PointRules { newRules = append(newRules, models.PointRule{ CompanyID: *user.CompanyID, MinAmount: r.MinAmount, PointReward: r.PointReward, }) } if len(newRules) > 0 { if err := config.DB.Create(&newRules).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menyimpan pengaturan poin"}) return } } var updatedCompany models.Company config.DB.Preload("PointRules").First(&updatedCompany, *user.CompanyID) c.JSON(http.StatusOK, gin.H{"message": "Pengaturan poin berhasil disimpan", "company": updatedCompany}) } func GetCompany(c *gin.Context) { companyIDStr := c.Param("company_id") companyID, err := strconv.Atoi(companyIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": "ID perusahaan tidak valid"}) return } var company models.Company if err := config.DB.First(&company, companyID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Company not found"}) return } c.JSON(http.StatusOK, company) } func CreateCompany(c *gin.Context) { var req CompanyCreateRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } var count int64 config.DB.Model(&models.Company{}).Where("name = ?", req.Name).Count(&count) if count > 0 { c.JSON(http.StatusBadRequest, gin.H{"detail": "Company name already exists"}) return } newComp := models.Company{ Name: req.Name, BankAccountNumber: req.BankAccountNumber, BankName: req.BankName, Address: req.Address, Email: req.Email, Phone: req.Phone, LogoURL: req.LogoURL, IsActive: true, FeePercentage: req.FeePercentage, TripayMerchantCode: req.TripayMerchantCode, TripayAPIKey: req.TripayAPIKey, TripayPrivateKey: req.TripayPrivateKey, TripayProxyURL: req.TripayProxyURL, } if err := config.DB.Create(&newComp).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal membuat perusahaan baru"}) return } c.JSON(http.StatusCreated, newComp) } func UpdateCompany(c *gin.Context) { companyIDStr := c.Param("company_id") companyID, err := strconv.Atoi(companyIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": "ID perusahaan tidak valid"}) return } var req CompanyCreateRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } var company models.Company if err := config.DB.First(&company, companyID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Company not found"}) return } if req.Name != "" { var count int64 config.DB.Model(&models.Company{}).Where("name = ? AND id != ?", req.Name, companyID).Count(&count) if count > 0 { c.JSON(http.StatusBadRequest, gin.H{"detail": "Company name already exists"}) return } company.Name = req.Name } company.BankAccountNumber = req.BankAccountNumber company.BankName = req.BankName company.Address = req.Address company.Email = req.Email company.Phone = req.Phone company.LogoURL = req.LogoURL company.FeePercentage = req.FeePercentage company.TripayMerchantCode = req.TripayMerchantCode company.TripayAPIKey = req.TripayAPIKey company.TripayPrivateKey = req.TripayPrivateKey company.TripayProxyURL = req.TripayProxyURL if err := config.DB.Save(&company).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal meng-update perusahaan"}) return } c.JSON(http.StatusOK, company) } func ToggleActiveCompany(c *gin.Context) { companyIDStr := c.Param("company_id") companyID, err := strconv.Atoi(companyIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": "ID perusahaan tidak valid"}) return } var company models.Company if err := config.DB.First(&company, companyID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Company not found"}) return } tx := config.DB.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() company.IsActive = !company.IsActive tx.Save(&company) // Ubah status keaktifan owner juga var owner models.User if err := tx.Where("company_id = ? AND role = ?", companyID, "owner").First(&owner).Error; err == nil { owner.IsActive = company.IsActive tx.Save(&owner) } if err := tx.Commit().Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal melakukan toggle status"}) return } status := "dinonaktifkan" if company.IsActive { status = "diaktifkan" } c.JSON(http.StatusOK, gin.H{ "message": fmt.Sprintf("Perusahaan %s berhasil %s", company.Name, status), "is_active": company.IsActive, }) } func DeleteCompany(c *gin.Context) { companyIDStr := c.Param("company_id") companyID, err := strconv.Atoi(companyIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": "ID perusahaan tidak valid"}) return } var company models.Company if err := config.DB.First(&company, companyID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Company not found"}) return } tx := config.DB.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() // 1. Ambil semua user terkait company ini, lalu hapus record OTP mereka var users []models.User tx.Where("company_id = ?", companyID).Find(&users) for _, u := range users { if u.Phone != "" { tx.Where("phone_number = ?", u.Phone).Delete(&models.OTP{}) } } // 2. Hapus order_items terlebih dahulu (menggunakan subquery) untuk menghindari FK violations if err := tx.Exec("DELETE FROM order_items WHERE order_id IN (SELECT id FROM orders WHERE company_id = ?)", companyID).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data order items"}) return } // 3. Hapus orders if err := tx.Where("company_id = ?", companyID).Delete(&models.Order{}).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data order"}) return } // 4. Hapus branch_products if err := tx.Exec("DELETE FROM branch_products WHERE product_id IN (SELECT id FROM products WHERE company_id = ?)", companyID).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data branch products"}) return } // 5. Hapus products if err := tx.Where("company_id = ?", companyID).Delete(&models.Product{}).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data produk"}) return } // 6. Hapus categories if err := tx.Where("company_id = ?", companyID).Delete(&models.Category{}).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data kategori"}) return } // 7. Hapus users if err := tx.Where("company_id = ?", companyID).Delete(&models.User{}).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data user"}) return } // 8. Hapus branches if err := tx.Where("company_id = ?", companyID).Delete(&models.Branch{}).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data cabang"}) return } // 9. Terakhir hapus company utama if err := tx.Delete(&company).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data perusahaan"}) return } tx.Commit() c.JSON(http.StatusOK, gin.H{ "message": fmt.Sprintf("Perusahaan '%s' dan seluruh data terkait berhasil dihapus", company.Name), }) } func RejectCompany(c *gin.Context) { companyIDStr := c.Param("company_id") companyID, err := strconv.Atoi(companyIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": "ID perusahaan tidak valid"}) return } var company models.Company if err := config.DB.First(&company, companyID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Company not found"}) return } if company.IsActive { c.JSON(http.StatusBadRequest, gin.H{"detail": "Tidak bisa menolak perusahaan yang sudah aktif"}) return } tx := config.DB.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() // 1. Ambil semua user terkait company ini, lalu hapus record OTP mereka var users []models.User tx.Where("company_id = ?", companyID).Find(&users) for _, u := range users { if u.Phone != "" { tx.Where("phone_number = ?", u.Phone).Delete(&models.OTP{}) } } // 2. Hapus order_items terlebih dahulu (menggunakan subquery) if err := tx.Exec("DELETE FROM order_items WHERE order_id IN (SELECT id FROM orders WHERE company_id = ?)", companyID).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data order items"}) return } // 3. Hapus orders if err := tx.Where("company_id = ?", companyID).Delete(&models.Order{}).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data order"}) return } // 4. Hapus branch_products if err := tx.Exec("DELETE FROM branch_products WHERE product_id IN (SELECT id FROM products WHERE company_id = ?)", companyID).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data branch products"}) return } // 5. Hapus products if err := tx.Where("company_id = ?", companyID).Delete(&models.Product{}).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data produk"}) return } // 6. Hapus categories if err := tx.Where("company_id = ?", companyID).Delete(&models.Category{}).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data kategori"}) return } // 7. Hapus users if err := tx.Where("company_id = ?", companyID).Delete(&models.User{}).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data user"}) return } // 8. Hapus branches if err := tx.Where("company_id = ?", companyID).Delete(&models.Branch{}).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data cabang"}) return } // 9. Terakhir hapus company utama if err := tx.Delete(&company).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus data perusahaan"}) return } tx.Commit() c.JSON(http.StatusOK, gin.H{ "message": fmt.Sprintf("Pendaftaran '%s' ditolak dan dihapus dari sistem", company.Name), }) } func ApproveCompany(c *gin.Context) { companyIDStr := c.Param("company_id") companyID, err := strconv.Atoi(companyIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": "ID perusahaan tidak valid"}) return } tx := config.DB.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }() var company models.Company if err := tx.First(&company, companyID).Error; err != nil { tx.Rollback() c.JSON(http.StatusNotFound, gin.H{"detail": "Company not found"}) return } var owner models.User if err := tx.Where("company_id = ? AND role = ?", companyID, "owner").First(&owner).Error; err != nil { tx.Rollback() c.JSON(http.StatusBadRequest, gin.H{"detail": "Company ini belum memiliki Owner"}) return } now := time.Now() company.IsActive = true company.ApprovedAt = &now tx.Save(&company) owner.IsActive = true tx.Save(&owner) if err := tx.Commit().Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menyetujui perusahaan"}) return } // Kirim Notifikasi WA (Async) go services.SendApprovalWa(owner.Phone, company.Name, owner.Email) c.JSON(http.StatusOK, gin.H{ "message": "Perusahaan dan owner berhasil disetujui", }) } // ==================== BRANCH CONTROLLER ==================== func ListBranches(c *gin.Context) { userVal, _ := c.Get("user") owner := userVal.(*models.User) branches := []models.Branch{} if err := config.DB.Select("id, company_id, name, address, phone, is_active, is_point_enabled, created_at").Where("company_id = ?", *owner.CompanyID).Find(&branches).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mengambil data cabang"}) return } c.JSON(http.StatusOK, branches) } func CreateBranch(c *gin.Context) { userVal, _ := c.Get("user") owner := userVal.(*models.User) var req BranchCreateRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } var count int64 config.DB.Model(&models.Branch{}).Where("company_id = ? AND name = ?", *owner.CompanyID, req.Name).Count(&count) if count > 0 { c.JSON(http.StatusBadRequest, gin.H{"detail": "Nama cabang sudah ada"}) return } var company models.Company if err := config.DB.First(&company, *owner.CompanyID).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mengambil data perusahaan"}) return } if !company.EnableMultiBranch && company.MaxBranches <= 0 { var branchCount int64 config.DB.Model(&models.Branch{}).Where("company_id = ?", *owner.CompanyID).Count(&branchCount) if branchCount >= 1 { c.JSON(http.StatusForbidden, gin.H{"detail": "Paket perusahaan Anda tidak mendukung pembuatan lebih dari 1 cabang"}) return } } else if company.MaxBranches > 0 { var branchCount int64 config.DB.Model(&models.Branch{}).Where("company_id = ?", *owner.CompanyID).Count(&branchCount) if branchCount >= int64(company.MaxBranches) { c.JSON(http.StatusForbidden, gin.H{"detail": fmt.Sprintf("Batas maksimal cabang (%d) telah tercapai untuk perusahaan Anda", company.MaxBranches)}) return } } isPointEnabled := true if req.IsPointEnabled != nil { isPointEnabled = *req.IsPointEnabled } newBranch := models.Branch{ CompanyID: *owner.CompanyID, Name: req.Name, Address: req.Address, Phone: req.Phone, IsActive: true, IsPointEnabled: isPointEnabled, } if err := config.DB.Create(&newBranch).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menyimpan cabang baru"}) return } c.JSON(http.StatusCreated, newBranch) } func UpdateBranch(c *gin.Context) { userVal, _ := c.Get("user") owner := userVal.(*models.User) branchIDStr := c.Param("branch_id") branchID, err := strconv.Atoi(branchIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": "ID cabang tidak valid"}) return } var req BranchCreateRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } var branch models.Branch if err := config.DB.Where("id = ? AND company_id = ?", branchID, *owner.CompanyID).First(&branch).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Cabang tidak ditemukan"}) return } if req.Name != "" { var count int64 config.DB.Model(&models.Branch{}).Where("company_id = ? AND name = ? AND id != ?", *owner.CompanyID, req.Name, branchID).Count(&count) if count > 0 { c.JSON(http.StatusBadRequest, gin.H{"detail": "Nama cabang sudah ada"}) return } branch.Name = req.Name } branch.Address = req.Address branch.Phone = req.Phone if req.IsPointEnabled != nil { branch.IsPointEnabled = *req.IsPointEnabled } if err := config.DB.Save(&branch).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal meng-update cabang"}) return } c.JSON(http.StatusOK, branch) } func DeleteBranch(c *gin.Context) { userVal, _ := c.Get("user") owner := userVal.(*models.User) branchIDStr := c.Param("branch_id") branchID, err := strconv.Atoi(branchIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": "ID cabang tidak valid"}) return } var branch models.Branch if err := config.DB.Where("id = ? AND company_id = ?", branchID, *owner.CompanyID).First(&branch).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Cabang tidak ditemukan"}) return } branch.IsActive = false config.DB.Save(&branch) c.JSON(http.StatusOK, gin.H{"message": "Cabang dinonaktifkan"}) } func ToggleBranchStatus(c *gin.Context) { userVal, _ := c.Get("user") owner := userVal.(*models.User) branchIDStr := c.Param("branch_id") branchID, err := strconv.Atoi(branchIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": "ID cabang tidak valid"}) return } var branch models.Branch if err := config.DB.Where("id = ? AND company_id = ?", branchID, *owner.CompanyID).First(&branch).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Cabang tidak ditemukan"}) return } branch.IsActive = !branch.IsActive config.DB.Save(&branch) status := "dinonaktifkan" if branch.IsActive { status = "diaktifkan" } c.JSON(http.StatusOK, gin.H{ "message": fmt.Sprintf("Cabang berhasil %s", status), "is_active": branch.IsActive, }) } func GetBranchProducts(c *gin.Context) { userVal, _ := c.Get("user") owner := userVal.(*models.User) branchIDStr := c.Param("branch_id") branchID, err := strconv.Atoi(branchIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": "ID cabang tidak valid"}) return } // Pastikan cabang milik perusahaan owner var branch models.Branch if err := config.DB.Where("id = ? AND company_id = ?", branchID, *owner.CompanyID).First(&branch).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Cabang tidak ditemukan"}) return } // Ambil semua produk aktif var products []models.Product if err := config.DB.Where("company_id = ? AND is_active = ?", *owner.CompanyID, true).Find(&products).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mengambil data produk"}) return } // Ambil relasi availability var branchProducts []models.BranchProduct config.DB.Where("branch_id = ?", branchID).Find(&branchProducts) bpMap := make(map[uint]bool) for _, bp := range branchProducts { bpMap[bp.ProductID] = bp.IsAvailable } resp := []BranchProductResponse{} for _, p := range products { isAvailable, exists := bpMap[p.ID] if !exists { isAvailable = true // Default tersedia } resp = append(resp, BranchProductResponse{ ID: p.ID, Name: p.Name, Price: p.Price, CategoryID: p.CategoryID, ImageURL: p.ImageURL, IsAvailable: isAvailable, }) } c.JSON(http.StatusOK, resp) } func ToggleProductAvailability(c *gin.Context) { userVal, _ := c.Get("user") owner := userVal.(*models.User) branchIDStr := c.Param("branch_id") branchID, err := strconv.Atoi(branchIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": "ID cabang tidak valid"}) return } productIDStr := c.Param("product_id") productID, err := strconv.Atoi(productIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": "ID produk tidak valid"}) return } var branch models.Branch if err := config.DB.Where("id = ? AND company_id = ?", branchID, *owner.CompanyID).First(&branch).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Cabang tidak ditemukan"}) return } var product models.Product if err := config.DB.Where("id = ? AND company_id = ?", productID, *owner.CompanyID).First(&product).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Produk tidak ditemukan"}) return } var bp models.BranchProduct err = config.DB.Where("branch_id = ? AND product_id = ?", branchID, productID).First(&bp).Error if err == nil { bp.IsAvailable = !bp.IsAvailable config.DB.Save(&bp) } else { // Belum pernah diset -> default True, jadi toggle = False bp = models.BranchProduct{ BranchID: uint(branchID), ProductID: uint(productID), IsAvailable: false, } config.DB.Create(&bp) } c.JSON(http.StatusOK, gin.H{ "message": "Status diperbarui", "is_available": bp.IsAvailable, }) } type UpdateCompanySettingsRequest struct { ChargeFeeBeforeDiscount *bool `json:"charge_fee_before_discount"` EnableMember *bool `json:"enable_member"` EnableVoucher *bool `json:"enable_voucher"` EnableMultiBranch *bool `json:"enable_multi_branch"` MaxBranches *int `json:"max_branches"` EnableMultiKasir *bool `json:"enable_multi_kasir"` MaxCashiers *int `json:"max_cashiers"` EnforceSingleSession *bool `json:"enforce_single_session"` } func AdminUpdateCompanySettings(c *gin.Context) { companyIDStr := c.Param("id") companyID, err := strconv.Atoi(companyIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": "ID Perusahaan tidak valid"}) return } var req UpdateCompanySettingsRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } var company models.Company if err := config.DB.First(&company, companyID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"detail": "Perusahaan tidak ditemukan"}) return } if req.ChargeFeeBeforeDiscount != nil { company.ChargeFeeBeforeDiscount = *req.ChargeFeeBeforeDiscount } if req.EnableMember != nil { company.EnableMember = *req.EnableMember } if req.EnableVoucher != nil { company.EnableVoucher = *req.EnableVoucher } if req.EnableMultiBranch != nil { company.EnableMultiBranch = *req.EnableMultiBranch } if req.EnableMultiKasir != nil { company.EnableMultiKasir = *req.EnableMultiKasir } if req.MaxBranches != nil { company.MaxBranches = *req.MaxBranches } if req.MaxCashiers != nil { company.MaxCashiers = *req.MaxCashiers } if req.EnforceSingleSession != nil { company.EnforceSingleSession = *req.EnforceSingleSession // Jika fitur dimatikan, hapus semua sesi aktif kasir perusahaan ini if !*req.EnforceSingleSession { var kasirIDs []uint config.DB.Model(&models.User{}).Where("company_id = ? AND role = ?", company.ID, "kasir").Pluck("id", &kasirIDs) if len(kasirIDs) > 0 { config.DB.Where("user_id IN ?", kasirIDs).Delete(&models.UserSession{}) } } } if err := config.DB.Save(&company).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menyimpan pengaturan"}) return } c.JSON(http.StatusOK, company) }