| 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" |
| ) |
|
|
| |
| 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"` |
| } |
|
|
| |
|
|
| 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 |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| if file.Size > 5*1024*1024 { |
| c.JSON(http.StatusBadRequest, gin.H{"detail": "Ukuran gambar maksimal 5MB"}) |
| return |
| } |
|
|
| |
| 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, |
| }) |
| } |
|
|
| |
|
|
| func ListCategories(c *gin.Context) { |
| userVal, _ := c.Get("user") |
| user := userVal.(*models.User) |
|
|
| var branchID *uint |
| |
| if qBranchID := c.Query("branch_id"); qBranchID != "" { |
| if val, err := strconv.ParseUint(qBranchID, 10, 32); err == nil { |
| uVal := uint(val) |
| branchID = &uVal |
| } |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| if branchID == nil { |
| c.JSON(http.StatusOK, allCategories) |
| return |
| } |
|
|
| |
| var unavailableProductIDs []uint |
| config.DB.Model(&models.BranchProduct{}). |
| Where("branch_id = ? AND is_available = ?", *branchID, false). |
| Pluck("product_id", &unavailableProductIDs) |
|
|
| |
| 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) |
|
|
| |
| 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 |
| } |
|
|
| |
| 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), |
| }) |
| } |
|
|
| |
|
|
| func ListProducts(c *gin.Context) { |
| userVal, _ := c.Get("user") |
| user := userVal.(*models.User) |
|
|
| var branchID *uint |
| |
| if qBranchID := c.Query("branch_id"); qBranchID != "" { |
| if val, err := strconv.ParseUint(qBranchID, 10, 32); err == nil { |
| uVal := uint(val) |
| branchID = &uVal |
| } |
| } |
|
|
| |
| 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)+"%") |
| } |
|
|
| |
| if branchID != nil { |
| query = query.Where("id NOT IN (SELECT product_id FROM branch_products WHERE branch_id = ? AND is_available = ?)", *branchID, false) |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| 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", |
| }) |
| } |
|
|