Dev / controllers /product_controller.go
Mhamdans17
fix: return empty array instead of null for list endpoints to prevent FE crashes
11c1b6f
Raw
History Blame Contribute Delete
14.6 kB
package controllers
import (
"fmt"
"net/http"
"strconv"
"strings"
"service-warungpos-go/config"
"service-warungpos-go/models"
"service-warungpos-go/services"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"math/rand"
)
// DTOs untuk Kategori & Produk
type CategoryCreateRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
}
type CategoryResponse struct {
ID uint `json:"id"`
CompanyID uint `json:"company_id"`
Name string `json:"name"`
Description string `json:"description"`
CreatedAt string `json:"created_at"`
}
type ProductCreateRequest struct {
Name string `json:"name" binding:"required"`
SKU *string `json:"sku"`
Price int `json:"price" binding:"required"`
Stock int `json:"stock"`
IsUnlimitedStock bool `json:"is_unlimited_stock"`
CategoryID *uint `json:"category_id"`
ImageURL string `json:"image_url"`
}
// ==================== IMAGE UPLOAD ====================
func UploadProductImage(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "File tidak ditemukan dalam request"})
return
}
// Validasi jenis file
allowedTypes := map[string]bool{
"image/jpeg": true,
"image/png": true,
"image/webp": true,
"image/gif": true,
}
contentType := file.Header.Get("Content-Type")
if !allowedTypes[contentType] {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Hanya file gambar (JPG, PNG, WebP, GIF) yang diizinkan"})
return
}
// Validasi ukuran (maksimal 5MB)
if file.Size > 5*1024*1024 {
c.JSON(http.StatusBadRequest, gin.H{"detail": "Ukuran gambar maksimal 5MB"})
return
}
// Upload langsung ke Cloudinary
secureURL, err := services.UploadToCloudinary(file)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": fmt.Sprintf("Gagal upload gambar: %v", err)})
return
}
c.JSON(http.StatusOK, gin.H{
"image_url": secureURL,
})
}
// ==================== CATEGORY CONTROLLER ====================
func ListCategories(c *gin.Context) {
userVal, _ := c.Get("user")
user := userVal.(*models.User)
var branchID *uint
// Periksa query param branch_id
if qBranchID := c.Query("branch_id"); qBranchID != "" {
if val, err := strconv.ParseUint(qBranchID, 10, 32); err == nil {
uVal := uint(val)
branchID = &uVal
}
}
// Jika kasir, paksa filter berdasarkan branch_id kasir
if user.Role == "kasir" && user.BranchID != nil {
branchID = user.BranchID
}
var allCategories []models.Category
if err := config.DB.Select("id, company_id, name, description, created_at").Where("company_id = ?", *user.CompanyID).Find(&allCategories).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mengambil data kategori"})
return
}
// Jika tidak ada filter branch_id, kembalikan semua kategori langsung
if branchID == nil {
c.JSON(http.StatusOK, allCategories)
return
}
// Ambil daftar product_id yang tidak tersedia di cabang ini
var unavailableProductIDs []uint
config.DB.Model(&models.BranchProduct{}).
Where("branch_id = ? AND is_available = ?", *branchID, false).
Pluck("product_id", &unavailableProductIDs)
// Cari kategori dari produk aktif yang tersedia di cabang ini
var availableCategoryIDs []uint
query := config.DB.Model(&models.Product{}).
Where("company_id = ? AND is_active = ?", *user.CompanyID, true)
if len(unavailableProductIDs) > 0 {
query = query.Where("id NOT IN ?", unavailableProductIDs)
}
query.Pluck("distinct category_id", &availableCategoryIDs)
// Filter allCategories agar hanya mengembalikan kategori yang memiliki produk tersedia di cabang
catMap := make(map[uint]bool)
for _, catID := range availableCategoryIDs {
catMap[catID] = true
}
var resp []models.Category
for _, cat := range allCategories {
if catMap[cat.ID] {
resp = append(resp, cat)
}
}
c.JSON(http.StatusOK, resp)
}
func GetCategory(c *gin.Context) {
userVal, _ := c.Get("user")
user := userVal.(*models.User)
catIDStr := c.Param("category_id")
catID, err := strconv.Atoi(catIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "ID kategori tidak valid"})
return
}
var category models.Category
if err := config.DB.Select("id, company_id, name, description, created_at").Where("id = ? AND company_id = ?", catID, *user.CompanyID).First(&category).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Kategori tidak ditemukan"})
return
}
c.JSON(http.StatusOK, category)
}
func CreateCategory(c *gin.Context) {
userVal, _ := c.Get("user")
user := userVal.(*models.User)
var req CategoryCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
// Cek duplikasi nama kategori di perusahaan yang sama
var count int64
config.DB.Model(&models.Category{}).Where("name = ? AND company_id = ?", req.Name, *user.CompanyID).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"detail": fmt.Sprintf("Kategori '%s' sudah ada", req.Name)})
return
}
newCat := models.Category{
CompanyID: *user.CompanyID,
Name: req.Name,
Description: req.Description,
}
if err := config.DB.Create(&newCat).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menyimpan kategori baru"})
return
}
c.JSON(http.StatusCreated, newCat)
}
func UpdateCategory(c *gin.Context) {
userVal, _ := c.Get("user")
user := userVal.(*models.User)
catIDStr := c.Param("category_id")
catID, err := strconv.Atoi(catIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "ID kategori tidak valid"})
return
}
var req CategoryCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
var category models.Category
if err := config.DB.Select("id, company_id, name, description").Where("id = ? AND company_id = ?", catID, *user.CompanyID).First(&category).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Kategori tidak ditemukan"})
return
}
if req.Name != "" {
var count int64
config.DB.Model(&models.Category{}).Where("name = ? AND company_id = ? AND id != ?", req.Name, *user.CompanyID, catID).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"detail": fmt.Sprintf("Kategori '%s' sudah ada", req.Name)})
return
}
category.Name = req.Name
}
category.Description = req.Description
if err := config.DB.Save(&category).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal meng-update kategori"})
return
}
c.JSON(http.StatusOK, category)
}
func DeleteCategory(c *gin.Context) {
userVal, _ := c.Get("user")
user := userVal.(*models.User)
catIDStr := c.Param("category_id")
catID, err := strconv.Atoi(catIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": "ID kategori tidak valid"})
return
}
var productCount int64
config.DB.Model(&models.Product{}).Where("category_id = ?", catID).Count(&productCount)
if productCount > 0 {
c.JSON(http.StatusBadRequest, gin.H{"detail": fmt.Sprintf("Kategori masih memiliki %d produk. Pindahkan produk dulu.", productCount)})
return
}
var category models.Category
if err := config.DB.Select("id, name").Where("id = ? AND company_id = ?", catID, *user.CompanyID).First(&category).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Kategori tidak ditemukan"})
return
}
if err := config.DB.Delete(&category).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus kategori"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Kategori '%s' berhasil dihapus", category.Name),
})
}
// ==================== PRODUCT CONTROLLER ====================
func ListProducts(c *gin.Context) {
userVal, _ := c.Get("user")
user := userVal.(*models.User)
var branchID *uint
// Cek filter branch_id query param (owner di POS)
if qBranchID := c.Query("branch_id"); qBranchID != "" {
if val, err := strconv.ParseUint(qBranchID, 10, 32); err == nil {
uVal := uint(val)
branchID = &uVal
}
}
// Kasir terikat branch
if user.Role == "kasir" && user.BranchID != nil {
branchID = user.BranchID
}
categoryFilter := c.Query("category_id")
activeOnlyStr := c.DefaultQuery("active_only", "true")
activeOnly := activeOnlyStr == "true"
searchQuery := c.Query("search")
limitStr := c.Query("limit")
skipStr := c.Query("skip")
query := config.DB.Select("id, company_id, category_id, name, sku, price, stock, is_unlimited_stock, image_url, is_active, created_at, updated_at").
Preload("Category", func(db *gorm.DB) *gorm.DB {
return db.Select("id, name")
}).
Where("company_id = ?", *user.CompanyID)
if activeOnly {
query = query.Where("is_active = ?", true)
}
if categoryFilter != "" {
if catID, err := strconv.Atoi(categoryFilter); err == nil {
query = query.Where("category_id = ?", catID)
}
}
if searchQuery != "" {
query = query.Where("LOWER(name) LIKE ? OR LOWER(sku) LIKE ?", "%"+strings.ToLower(searchQuery)+"%", "%"+strings.ToLower(searchQuery)+"%")
}
// Filter ketersediaan cabang (BranchProduct) di level database
if branchID != nil {
query = query.Where("id NOT IN (SELECT product_id FROM branch_products WHERE branch_id = ? AND is_available = ?)", *branchID, false)
}
// Terapkan pagination jika limit diisi
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)
}
}
products := []models.Product{}
if err := query.Find(&products).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal mengambil data produk"})
return
}
c.JSON(http.StatusOK, products)
}
func GetProduct(c *gin.Context) {
userVal, _ := c.Get("user")
user := userVal.(*models.User)
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 product models.Product
if err := config.DB.Select("id, company_id, category_id, name, sku, price, stock, is_unlimited_stock, image_url, is_active, created_at, updated_at").
Preload("Category", func(db *gorm.DB) *gorm.DB {
return db.Select("id, name")
}).
Where("id = ? AND company_id = ?", productID, *user.CompanyID).
First(&product).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Produk tidak ditemukan"})
return
}
c.JSON(http.StatusOK, product)
}
func CreateProduct(c *gin.Context) {
userVal, _ := c.Get("user")
user := userVal.(*models.User)
var req ProductCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
if req.SKU == nil || *req.SKU == "" {
generated := fmt.Sprintf("SKU-%04X%04X", rand.Intn(65536), rand.Intn(65536))
req.SKU = &generated
}
// Verifikasi SKU jika diisi
if req.SKU != nil && *req.SKU != "" {
var count int64
config.DB.Model(&models.Product{}).Where("sku = ?", *req.SKU).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"detail": fmt.Sprintf("Produk dengan SKU '%s' sudah ada", *req.SKU)})
return
}
}
newProduct := models.Product{
CompanyID: *user.CompanyID,
Name: req.Name,
SKU: req.SKU,
Price: req.Price,
Stock: req.Stock,
IsUnlimitedStock: req.IsUnlimitedStock,
CategoryID: req.CategoryID,
ImageURL: req.ImageURL,
IsActive: true,
}
if err := config.DB.Create(&newProduct).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menyimpan produk baru"})
return
}
if newProduct.CategoryID != nil {
config.DB.Select("id, name").First(&newProduct.Category, *newProduct.CategoryID)
}
c.JSON(http.StatusCreated, newProduct)
}
func UpdateProduct(c *gin.Context) {
userVal, _ := c.Get("user")
user := userVal.(*models.User)
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 req ProductCreateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()})
return
}
if req.SKU != nil && *req.SKU == "" {
req.SKU = nil
}
var product models.Product
if err := config.DB.Where("id = ? AND company_id = ?", productID, *user.CompanyID).First(&product).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"detail": "Produk tidak ditemukan"})
return
}
// Verifikasi SKU jika berubah
if req.SKU != nil && *req.SKU != "" {
if product.SKU == nil || *product.SKU != *req.SKU {
var count int64
config.DB.Model(&models.Product{}).Where("sku = ? AND id != ?", *req.SKU, productID).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, gin.H{"detail": fmt.Sprintf("Produk dengan SKU '%s' sudah ada", *req.SKU)})
return
}
product.SKU = req.SKU
}
} else {
product.SKU = nil
}
product.Name = req.Name
product.Price = req.Price
product.Stock = req.Stock
product.IsUnlimitedStock = req.IsUnlimitedStock
product.CategoryID = req.CategoryID
product.ImageURL = req.ImageURL
if err := config.DB.Save(&product).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal meng-update produk"})
return
}
if product.CategoryID != nil {
config.DB.Select("id, name").First(&product.Category, *product.CategoryID)
}
c.JSON(http.StatusOK, product)
}
func DeleteProduct(c *gin.Context) {
userVal, _ := c.Get("user")
user := userVal.(*models.User)
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
}
result := config.DB.Model(&models.Product{}).
Where("id = ? AND company_id = ?", productID, *user.CompanyID).
Update("is_active", false)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"detail": "Gagal menghapus produk"})
return
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"detail": "Produk tidak ditemukan"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Produk berhasil dinonaktifkan",
})
}