package main import ( "database/sql" "log" "net/http" "strconv" "time" _ "time/tzdata" "crypto/aes" "crypto/cipher" // "crypto/rand" "encoding/base64" "bytes" // For byte manipulation (e.g., PKCS7 padding) "fmt" // For formatted I/O (e.g., error messages) "context" // "os" // "os/signal" // "syscall" "errors" "math/rand" "net/url" "os" "path" "path/filepath" "strings" "io" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/dgrijalva/jwt-go" _ "github.com/go-sql-driver/mysql" // "github.com/skip2/go-qrcode" // Import the QR code library "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/store/sqlstore" "go.mau.fi/whatsmeow/types" // "go.mau.fi/whatsmeow/types/events" _ "github.com/mattn/go-sqlite3" // Im the SQLite driver "github.com/skip2/go-qrcode" "sync" "go.mau.fi/whatsmeow/types/events" waLog "go.mau.fi/whatsmeow/util/log" "regexp" waE2E "go.mau.fi/whatsmeow/binary/proto" ) var ( whatsappClient *whatsmeow.Client whatsappContainer *sqlstore.Container clientMutex sync.RWMutex ) // WhatsApp-specific types type WhatsAppAPI struct { client *whatsmeow.Client } type LoginResponse struct { Status string `json:"status"` QR string `json:"qr,omitempty"` Error string `json:"error,omitempty"` } type SendRequest struct { To string `json:"to" binding:"required"` Message string `json:"message" binding:"required"` } type SendResponse struct { Status string `json:"status"` Message string `json:"message,omitempty"` Error string `json:"error,omitempty"` } type StatusResponse struct { Connected bool `json:"connected"` LoggedIn bool `json:"logged_in"` } type Product struct { KodeBrg string `json:"kodeBrg"` NamaBrg string `json:"namaBrg"` Harga *string `json:"harga"` Berat *string `json:"berat"` Limit *string `json:"limit"` HargaPromo *string `json:"hargaPromo"` ImageURL *string `json:"imageUrl"` ImageKitURL sql.NullString `json:"imageKitUrl"` KodeGolongan *string `json:"kodeGolongan"` Kategori *string `json:"kategori"` KodePromo *string `json:"kodePromo"` Deskripsi *string `json:"deskripsi"` Stock *string `json:"stock"` Prioritas *string `json:"prioritas"` BonusItem *string `json:"bonusItem"` JenisPotongan *string `json:"jenisPotongan"` JumlahPotongan *int `json:"jumlahPotongan"` MinQtyGrosir *int `json:"minQtyGrosir"` } type Customer struct { Username string `json:"username"` Password string `json:"password"` Email string `json:"email"` TipeUser string `json:"tipeUser"` Name string `json:"name"` Address string `json:"address"` Regency string `json:"regency"` District string `json:"district"` Village string `json:"village"` Phone string `json:"phone"` LastLogin string `json:"lastLogin"` } type Order struct { KodeOrder string `json:"kode_order,omitempty"` // Optional; generated later Tanggal string `json:"tanggal,omitempty"` // Optional; set later NamaCabang string `json:"nama_cabang"` Username string `json:"username"` NamaCust string `json:"nama_cust"` AlamatCust string `json:"alamat_cust"` Lat string `json:"lat"` Long string `json:"long"` Nilai float64 `json:"nilai"` Discount *float64 `json:"discount"` StatusOrder string `json:"status_order,omitempty"` // Optional; default to "proses" BatalOleh *string `json:"batalOleh,omitempty"` KetBatal *string `json:"ketBatal,omitempty"` Ongkir float64 `json:"ongkir"` Versi string `json:"versi"` CaraBayar *string `json:"cara_bayar"` KodeCabang string `json:"kode_cabang"` KodeVoucher string `json:"kode_voucher"` Items []OrderItem `json:"items"` } type OrderItem struct { KodeOrder string `json:"kode_order,omitempty"` // Optional; generated later KodeBarang string `json:"kode_barang"` NamaBarang string `json:"nama_barang"` ImageURL string `json:"image_url"` Jumlah int `json:"jumlah"` Harga float64 `json:"harga,string"` HargaPromo float64 `json:"harga_promo,string"` IsFree string `json:"is_free"` Berat float64 `json:"berat,string"` HargaJual float64 `json:"harga_jual,string"` Promo string `json:"promo"` } type Banner struct { ImageURL string `json:"url"` } type Info struct { ID int `json:"id_info"` NamaInfo string `json:"nama_info"` KodeInfo string `json:"kode_info"` Keterangan string `json:"keterangan"` Mode InfoMode `json:"mode"` } type InfoMode struct { Type string `json:"type"` Payload string `json:"payload"` } type Voucher struct { KodeVoucher string `json:"kodeVoucher"` StartDate string `json:"startDate"` EndDate string `json:"endDate"` TargetVoucher string `json:"targetVoucher"` IdTarget string `json:"idTarget"` JenisPotongan string `json:"jenisPotongan"` JumlahPotongan int `json:"jumlahPotongan"` IsDoubleDisc *int `json:"isDoubleDisc"` } type Cabang struct { NamaCabang *string `json:"nama_cabang"` KodeCabang *string `json:"kode_cabang"` Lat *string `json:"lat"` Long *string `json:"long"` Keterangan *string `json:"keterangan"` Persen float64 `json:"persen"` Aktif *string `json:"aktif"` } type Setup struct { Group string `json:"group"` Subgroup string `json:"subgroup"` Nilai int `json:"nilai"` } type Kabupaten struct { Id int `json:"id"` ProvinceId int `json:"province_id"` Nama string `json:"name"` } type Kecamatan struct { Id int `json:"id"` RegencyId int `json:"regency_id"` Nama string `json:"name"` } type Desa struct { Id int `json:"id"` DistrictId int `json:"district_id"` Nama string `json:"name"` } var db *sql.DB var jwtSecret = []byte("your_secret_key") // Key for AES-256 encryption (must be 32 bytes for AES-256) var encryptionKey = []byte("MR5EPvupatnIMbDq3Ix178RAZbYzF94A") // 32 bytes key for AES-256 // AES encryption func encryptAES256CBC(plainText string) (string, error) { block, err := aes.NewCipher(encryptionKey) if err != nil { return "", err } // Generate random IV (16 bytes for AES block size) iv := make([]byte, aes.BlockSize) if _, err := rand.Read(iv); err != nil { return "", err } // Pad the plaintext using PKCS7 padding := aes.BlockSize - len(plainText)%aes.BlockSize paddedText := append([]byte(plainText), bytes.Repeat([]byte{byte(padding)}, padding)...) // Encrypt the plaintext ciphertext := make([]byte, len(paddedText)) mode := cipher.NewCBCEncrypter(block, iv) mode.CryptBlocks(ciphertext, paddedText) // Combine IV and ciphertext and encode to Base64 ivAndCiphertext := append(iv, ciphertext...) return base64.StdEncoding.EncodeToString(ivAndCiphertext), nil } // AES decryption func decryptAES256CBC(cipherTextBase64 string) (string, error) { block, err := aes.NewCipher(encryptionKey) if err != nil { return "", err } // Decode Base64 string cipherText, err := base64.StdEncoding.DecodeString(cipherTextBase64) if err != nil { return "", err } // Extract IV and ciphertext if len(cipherText) < aes.BlockSize { return "", fmt.Errorf("ciphertext too short") } iv := cipherText[:aes.BlockSize] cipherText = cipherText[aes.BlockSize:] // Decrypt the ciphertext plainText := make([]byte, len(cipherText)) mode := cipher.NewCBCDecrypter(block, iv) mode.CryptBlocks(plainText, cipherText) // Remove PKCS7 padding padding := int(plainText[len(plainText)-1]) if padding < 1 || padding > aes.BlockSize { return "", fmt.Errorf("invalid padding") } for i := len(plainText) - padding; i < len(plainText); i++ { if plainText[i] != byte(padding) { return "", fmt.Errorf("invalid padding") } } plainText = plainText[:len(plainText)-padding] return string(plainText), nil } // generateASCIIQR creates an ASCII QR code from the given text func generateASCIIQR(text string) (string, error) { qr, err := qrcode.New(text, qrcode.Medium) if err != nil { return "", fmt.Errorf("failed to generate QR code: %v", err) } ascii := qr.ToSmallString(false) // false for no inverted colors return ascii, nil } // normalizePhoneNumber converts various Indonesian number formats to 628xxxxxxxxxx func normalizePhoneNumber(phone string) (string, error) { // Remove all non-digits (spaces, dashes, etc.) re := regexp.MustCompile(`\D`) cleaned := re.ReplaceAllString(phone, "") // Handle different prefixes if strings.HasPrefix(cleaned, "62") { return cleaned, nil } else if strings.HasPrefix(cleaned, "+62") { return strings.TrimPrefix(cleaned, "+"), nil } else if strings.HasPrefix(cleaned, "08") { return "62" + cleaned[1:], nil } // Validate length (10-13 digits, typically 12 for Indonesian mobile) if len(cleaned) < 10 || len(cleaned) > 13 { return "", fmt.Errorf("invalid phone number length: %s", phone) } return cleaned, nil } // Initialize WhatsApp client func initWhatsAppClient() error { // Setup logger logger := waLog.Stdout("wa", "INFO", true) // Setup SQL store with logger container, err := sqlstore.New(context.Background(), "sqlite3", "whatsmeow.db?_foreign_keys=on", logger) if err != nil { return fmt.Errorf("failed to create SQL store: %v", err) } whatsappContainer = container // Get or create device device, err := container.GetFirstDevice(context.Background()) if err != nil { return fmt.Errorf("failed to get device: %v", err) } // Create client with logger and auto-reconnect client := whatsmeow.NewClient(device, logger) client.EnableAutoReconnect = true client.AddEventHandler(func(evt interface{}) { switch evt.(type) { case *events.Message: // Handle incoming messages if needed } }) whatsappClient = client return nil } // Get WhatsApp client func getWhatsAppClient() (*whatsmeow.Client, error) { clientMutex.RLock() client := whatsappClient clientMutex.RUnlock() if client == nil { return nil, errors.New("WhatsApp client not initialized") } return client, nil } func main() { var err error // Connect to MySQL db, err = sql.Open("mysql", "client:@tcp(server.linksoftindo.com:3308)/dbbayiku?parseTime=true&multiStatements=true&allowOldPasswords=1&loc="+url.QueryEscape("Asia/Jakarta")) // rabasuni11A // sql.Open("mysql", "client:@tcp(server.linksoftindo.com:3308)/dbbayiku?parseTime=true&allowOldPasswords=1&multiStatements=true&loc=" + url.QueryEscape("Asia/Bangkok")) if err != nil { log.Fatal("Failed to connect to database:", err) } defer db.Close() // Test the connection if err := db.Ping(); err != nil { log.Fatal("Database connection failed:", err) } r := gin.Default() // Add CORS middleware globally to allow cross-origin requests r.Use(cors.New(cors.Config{ AllowOrigins: []string{"*"}, // Allow all origins (replace with specific origins if needed) AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, AllowCredentials: true, })) // Initialize WhatsApp client if err := initWhatsAppClient(); err != nil { log.Printf("Warning: Failed to initialize WhatsApp client: %v", err) } // Serve static images from the /images/ directory r.Static("/images", "./images") // Assuming your images are in a directory named "images" at the root of your project // Public Routes r.POST("/auth/login", loginHandler) r.POST("/auth/register", registerHandler) // Add the new OTP route r.POST("/send-otp", sendOTPHandler) r.POST("/send-otp-forgot", sendOTPForgotHandler) r.POST("/verify-otp", verifyOTPHandler) r.POST("/verify-otp-forgot", verifyForgotOTPHandler) r.GET("/cek-time", getCurrentTimeHandler) // Address r.POST("/regency", regencyHandler) r.POST("/district/:regency_id", districtHandler) r.POST("/village/:district_id", villageHandler) // WhatsApp routes (from working code) r.GET("/whatsapp/login", whatsappLoginHandler) r.POST("/whatsapp/send", whatsappSendHandler) r.GET("/whatsapp/status", whatsappStatusHandler) r.POST("/whatsapp/logout", whatsappLogoutHandler) // Protected Routes protected := r.Group("/products") // protected.Use(jwtAuthMiddleware()) protected.GET("/", getProducts) protected.GET("/grosir", getGrosirProducts) protected.POST("/address", getAdministrativeData) protected.GET("/home", getHomeProducts) protected.GET("/promo", getPromoProducts) protected.GET("/:id", getProductByID) protected.GET("/grosir/:id", getProductGrosirByID) protected.GET("/golongan/:id", getProductByIDGolongan) protected.GET("/grosir/golongan/:id", getProductGrosirByIDGolongan) // protected.POST("/", createProduct) // protected.PUT("/:id", updateProduct) // protected.DELETE("/:id", deleteProduct) protectedAuth := r.Group("/auth") protectedAuth.Use(jwtAuthMiddleware()) protectedAuth.POST("/password", changePasswordHandler) protectedAuth.POST("/forgot-change-password", changePassworForgotdHandler) protectedAuth.POST("/profile", updateProfileHandler) protectedAuth.GET("/profile/:username", getProfileHandler) protectedOrders := r.Group("/orders") protectedOrders.Use(jwtAuthMiddleware()) protectedOrders.GET("/show", showOrdersHandler) protectedOrders.POST("/insert", insertOrderHandler) protectedOrders.POST("/batal", batalOrderHandler) protectedSettings := r.Group("/settings") protectedSettings.Use(jwtAuthMiddleware()) protectedSettings.GET("/banner", getBannerHandler) protectedSettings.GET("/info", getInfoHandler) protectedVoucher := r.Group("/voucher") protectedVoucher.Use(jwtAuthMiddleware()) protectedVoucher.GET("/", getVoucher) protectedVoucher.GET("/code/:code", getVoucherCode) protectedVoucher.POST("/", createVoucher) protectedCabang := r.Group("/cabang") protectedCabang.Use(jwtAuthMiddleware()) protectedCabang.GET("/", getCabang) // Add the new setup route protectedSetup := r.Group("/setup") protectedSetup.Use(jwtAuthMiddleware()) protectedSetup.GET("/ongkir", getOngkirHandler) // r.GET("/generate-token", func(c *gin.Context) { // token, err := generateToken("admin") // Replace "admin" with any username // if err != nil { // c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) // return // } // c.JSON(http.StatusOK, gin.H{"token": token}) // }) // Run the server r.Run(":9797") } // JWT utilities func generateToken(username string) (string, error) { claims := jwt.MapClaims{ "username": username, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(jwtSecret) } func jwtAuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization token required"}) c.Abort() return } tokenString := authHeader[len("Bearer "):] token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, gin.Error{Err: jwt.ErrSignatureInvalid} } return jwtSecret, nil }) if err != nil || !token.Valid { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"}) c.Abort() return } // Add username to context claims := token.Claims.(jwt.MapClaims) c.Set("username", claims["username"]) c.Next() } } // Auth Handlers // Auth Handlers func loginHandler(c *gin.Context) { var loginData struct { Username string `json:"username"` Password string `json:"password"` } if err := c.ShouldBindJSON(&loginData); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) return } var customer Customer err := db.QueryRow("SELECT username,password, nama, alamat,kabupaten,kecamatan,desa,no_telp,email,tipe_user FROM tb_customer WHERE username = ?", loginData.Username).Scan(&customer.Username, &customer.Password, &customer.Name, &customer.Address, &customer.Regency, &customer.District, &customer.Village, &customer.Phone, &customer.Email, &customer.TipeUser) if err == sql.ErrNoRows { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"}) return } // Decrypt the stored password (encrypted) decryptedPassword, err := decryptAES256CBC(customer.Password) if err != nil || decryptedPassword != loginData.Password { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password", "decryptedPassword": decryptedPassword, "inputPassword": loginData.Password}) return } token, err := generateToken(customer.Username) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) return } c.JSON(http.StatusOK, gin.H{"token": token, "data": customer, "status": http.StatusOK}) } func registerHandler(c *gin.Context) { var newCustomer Customer if err := c.ShouldBindJSON(&newCustomer); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) return } // Encrypt the password before storing encryptedPassword, err := encryptAES256CBC(newCustomer.Password) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to encrypt password"}) return } _, err = db.Exec("INSERT INTO tb_customer (username, password, email, nama, alamat, kabupaten, kecamatan, desa, no_telp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", newCustomer.Username, encryptedPassword, newCustomer.Email, newCustomer.Name, newCustomer.Address, newCustomer.Regency, newCustomer.District, newCustomer.Village, newCustomer.Phone) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err}) return } c.JSON(http.StatusCreated, gin.H{"message": "Registration successful"}) } func changePasswordHandler(c *gin.Context) { username := c.GetString("username") // Get the logged-in user's username var passwordData struct { OldPassword string `json:"password_lama"` NewPassword string `json:"password_baru"` } if err := c.ShouldBindJSON(&passwordData); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) return } // Fetch the encrypted password from the database for the logged-in user var encryptedPassword string err := db.QueryRow("SELECT password FROM tb_customer WHERE username = ?", username).Scan(&encryptedPassword) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve user information"}) return } // Decrypt the old password from the database decryptedOldPassword, err := decryptAES256CBC(encryptedPassword) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decrypt old password"}) return } // Compare the decrypted old password with the provided old password if decryptedOldPassword != passwordData.OldPassword { c.JSON(http.StatusUnauthorized, gin.H{"error": "Incorrect old password", "old": passwordData.OldPassword, "decrypted": decryptedOldPassword}) return } // Encrypt the new password before storing it encryptedNewPassword, err := encryptAES256CBC(passwordData.NewPassword) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to encrypt new password"}) return } // Update the password in the database _, err = db.Exec("UPDATE tb_customer SET password = ? WHERE username = ?", encryptedNewPassword, username) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update password"}) return } c.JSON(http.StatusOK, gin.H{"message": "Password updated successfully"}) // c.JSON(http.StatusOK, gin.H{"message": "MASHOOOK"}) } func changePassworForgotdHandler(c *gin.Context) { username := c.GetString("username") // Get the logged-in user's username var passwordData struct { NewPassword string `json:"password_baru"` } if err := c.ShouldBindJSON(&passwordData); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) return } // Encrypt the new password before storing it encryptedNewPassword, err := encryptAES256CBC(passwordData.NewPassword) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to encrypt new password"}) return } // Update the password in the database _, err = db.Exec("UPDATE tb_customer SET password = ? WHERE username = ?", encryptedNewPassword, username) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update password"}) return } c.JSON(http.StatusOK, gin.H{"message": "Password updated successfully"}) // c.JSON(http.StatusOK, gin.H{"message": "MASHOOOK"}) } func updateProfileHandler(c *gin.Context) { username := c.GetString("username") var profileData Customer // Bind JSON data if err := c.ShouldBindJSON(&profileData); err != nil { log.Printf("Failed to bind JSON: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) return } // Log the received data log.Printf("Received profile data: %+v", profileData) // Execute the SQL query result, err := db.Exec("UPDATE tb_customer SET nama = ?, alamat = ?, kabupaten = ?, kecamatan = ?, desa = ?, email = ? WHERE username = ?", profileData.Name, profileData.Address, profileData.Regency, profileData.District, profileData.Village, profileData.Email, username) if err != nil { log.Printf("Database error: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update profile", "msg": err.Error()}) return } // Log the number of rows affected rowsAffected, _ := result.RowsAffected() log.Printf("Rows affected: %d", rowsAffected) c.JSON(http.StatusOK, gin.H{"message": "Profile updated successfully"}) } func getProfileHandler(c *gin.Context) { username := c.GetString("username") var customer Customer err := db.QueryRow("SELECT username, email, nama, alamat, kecamatan, no_telp FROM tb_customer WHERE username = ?", username).Scan( &customer.Username, &customer.Email, &customer.Name, &customer.Address, &customer.District, &customer.Phone) if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "Profile not found"}) return } c.JSON(http.StatusOK, customer) } // Handlers type ImagePaths struct { ImageKitURL string LocalPath string LocalURL string } func downloadAndSaveImage(imagePath ImagePaths) error { // Create the directory if it doesn't exist dir := filepath.Dir(imagePath.LocalPath) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("failed to create directory: %v", err) } if _, err := os.Stat(imagePath.LocalPath); errors.Is(err, os.ErrNotExist) { // path/to/whatever does not exist // Download the image resp, err := http.Get(imagePath.ImageKitURL + "?tr=w-133,h-153") if err != nil { return fmt.Errorf("failed to download image: %v", err) } defer resp.Body.Close() // Create the local file file, err := os.Create(imagePath.LocalPath) if err != nil { return fmt.Errorf("failed to create file: %v", err) } defer file.Close() // Copy the content _, err = io.Copy(file, resp.Body) if err != nil { return fmt.Errorf("failed to save image: %v", err) } } return nil } func getFileNameFromURL(urlString string) string { // Parse the URL u, err := url.Parse(urlString) if err != nil { return "" } // Get the path component urlPath := u.Path // Extract the filename from the path fileName := path.Base(urlPath) // Remove any query parameters if present fileName = strings.Split(fileName, "?")[0] return fileName } // Get all products func getProducts(c *gin.Context) { // Get kode_cabang from the request. This assumes you're passing it as a query parameter. // You could also get it from the request body or a custom header. kodeCabang := c.Query("kode_cabang") // Construct the query with the new join and WHERE clause. rows, err := db.Query(` SELECT b.kode_barang AS kodeBrg, b.nama_barang AS namaBrg, b.harga, b.berat, b.limit, b.harga_promo AS hargaPromo, b.image_url AS imageUrl, b.kode_golongan AS kodeGolongan, b.nama_golongan AS kategori, b.kode_promo AS kodePromo, b.deskripsi, COALESCE(s.stock, 0) AS stock, b.prioritas, b.bonusItem AS bonusItem, v.jenis_potongan AS jenisPotongan, v.jumlah_potongan AS jumlahPotongan FROM tm_barang b LEFT JOIN tb_voucher v ON v.id_target LIKE CONCAT('%', b.kode_barang, '%') AND v.aktif = 1 AND NOW() <= v.end_date AND v.is_redeem = 0 LEFT JOIN tb_stockbrg s ON b.kode_barang = s.kode_barang AND s.kode_cabang = ? WHERE b.aktif = 'Y' AND b.kode_barang IS NOT NULL AND b.kode_barang != "" GROUP BY b.kode_barang ORDER BY (b.prioritas IS NULL) DESC, b.prioritas ASC ; `, kodeCabang) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } defer rows.Close() var products []Product for rows.Next() { var product Product var hargaPromo sql.NullString var bonusItem sql.NullString if err := rows.Scan( &product.KodeBrg, &product.NamaBrg, &product.Harga, &product.Berat, &product.Limit, &hargaPromo, &product.ImageKitURL, &product.KodeGolongan, &product.Kategori, &product.KodePromo, &product.Deskripsi, &product.Stock, &product.Prioritas, &bonusItem, &product.JenisPotongan, &product.JumlahPotongan, ); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if product.ImageKitURL.Valid == false { } imagePath := ImagePaths{ ImageKitURL: product.ImageKitURL.String, LocalPath: "./images/" + getFileNameFromURL(product.ImageKitURL.String), LocalURL: "https://api.bayiku.linksoftindo.com/images/" + getFileNameFromURL(product.ImageKitURL.String), } downloadAndSaveImage(imagePath) product.ImageURL = &imagePath.LocalURL // Assign pointer or nil to HargaPromo if hargaPromo.Valid { product.HargaPromo = &hargaPromo.String } else { product.HargaPromo = nil } if bonusItem.Valid { product.BonusItem = &bonusItem.String } else { product.BonusItem = nil } products = append(products, product) } c.JSON(http.StatusOK, products) } // Get all products func getGrosirProducts(c *gin.Context) { // Get kode_cabang from the request. This assumes you're passing it as a query parameter. // You could also get it from the request body or a custom header. kodeCabang := c.Query("kode_cabang") // Construct the query with the new join and WHERE clause. rows, err := db.Query(` SELECT b.kode_barang AS kodeBrg, b.nama_barang AS namaBrg, b.harga_grosir AS harga, b.berat, b.limit, b.harga_promo AS hargaPromo, b.image_url AS imageUrl, b.kode_golongan AS kodeGolongan, b.nama_golongan AS kategori, b.kode_promo AS kodePromo, b.deskripsi, COALESCE(s.stock, 0) AS stock, b.prioritas, b.bonusItem AS bonusItem, v.jenis_potongan AS jenisPotongan, v.jumlah_potongan AS jumlahPotongan FROM tm_barang b LEFT JOIN tb_voucher v ON v.id_target LIKE CONCAT('%', b.kode_barang, '%') AND v.aktif = 1 AND NOW() <= v.end_date AND v.is_redeem = 0 LEFT JOIN tb_stockbrg s ON b.kode_barang = s.kode_barang AND s.kode_cabang = ? WHERE b.aktif = 'Y' AND b.kode_barang IS NOT NULL AND b.kode_barang != "" GROUP BY b.kode_barang ORDER BY (b.prioritas IS NULL) DESC, b.prioritas ASC ; `, kodeCabang) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } defer rows.Close() var products []Product for rows.Next() { var product Product var hargaPromo sql.NullString var bonusItem sql.NullString if err := rows.Scan( &product.KodeBrg, &product.NamaBrg, &product.Harga, &product.Berat, &product.Limit, &hargaPromo, &product.ImageKitURL, &product.KodeGolongan, &product.Kategori, &product.KodePromo, &product.Deskripsi, &product.Stock, &product.Prioritas, &bonusItem, ); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if product.ImageKitURL.Valid == false { } imagePath := ImagePaths{ ImageKitURL: product.ImageKitURL.String, LocalPath: "./images/" + getFileNameFromURL(product.ImageKitURL.String), LocalURL: "https://api.bayiku.linksoftindo.com/images/" + getFileNameFromURL(product.ImageKitURL.String), } downloadAndSaveImage(imagePath) product.ImageURL = &imagePath.LocalURL // Assign pointer or nil to HargaPromo if hargaPromo.Valid { product.HargaPromo = &hargaPromo.String } else { product.HargaPromo = nil } if bonusItem.Valid { product.BonusItem = &bonusItem.String } else { product.BonusItem = nil } products = append(products, product) } c.JSON(http.StatusOK, products) } func getHomeProducts(c *gin.Context) { kodeCabang := c.Query("kode_cabang") rows, err := db.Query(` WITH RankedProducts AS ( SELECT b.kode_barang AS kodeBrg, b.nama_barang AS namaBrg, b.harga, b.berat, b.limit, b.harga_promo AS hargaPromo, -- Corrected alias here b.image_url AS imageUrl, b.kode_golongan AS kodeGolongan, b.nama_golongan AS kategori, b.kode_promo AS kodePromo, b.deskripsi, COALESCE(s.stock, 0) AS stock, b.prioritas, b.bonusItem AS bonusItem, v.jenis_potongan AS jenisPotongan, v.jumlah_potongan AS jumlahPotongan, ROW_NUMBER() OVER (PARTITION BY b.kode_golongan ORDER BY b.prioritas ASC) AS row_num FROM tm_barang b LEFT JOIN tb_voucher v ON v.id_target LIKE CONCAT('%', b.kode_barang, '%') AND v.aktif = 1 AND NOW() <= v.end_date AND v.is_redeem = 0 LEFT JOIN tb_stockbrg s ON b.kode_barang = s.kode_barang AND s.kode_cabang = ? WHERE b.aktif = 'Y' ) SELECT kodeBrg, namaBrg, harga, berat, "limit", hargaPromo, -- Using alias here imageUrl, kodeGolongan, kategori, kodePromo, deskripsi, stock, prioritas, bonusItem, jenisPotongan, jumlahPotongan FROM RankedProducts WHERE row_num <= 10 ORDER BY kodeGolongan, row_num; `, kodeCabang) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } defer rows.Close() var products []Product for rows.Next() { var product Product var hargaPromo sql.NullString var bonusItem sql.NullString if err := rows.Scan( &product.KodeBrg, &product.NamaBrg, &product.Harga, &product.Berat, &product.Limit, &hargaPromo, &product.ImageKitURL, &product.KodeGolongan, &product.Kategori, &product.KodePromo, &product.Deskripsi, &product.Stock, &product.Prioritas, &bonusItem, &product.JenisPotongan, &product.JumlahPotongan, ); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if product.ImageKitURL.Valid == false { } imagePath := ImagePaths{ ImageKitURL: product.ImageKitURL.String, LocalPath: "./images/" + getFileNameFromURL(product.ImageKitURL.String), LocalURL: "https://api.bayiku.linksoftindo.com/images/" + getFileNameFromURL(product.ImageKitURL.String), } downloadAndSaveImage(imagePath) product.ImageURL = &imagePath.LocalURL // Assign pointer or nil to HargaPromo if hargaPromo.Valid { product.HargaPromo = &hargaPromo.String } else { product.HargaPromo = nil } if bonusItem.Valid { product.BonusItem = &bonusItem.String } else { product.BonusItem = nil } products = append(products, product) } c.JSON(http.StatusOK, products) } // Get all promo products func getPromoProducts(c *gin.Context) { kodeCabang := c.Query("kode_cabang") rows, err := db.Query(` SELECT b.kode_barang AS kodeBrg, b.nama_barang AS namaBrg, b.harga, b.berat, b.limit, b.harga_promo AS hargaPromo, b.image_url AS imageUrl, b.kode_golongan AS kodeGolongan, b.nama_golongan AS kategori, b.kode_promo AS kodePromo, b.deskripsi, COALESCE(s.stock, 0) AS stock, b.prioritas, COALESCE(bonusItem, '') AS bonusItem, v.jenis_potongan AS jenisPotongan, v.jumlah_potongan AS jumlahPotongan FROM tm_barang b LEFT JOIN tb_voucher v ON v.id_target LIKE CONCAT('%', b.kode_barang, '%') AND v.aktif = 1 AND NOW() <= v.end_date AND v.is_redeem = 0 LEFT JOIN tb_stockbrg s ON b.kode_barang = s.kode_barang AND s.kode_cabang = ? WHERE b.promo = 'Y' AND b.aktif = 'Y' ORDER BY b.prioritas ASC `, kodeCabang) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } defer rows.Close() var products []Product for rows.Next() { var product Product var hargaPromo sql.NullString var bonusItem sql.NullString if err := rows.Scan( &product.KodeBrg, &product.NamaBrg, &product.Harga, &product.Berat, &product.Limit, &hargaPromo, &product.ImageKitURL, &product.KodeGolongan, &product.Kategori, &product.KodePromo, &product.Deskripsi, &product.Stock, &product.Prioritas, &bonusItem, &product.JenisPotongan, &product.JumlahPotongan, ); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if product.ImageKitURL.Valid == false { } imagePath := ImagePaths{ ImageKitURL: product.ImageKitURL.String, LocalPath: "./images/" + getFileNameFromURL(product.ImageKitURL.String), LocalURL: "https://api.bayiku.linksoftindo.com/images/" + getFileNameFromURL(product.ImageKitURL.String), } downloadAndSaveImage(imagePath) product.ImageURL = &imagePath.LocalURL // Assign pointer or nil to HargaPromo if hargaPromo.Valid { product.HargaPromo = &hargaPromo.String } else { product.HargaPromo = nil } if bonusItem.Valid { product.BonusItem = &bonusItem.String } else { product.BonusItem = nil } products = append(products, product) } c.JSON(http.StatusOK, products) } // Get product by ID func getProductByID(c *gin.Context) { id := c.Param("id") kodeCabang := c.Query("kode_cabang") var product Product var hargaPromo sql.NullString // var bonusItem sql.NullString err := db.QueryRow(` SELECT b.kode_barang AS kodeBrg, nama_barang AS namaBrg, harga, berat, ` + "`limit`" + `, harga_promo AS hargaPromo, image_url AS imageUrl, kode_golongan AS kodeGolongan, nama_golongan AS kategori, kode_promo AS kodePromo, deskripsi, COALESCE(s.stock, 0) AS stock, prioritas, COALESCE(b.bonusItem, '') AS bonusItem, v.jenis_potongan AS jenisPotongan, v.jumlah_potongan AS jumlahPotongan FROM tm_barang b LEFT JOIN tb_voucher v ON v.id_target LIKE CONCAT('%', b.kode_barang, '%') AND v.aktif = 1 AND NOW() <= v.end_date AND v.is_redeem = 0 LEFT JOIN tb_stockbrg s ON b.kode_barang = s.kode_barang AND s.kode_cabang = ? WHERE b.kode_barang = ?`, kodeCabang, id).Scan( &product.KodeBrg, &product.NamaBrg, &product.Harga, &product.Berat, &product.Limit, &hargaPromo, &product.ImageKitURL, &product.KodeGolongan, &product.Kategori, &product.KodePromo, &product.Deskripsi, &product.Stock, &product.Prioritas, &product.BonusItem, &product.JenisPotongan, &product.JumlahPotongan, ) if product.ImageKitURL.Valid == false { } imagePath := ImagePaths{ ImageKitURL: product.ImageKitURL.String, LocalPath: "./images/" + getFileNameFromURL(product.ImageKitURL.String), LocalURL: "https://api.bayiku.linksoftindo.com/images/" + getFileNameFromURL(product.ImageKitURL.String), } downloadAndSaveImage(imagePath) product.ImageURL = &imagePath.LocalURL if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"message": "Product not found"}) return } else if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, product) } // Get product by ID func getProductGrosirByID(c *gin.Context) { id := c.Param("id") kodeCabang := c.Query("kode_cabang") var product Product var hargaPromo sql.NullString var bonusItem sql.NullString err := db.QueryRow(` SELECT b.kode_barang AS kodeBrg, nama_barang AS namaBrg, harga_grosir AS harga, berat, ` + "`limit`" + `, harga_promo AS hargaPromo, image_url AS imageUrl, kode_golongan AS kodeGolongan, nama_golongan AS kategori, kode_promo AS kodePromo, deskripsi, COALESCE(s.stock, 0) AS stock, prioritas, COALESCE(bonusItem, '') AS bonusItem, min_qty_grosir AS minQtyGrosir FROM tm_barang b LEFT JOIN tb_stockbrg s ON b.kode_barang = s.kode_barang AND s.kode_cabang = ? WHERE b.kode_barang = ?`, kodeCabang, id).Scan( &product.KodeBrg, &product.NamaBrg, &product.Harga, &product.Berat, &product.Limit, &hargaPromo, &product.ImageKitURL, &product.KodeGolongan, &product.Kategori, &product.KodePromo, &product.Deskripsi, &product.Stock, &product.Prioritas, &bonusItem, &product.MinQtyGrosir, ) if product.ImageKitURL.Valid == false { } imagePath := ImagePaths{ ImageKitURL: product.ImageKitURL.String, LocalPath: "./images/" + getFileNameFromURL(product.ImageKitURL.String), LocalURL: "https://api.bayiku.linksoftindo.com/images/" + getFileNameFromURL(product.ImageKitURL.String), } downloadAndSaveImage(imagePath) product.ImageURL = &imagePath.LocalURL if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"message": "Product not found"}) return } else if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, product) } // Get product by ID Golongan func getProductByIDGolongan(c *gin.Context) { id := c.Param("id") kodeCabang := c.Query("kode_cabang") // Use db.Query to fetch multiple rows rows, err := db.Query(` SELECT b.kode_barang AS kodeBrg, nama_barang AS namaBrg, harga, berat, ` + "`limit`" + `, harga_promo AS hargaPromo, image_url AS imageUrl, kode_golongan AS kodeGolongan, nama_golongan AS kategori, kode_promo AS kodePromo, deskripsi, COALESCE(s.stock, 0) AS stock, prioritas, COALESCE(bonusItem, '') AS bonusItem, v.jenis_potongan AS jenisPotongan, v.jumlah_potongan AS jumlahPotongan FROM tm_barang b LEFT JOIN tb_voucher v ON v.id_target LIKE CONCAT('%', b.kode_barang, '%') AND v.aktif = 1 AND NOW() <= v.end_date AND v.is_redeem = 0 LEFT JOIN tb_stockbrg s ON b.kode_barang = s.kode_barang AND s.kode_cabang = ? WHERE b.kode_golongan = ? AND b.aktif = 'Y'`, kodeCabang, id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } defer rows.Close() var products []Product for rows.Next() { var product Product var hargaPromo sql.NullString var bonusItem sql.NullString if err := rows.Scan( &product.KodeBrg, &product.NamaBrg, &product.Harga, &product.Berat, &product.Limit, &hargaPromo, &product.ImageKitURL, &product.KodeGolongan, &product.Kategori, &product.KodePromo, &product.Deskripsi, &product.Stock, &product.Prioritas, &bonusItem, &product.JenisPotongan, &product.JumlahPotongan, ); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if product.ImageKitURL.Valid == false { } imagePath := ImagePaths{ ImageKitURL: product.ImageKitURL.String, LocalPath: "./images/" + getFileNameFromURL(product.ImageKitURL.String), LocalURL: "https://api.bayiku.linksoftindo.com/images/" + getFileNameFromURL(product.ImageKitURL.String), } downloadAndSaveImage(imagePath) product.ImageURL = &imagePath.LocalURL // Assign pointer or nil to HargaPromo if hargaPromo.Valid { product.HargaPromo = &hargaPromo.String } else { product.HargaPromo = nil } if bonusItem.Valid { product.BonusItem = &bonusItem.String } else { product.BonusItem = nil } products = append(products, product) } c.JSON(http.StatusOK, products) } // Get product by ID Golongan func getProductGrosirByIDGolongan(c *gin.Context) { id := c.Param("id") kodeCabang := c.Query("kode_cabang") // Use db.Query to fetch multiple rows rows, err := db.Query(` SELECT b.kode_barang AS kodeBrg, nama_barang AS namaBrg, harga_grosir AS harga, berat, ` + "`limit`" + `, harga_promo AS hargaPromo, image_url AS imageUrl, kode_golongan AS kodeGolongan, nama_golongan AS kategori, kode_promo AS kodePromo, deskripsi, COALESCE(s.stock, 0) AS stock, prioritas, COALESCE(bonusItem, '') AS bonusItem, min_qty_grosir AS minQtyGrosir FROM tm_barang b LEFT JOIN tb_stockbrg s ON b.kode_barang = s.kode_barang AND s.kode_cabang = ? WHERE b.kode_golongan = ? AND b.aktif = 'Y'`, kodeCabang, id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } defer rows.Close() var products []Product for rows.Next() { var product Product var hargaPromo sql.NullString var bonusItem sql.NullString if err := rows.Scan( &product.KodeBrg, &product.NamaBrg, &product.Harga, &product.Berat, &product.Limit, &hargaPromo, &product.ImageKitURL, &product.KodeGolongan, &product.Kategori, &product.KodePromo, &product.Deskripsi, &product.Stock, &product.Prioritas, &bonusItem, &product.MinQtyGrosir, ); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if product.ImageKitURL.Valid == false { } imagePath := ImagePaths{ ImageKitURL: product.ImageKitURL.String, LocalPath: "./images/" + getFileNameFromURL(product.ImageKitURL.String), LocalURL: "https://api.bayiku.linksoftindo.com/images/" + getFileNameFromURL(product.ImageKitURL.String), } downloadAndSaveImage(imagePath) product.ImageURL = &imagePath.LocalURL // Assign pointer or nil to HargaPromo if hargaPromo.Valid { product.HargaPromo = &hargaPromo.String } else { product.HargaPromo = nil } if bonusItem.Valid { product.BonusItem = &bonusItem.String } else { product.BonusItem = nil } products = append(products, product) } c.JSON(http.StatusOK, products) } // Create a new product func createProduct(c *gin.Context) { var newProduct Product if err := c.ShouldBindJSON(&newProduct); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } result, err := db.Exec(` INSERT INTO tm_barang (kode_barang, nama_barang, harga, berat, `+"`limit`"+`, harga_promo, image_url, kode_golongan, nama_golongan, kode_promo, deskripsi, stock_akhir, prioritas, bonusItem) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, newProduct.KodeBrg, newProduct.NamaBrg, newProduct.Harga, newProduct.Berat, newProduct.Limit, newProduct.HargaPromo, newProduct.ImageURL, newProduct.KodeGolongan, newProduct.Kategori, newProduct.KodePromo, newProduct.Deskripsi, newProduct.Stock, newProduct.Prioritas, newProduct.BonusItem, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } id, _ := result.LastInsertId() newProduct.KodeBrg = strconv.FormatInt(id, 10) c.JSON(http.StatusCreated, newProduct) } // Update an existing product func updateProduct(c *gin.Context) { id := c.Param("id") var updatedProduct Product if err := c.ShouldBindJSON(&updatedProduct); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } _, err := db.Exec(` UPDATE tm_barang SET nama_barang = ?, harga = ?, berat = ?, `+"`limit`"+` = ?, harga_promo = ?, image_url = ?, kode_golongan = ?, nama_golongan = ?, kode_promo = ?, deskripsi = ?, stock_akhir = ?, prioritas = ?, bonusItem = ? WHERE kode_barang = ?`, updatedProduct.NamaBrg, updatedProduct.Harga, updatedProduct.Berat, updatedProduct.Limit, updatedProduct.HargaPromo, updatedProduct.ImageURL, updatedProduct.KodeGolongan, updatedProduct.Kategori, updatedProduct.KodePromo, updatedProduct.Deskripsi, updatedProduct.Stock, updatedProduct.Prioritas, updatedProduct.BonusItem, id, ) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, updatedProduct) } // Delete a product func deleteProduct(c *gin.Context) { id := c.Param("id") _, err := db.Exec("DELETE FROM tm_barang WHERE kode_barang = ?", id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "Product deleted"}) } // ORDERS START func showOrdersHandler(c *gin.Context) { username := c.GetString("username") rows, err := db.Query(` SELECT kode_order, DATE_FORMAT(tanggal, '%d %M %Y %H:%i.%s') AS tanggal, username, nama_cabang, nama_cust, alamat_cust, ongkir, discount, nilai, cara_bayar, status_order, batal_oleh, ket_batal FROM tr_order_m m LEFT JOIN tb_cabang b ON m.kode_cabang = b.kode_cabang WHERE username = ? AND tanggal > DATE_SUB(CURDATE(), INTERVAL 1 MONTH) ORDER BY kode_order DESC`, username) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } defer rows.Close() var orders []Order for rows.Next() { var order Order err := rows.Scan(&order.KodeOrder, &order.Tanggal, &order.Username, &order.NamaCabang, &order.NamaCust, &order.AlamatCust, &order.Ongkir, &order.Discount, &order.Nilai, &order.CaraBayar, &order.StatusOrder, &order.BatalOleh, &order.KetBatal) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } itemRows, err := db.Query("SELECT kode_order, kode_barang, nama_barang, image_url, jumlah, berat, harga, harga_promo, harga_jual, promo, isFree FROM tr_order_d WHERE kode_order = ?", order.KodeOrder) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } defer itemRows.Close() var items []OrderItem for itemRows.Next() { var item OrderItem if err := itemRows.Scan(&item.KodeOrder, &item.KodeBarang, &item.NamaBarang, &item.ImageURL, &item.Jumlah, &item.Berat, &item.Harga, &item.HargaPromo, &item.HargaJual, &item.Promo, &item.IsFree); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } items = append(items, item) } order.Items = items orders = append(orders, order) } c.JSON(http.StatusOK, orders) } func insertOrderHandler(c *gin.Context) { var order Order if err := c.ShouldBindJSON(&order); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input", "details": err.Error()}) return } // Generate new order code var newKodeOrder string err := db.QueryRow(`SELECT IFNULL(MAX(kode_order) + 1, CONCAT(DATE_FORMAT(NOW(), '%y%m'), '000001')) AS kode_order FROM tr_order_m WHERE LENGTH(kode_order) = 10 AND LEFT(kode_order, 4) = DATE_FORMAT(NOW(), '%y%m')`).Scan(&newKodeOrder) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate order code"}) return } // Start a transaction tx, err := db.Begin() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to start transaction"}) return } // Insert order into tr_order_m _, err = tx.Exec(` INSERT INTO tr_order_m (kode_order, tanggal, username, nama_cust, alamat_cust, lat, `+"`long`"+`, ongkir, discount, nilai, cara_bayar, status_order, versi, kode_cabang, kode_voucher) VALUES (?, NOW(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, newKodeOrder, order.Username, order.NamaCust, order.AlamatCust, order.Lat, order.Long, order.Ongkir, order.Discount, order.Nilai, order.CaraBayar, "Proses", order.Versi, order.KodeCabang, order.KodeVoucher) if err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to insert order","msg": err}) return } // Insert items into tr_order_d for i, item := range order.Items { _, err := tx.Exec(` INSERT INTO tr_order_d (item, kode_order, kode_barang, nama_barang, image_url, jumlah, berat, harga, harga_promo, harga_jual, promo, isFree) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, i+1, newKodeOrder, item.KodeBarang, item.NamaBarang, item.ImageURL, item.Jumlah, item.Berat, item.Harga, item.HargaPromo, item.HargaJual, item.Promo, item.IsFree) if err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to insert order items", "sql_query": "INSERT INTO tr_order_d (item, kode_order, kode_barang, nama_barang, image_url, jumlah, berat, harga, harga_promo, harga_jual, promo, isFree) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", "sql_values": []interface{}{i+1, newKodeOrder, item.KodeBarang, item.NamaBarang, item.ImageURL, item.Jumlah, item.Berat, item.Harga, item.HargaPromo, item.HargaJual, item.Promo, item.IsFree}, "error_detail": err.Error(), }) return } } // Commit the transaction if err := tx.Commit(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Transaction failed"}) return } c.JSON(http.StatusCreated, gin.H{"message": "Order successfully created", "kodeOrder": newKodeOrder}) } func batalOrderHandler(c *gin.Context) { var order struct { KodeOrder string `json:"kodeOrder"` KetBatal string `json:"ketBatal"` } if err := c.ShouldBindJSON(&order); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"}) return } _, err := db.Exec(` UPDATE tr_order_m SET status_order = 'Batal', batal_oleh = 'User', ket_batal = ? WHERE kode_order = ?`, order.KetBatal, order.KodeOrder) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to cancel order"}) return } c.JSON(http.StatusOK, gin.H{"message": "Order successfully canceled"}) } // ORDERS END // SETTINGS START func getBannerHandler(c *gin.Context) { username := c.GetString("username") var banner Banner err := db.QueryRow("SELECT image_url FROM tb_banner WHERE aktif = 'Y' LIMIT 1").Scan(&banner.ImageURL) if err == sql.ErrNoRows { // No active banner found, use default banner.ImageURL = "https://ik.imagekit.io/linksoft/banner/default-banner_YzInpHc4F.jpg" } else if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch banner"}) return } c.JSON(http.StatusOK, gin.H{"username": username, "url": banner.ImageURL}) } func getInfoHandler(c *gin.Context) { username := c.GetString("username") var info Info err := db.QueryRow(` SELECT id, nama_info, kode_info, keterangan, mode, payload FROM tb_info WHERE aktif = 1 LIMIT 1 `).Scan(&info.ID, &info.NamaInfo, &info.KodeInfo, &info.Keterangan, &info.Mode.Type, &info.Mode.Payload) if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"message": "Setup info tidak ditemukan!"}) return } else if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch info"}) return } c.JSON(http.StatusOK, gin.H{"username": username, "info": info}) } // SETTINGS END // VOUCHER START func getVoucher(c *gin.Context) { var voucher Voucher err := db.QueryRow(` SELECT kode_voucher, start_date, end_date, target_voucher, id_target, jenis_potongan, jumlah_potongan FROM tb_voucher WHERE aktif = 1 AND NOW() <= end_date `).Scan(&voucher.KodeVoucher, &voucher.StartDate, &voucher.EndDate, &voucher.TargetVoucher, &voucher.IdTarget, &voucher.JenisPotongan, &voucher.JumlahPotongan) if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"message": "Voucher tidak ditemukan!"}) return } else if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch voucher"}) return } c.JSON(http.StatusOK, gin.H{"voucher": voucher}) } func getVoucherCode(c *gin.Context) { code := c.Param("code") username := c.Query("username") // Get username from query parameters fmt.Println("Received voucher code:", code) fmt.Println("Received username:", username) // Log the received username var voucher Voucher err := db.QueryRow(` SELECT kode_voucher, start_date, end_date, target_voucher, id_target, jenis_potongan, jumlah_potongan, is_double_disc FROM tb_voucher WHERE aktif = 1 AND is_redeem = 1 AND kode_voucher = ? AND NOW() <= end_date AND limit_voucher > 0 LIMIT 1 `, code).Scan(&voucher.KodeVoucher, &voucher.StartDate, &voucher.EndDate, &voucher.TargetVoucher, &voucher.IdTarget, &voucher.JenisPotongan, &voucher.JumlahPotongan, &voucher.IsDoubleDisc) if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"message": "Voucher tidak ditemukan!", "code": code}) return } else if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch info"}) return } // Check if the voucher has already been used by the user // var usedCount int // err = db.QueryRow(` // SELECT COUNT(*) // FROM tr_order_m // WHERE kode_voucher = ? AND username = ? AND status_order NOT 'Batal' // `, code, username).Scan(&usedCount) // if err != nil { // c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check voucher usage"}) // return // } // // if usedCount > 0 { // c.JSON(http.StatusForbidden, gin.H{"message": "Voucher sudah digunakan oleh pengguna ini."}) // return // } // Check tb_setupall var limitActive int err = db.QueryRow(` SELECT nilaiint FROM tb_setupall WHERE tb_setupall.group = 'voucher' AND subgroup = 'limit' `).Scan(&limitActive) if err != nil && err != sql.ErrNoRows { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check voucher limit settings"}) return } // Only check voucher usage if limit is active if limitActive == 1 { var usedCount int err = db.QueryRow(` SELECT COUNT(*) FROM tr_order_m WHERE kode_voucher = ? AND username = ? AND status_order NOT 'Batal' `, code, username).Scan(&usedCount) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check voucher usage"}) return } if usedCount > 0 { c.JSON(http.StatusForbidden, gin.H{"message": "Voucher sudah digunakan oleh pengguna ini."}) return } } c.JSON(http.StatusOK, gin.H{"voucher": voucher}) } func createVoucher(c *gin.Context) { var newVoucher Voucher if err := c.ShouldBindJSON(&newVoucher); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request", "message": newVoucher}) return } _, err := db.Exec("INSERT INTO tb_voucher (kode_voucher, start_date, end_date, target_voucher, id_target, jenis_potongan, jumlah_potongan) VALUES (?, ?, ?, ?, ?, ?, ?)", newVoucher.KodeVoucher, newVoucher.StartDate, newVoucher.EndDate, newVoucher.TargetVoucher, newVoucher.IdTarget, newVoucher.JenisPotongan, newVoucher.JumlahPotongan) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err}) return } c.JSON(http.StatusCreated, gin.H{"message": "Voucher successfully created"}) } // VOUCHER END // CABANG START func getCabang(c *gin.Context) { // // // Log the incoming request details // log.Printf("Request Headers: %v\n", c.Request.Header) // log.Printf("Request Method: %s\n", c.Request.Method) // log.Printf("Request URL: %s\n", c.Request.URL) rows, err := db.Query(` SELECT b.kode_cabang, b.nama_cabang, b.lat, b.long, b.keterangan, k.persen, b.aktif FROM tb_cabang b LEFT JOIN tb_kelompok_harga k ON b.kode_kelompok = k.kode_kelompok WHERE b.aktif = 'Y' `) if err != nil { log.Printf("Database error: %v\n", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch Cabang", "details": err.Error()}) return } defer rows.Close() var cabangs []Cabang for rows.Next() { var cabang Cabang if err := rows.Scan(&cabang.KodeCabang, &cabang.NamaCabang, &cabang.Lat, &cabang.Long, &cabang.Keterangan, &cabang.Persen, &cabang.Aktif); err != nil { // log.Printf("Error scanning row: %v\n", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch Cabang", "details": err.Error()}) return } cabangs = append(cabangs, cabang) } if err := rows.Err(); err != nil { // log.Printf("Error iterating rows: %v\n", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch Cabang", "details": err.Error()}) return } if len(cabangs) == 0 { c.JSON(http.StatusNotFound, gin.H{"message": "Cabang tidak ditemukan!"}) return } // log.Printf("Cabang fetched successfully: %+v\n", cabangs) c.JSON(http.StatusOK, gin.H{"cabangs": cabangs}) } // CABANG END // SETUP START func getOngkirHandler(c *gin.Context) { rows, err := db.Query("SELECT `group`, subgroup, nilaiint as nilai FROM tb_setupall") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch ongkir data", "detail": err}) return } defer rows.Close() dataSetup := make(map[string]int) for rows.Next() { var item Setup if err := rows.Scan(&item.Group, &item.Subgroup, &item.Nilai); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan ongkir data", "detail": err}) return } dataSetup[item.Subgroup] = item.Nilai } // Verify the request (assuming AUTHORIZATION::verify_request is a JWT verification) username := c.GetString("username") if username == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) return } // If verification is successful, return the data c.JSON(http.StatusOK, dataSetup) } // SETUP END // OTP THINGS // sendOTPHandler sends an OTP message to a user via WhatsApp and stores it in the database func sendOTPHandler(c *gin.Context) { // Parse the request body log.Printf("Sending OTP: Start") var request struct { PhoneNumber string `json:"phone_number"` // Recipient's phone number RealPhoneNumber string `json:"real_phone_number"` // Recipient's real phone number (username) } if err := c.ShouldBindJSON(&request); err != nil { log.Printf("Error: Failed to send => Invalid request body") c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) return } // Check if the real_phone_number is already registered as a username var username string err := db.QueryRow("SELECT username FROM tb_customer WHERE username = ?", request.RealPhoneNumber).Scan(&username) if err == nil { // If the username exists, return an error log.Printf("Error: Phone number is already registered as a username") c.JSON(http.StatusBadRequest, gin.H{"error": "Phone number is already registered as a username"}) return } else if err != sql.ErrNoRows { // If there's a database error, log it and return an internal server error log.Printf("Failed to execute SQL: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check user registration"}) return } // Generate a random 6-digit OTP rand.Seed(time.Now().UnixNano()) // Seed the random number generator otp := fmt.Sprintf("%06d", rand.Intn(1000000)) // Generate a 6-digit OTP // Set the OTP expiration time (5 minutes from now) expiresAt := time.Now().Add(5 * time.Minute) // Send the OTP via WhatsApp first err = sendWhatsAppMessage(request.PhoneNumber, fmt.Sprintf("Your OTP is: %s", otp)) if err != nil { // If WhatsApp fails to send, return an error and do not store the OTP log.Printf("Error: Failed to send OTP via WhatsApp %s",err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send OTP via WhatsApp", "details": err}) return } // Log the SQL query with parameters log.Printf("Executing SQL: INSERT INTO tb_otp (phone_number, otp, expires_at) VALUES ('%s', '%s', '%s')", request.PhoneNumber, otp, expiresAt.Format("2006-01-02 15:04:05")) // Execute the SQL query to store the OTP _, err = db.Exec("INSERT INTO tb_otp (phone_number, otp, expires_at) VALUES (?, ?, ?)", request.PhoneNumber, otp, expiresAt) if err != nil { log.Printf("Failed to execute SQL: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to store OTP in database"}) return } // Return success response c.JSON(http.StatusOK, gin.H{"message": "OTP sent successfully!"}) } // sendOTPHandler sends an OTP message to a user via WhatsApp and stores it in the database func sendOTPForgotHandler(c *gin.Context) { // Parse the request body var request struct { PhoneNumber string `json:"phone_number"` // Recipient's phone number RealPhoneNumber string `json:"real_phone_number"` // Recipient's real phone number (username) } if err := c.ShouldBindJSON(&request); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) return } // Generate a random 6-digit OTP rand.Seed(time.Now().UnixNano()) // Seed the random number generator otp := fmt.Sprintf("%06d", rand.Intn(1000000)) // Generate a 6-digit OTP // Set the OTP expiration time (5 minutes from now) expiresAt := time.Now().Add(5 * time.Minute) // Send the OTP via WhatsApp first err := sendWhatsAppMessage(request.PhoneNumber, fmt.Sprintf("Your OTP is: %s", otp)) if err != nil { // If WhatsApp fails to send, return an error and do not store the OTP c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send OTP via WhatsApp", "details": err}) return } // Log the SQL query with parameters log.Printf("Executing SQL: INSERT INTO tb_otp (phone_number, otp, expires_at) VALUES ('%s', '%s', '%s')", request.PhoneNumber, otp, expiresAt.Format("2006-01-02 15:04:05")) // Execute the SQL query to store the OTP _, err = db.Exec("INSERT INTO tb_otp (phone_number, otp, expires_at) VALUES (?, ?, ?)", request.PhoneNumber, otp, expiresAt) if err != nil { log.Printf("Failed to execute SQL: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to store OTP in database"}) return } // Return success response c.JSON(http.StatusOK, gin.H{"message": "OTP sent successfully!"}) } // verifyOTPHandler verifies the OTP provided by the user func verifyOTPHandler(c *gin.Context) { // Parse the request body var request struct { PhoneNumber string `json:"phone_number"` // Recipient's phone number OTP string `json:"otp"` // OTP to verify } if err := c.ShouldBindJSON(&request); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) return } // Fetch the OTP from the database var storedOTP string var expiresAt time.Time // Use time.Time to store the expiration time err := db.QueryRow(` SELECT otp, expires_at FROM tb_otp WHERE phone_number = ? ORDER BY created_at DESC LIMIT 1`, request.PhoneNumber).Scan(&storedOTP, &expiresAt) if err != nil { if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "No OTP found for this phone number"}) } else { // Log the exact error for debugging log.Printf("Database error: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch OTP from database", "details": err.Error()}) } return } // Log the expiration time for debugging log.Printf("Expiration Time: %s", expiresAt.Format("2006-01-02 15:04:05")) // Check if the OTP has expired currentTime := time.Now() if currentTime.After(expiresAt) { c.JSON(http.StatusBadRequest, gin.H{"error": "OTP has expired"}) return } // Verify the OTP if request.OTP != storedOTP { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid OTP"}) return } // Return success response c.JSON(http.StatusOK, gin.H{"message": "OTP verified successfully!"}) } // verifyOTPHandler verifies the OTP provided by the user func verifyForgotOTPHandler(c *gin.Context) { // Parse the request body var request struct { PhoneNumber string `json:"phone_number"` // Recipient's phone number PhoneNumberAsli string `json:"phone_number_asli"` // Recipient's phone number OTP string `json:"otp"` // OTP to verify } if err := c.ShouldBindJSON(&request); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}) return } // Fetch the OTP from the database var storedOTP string var expiresAt time.Time // Use time.Time to store the expiration time err := db.QueryRow(` SELECT otp, expires_at FROM tb_otp WHERE phone_number = ? ORDER BY created_at DESC LIMIT 1`, request.PhoneNumber).Scan(&storedOTP, &expiresAt) if err != nil { if err == sql.ErrNoRows { log.Printf("errornya: ", err) c.JSON(http.StatusNotFound, gin.H{"error": "No OTP found for this phone number"}) } else { // Log the exact error for debugging log.Printf("Database error: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch OTP from database", "details": err.Error()}) } return } // Log the expiration time for debugging log.Printf("Expiration Time: %s", expiresAt.Format("2006-01-02 15:04:05")) // Check if the OTP has expired currentTime := time.Now() if currentTime.After(expiresAt) { c.JSON(http.StatusBadRequest, gin.H{"error": "OTP has expired"}) return } // Verify the OTP if request.OTP != storedOTP { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid OTP"}) return } var customer Customer err = db.QueryRow("SELECT username,password, nama, alamat,kecamatan,no_telp,email FROM tb_customer WHERE username = ?", request.PhoneNumberAsli).Scan(&customer.Username, &customer.Password, &customer.Name, &customer.Address, &customer.District, &customer.Phone, &customer.Email) if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "Invalid username or password"}) return } token, err := generateToken(customer.Username) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) return } c.JSON(http.StatusOK, gin.H{"token": token, "data": customer, "status": http.StatusOK}) } // sendWhatsAppMessage sends a message via WhatsApp // Updated sendWhatsAppMessage function using the working code pattern func sendWhatsAppMessage(phoneNumber, message string) error { client, err := getWhatsAppClient() if err != nil { return fmt.Errorf("failed to get WhatsApp client: %v", err) } if !client.IsLoggedIn() { return errors.New("WhatsApp client is not logged in") } // Normalize phone number normalizedTo, err := normalizePhoneNumber(phoneNumber) if err != nil { return fmt.Errorf("failed to normalize phone number: %v", err) } // Force reconnect to ensure fresh session if client.IsConnected() { log.Println("Disconnecting to ensure fresh session for send") client.Disconnect() time.Sleep(1 * time.Second) } if err := client.Connect(); err != nil { return fmt.Errorf("reconnect failed: %v", err) } // Parse JID with normalized number jid := types.NewJID(normalizedTo, types.DefaultUserServer) msg := &waE2E.Message{ Conversation: &message, } // Use dedicated timeout context (120s) and retry logic const maxRetries = 3 const retryDelay = 5 * time.Second var sendErr error for attempt := 1; attempt <= maxRetries; attempt++ { ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) _, err := client.SendMessage(ctx, jid, msg) cancel() if err == nil { log.Printf("Message sent successfully to %s", normalizedTo) return nil } sendErr = err log.Printf("Send attempt %d failed: %v", attempt, err) if attempt < maxRetries { time.Sleep(retryDelay) } } return fmt.Errorf("send failed after retries: %v", sendErr) } // WhatsApp handlers from working code func whatsappLoginHandler(c *gin.Context) { client, err := getWhatsAppClient() if err != nil { c.JSON(http.StatusInternalServerError, LoginResponse{Status: "client_error", Error: err.Error()}) return } if client.IsLoggedIn() { c.JSON(http.StatusOK, LoginResponse{Status: "already_logged_in"}) return } // Force disconnect if any stale connection exists if client.IsConnected() { log.Println("Disconnecting existing connection to initiate login") client.Disconnect() time.Sleep(1 * time.Second) } qrChan, _ := client.GetQRChannel(context.Background()) if err := client.Connect(); err != nil { c.JSON(http.StatusInternalServerError, LoginResponse{Status: "connect_failed", Error: err.Error()}) return } select { case evt := <-qrChan: if evt.Event == "code" { // Generate ASCII QR code asciiQR, err := generateASCIIQR(evt.Code) if err != nil { log.Println("Failed to generate ASCII QR:", err) asciiQR = evt.Code // Fallback to raw text } // Print to terminal fmt.Println("Scan this QR code with WhatsApp (Linked Devices):") fmt.Println(asciiQR) // Return QR code immediately in response c.JSON(http.StatusOK, LoginResponse{Status: "qr_generated", QR: asciiQR}) // Continue waiting for login in background go func() { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() for { select { case evt := <-qrChan: if evt.Event == "error" { log.Println("Login failed:", evt.Error.Error()) return } else if evt.Event == "success" { log.Println("Login successful") return } case <-ctx.Done(): log.Println("Login timed out") return } } }() return } case <-time.After(10 * time.Second): c.JSON(http.StatusGatewayTimeout, LoginResponse{Status: "no_qr_generated"}) } } func whatsappSendHandler(c *gin.Context) { var req SendRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, SendResponse{Status: "error", Error: err.Error()}) return } client, err := getWhatsAppClient() if err != nil { c.JSON(http.StatusInternalServerError, SendResponse{Status: "error", Error: err.Error()}) return } if !client.IsLoggedIn() { c.JSON(http.StatusUnauthorized, SendResponse{Status: "error", Error: "not_logged_in"}) return } // Use the updated sendWhatsAppMessage function err = sendWhatsAppMessage(req.To, req.Message) if err != nil { c.JSON(http.StatusInternalServerError, SendResponse{Status: "error", Error: err.Error()}) return } c.JSON(http.StatusOK, SendResponse{Status: "sent", Message: "Message sent successfully"}) } func whatsappStatusHandler(c *gin.Context) { client, err := getWhatsAppClient() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } connected := client.IsConnected() loggedIn := client.IsLoggedIn() if loggedIn && !connected { // Reconnect if logged in but disconnected log.Println("Reconnecting for status check") _ = client.Connect() time.Sleep(2 * time.Second) connected = client.IsConnected() } else if !loggedIn && connected { // Disconnect stale connection if not logged in log.Println("Disconnecting stale connection") client.Disconnect() connected = false } c.JSON(http.StatusOK, StatusResponse{ Connected: connected, LoggedIn: loggedIn, }) } func whatsappLogoutHandler(c *gin.Context) { client, err := getWhatsAppClient() if err != nil { c.JSON(http.StatusInternalServerError, SendResponse{Status: "error", Error: err.Error()}) return } if client.IsConnected() { client.Disconnect() } if client.Store.ID != nil { if err := client.Store.Delete(context.Background()); err != nil { c.JSON(http.StatusInternalServerError, SendResponse{Status: "error", Error: "logout_failed: " + err.Error()}) return } } c.JSON(http.StatusOK, SendResponse{Status: "logged_out", Message: "Session cleared"}) } // getCurrentTimeHandler returns the current time in Indonesia Time (WIB, UTC+7) func getCurrentTimeHandler(c *gin.Context) { currentTime := time.Now() c.JSON(http.StatusOK, gin.H{ "current_time": currentTime.Format("2006-01-02 15:04:05"), "timezone": "WIB (UTC+7)", }) } func regencyHandler(c *gin.Context) { // Execute the SQL query to retrieve regency data rows, err := db.Query(` SELECT id, province_id, name FROM reg_regencies WHERE aktif = 1 ORDER BY name ASC; `) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } defer rows.Close() var regencies []Kabupaten for rows.Next() { var kabupaten Kabupaten // Scan the row into the Kabupaten struct if err := rows.Scan( &kabupaten.Id, &kabupaten.ProvinceId, &kabupaten.Nama, ); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Append the scanned regency to the slice regencies = append(regencies, kabupaten) } // Check for errors from iterating over rows if err := rows.Err(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Return the regencies as a JSON response c.JSON(http.StatusOK, regencies) } func districtHandler(c *gin.Context) { // Execute the SQL query to retrieve district data id := c.Param("regency_id") rows, err := db.Query(` SELECT id, regency_id, name FROM reg_districts WHERE regency_id = ? ORDER BY name ASC; `, id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } defer rows.Close() var kecamatanList []Kecamatan for rows.Next() { var kecamatan Kecamatan // Scan the row into the Kecamatan struct if err := rows.Scan( &kecamatan.Id, &kecamatan.RegencyId, &kecamatan.Nama, ); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Append the scanned district to the slice kecamatanList = append(kecamatanList, kecamatan) } // Check for errors from iterating over rows if err := rows.Err(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Return the districts as a JSON response c.JSON(http.StatusOK, kecamatanList) } func villageHandler(c *gin.Context) { // Execute the SQL query to retrieve village data id := c.Param("district_id") rows, err := db.Query(` SELECT id, district_id, name FROM reg_villages WHERE district_id = ? ORDER BY name ASC; `, id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } defer rows.Close() var desaList []Desa for rows.Next() { var desa Desa // Scan the row into the Desa struct if err := rows.Scan( &desa.Id, &desa.DistrictId, &desa.Nama, ); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Append the scanned village to the slice desaList = append(desaList, desa) } // Check for errors from iterating over rows if err := rows.Err(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Return the villages as a JSON response c.JSON(http.StatusOK, desaList) } func getAdministrativeData(c *gin.Context) { rows, err := db.Query(` SELECT r.id AS regency_id, r.name AS regency_name, d.id AS district_id, d.name AS district_name, v.id AS village_id, v.name AS village_name FROM reg_regencies r INNER JOIN reg_districts d ON r.id = d.regency_id LEFT JOIN reg_villages v ON d.id = v.district_id WHERE r.aktif = 1 ORDER BY r.name, d.name, v.name `) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } defer rows.Close() type Village struct { Id int `json:"id"` Name string `json:"name"` } type District struct { Id int `json:"id"` Name string `json:"name"` Villages []Village `json:"villages"` } type Regency struct { Id int `json:"id"` Name string `json:"name"` Districts []District `json:"districts"` } regencies := make(map[int]Regency) for rows.Next() { var regencyId, districtId, villageId int var regencyName, districtName, villageName string err := rows.Scan(®encyId, ®encyName, &districtId, &districtName, &villageId, &villageName) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } regency, ok := regencies[regencyId] if !ok { regency = Regency{Id: regencyId, Name: regencyName, Districts: []District{}} } districtIndex := -1 for i, d := range regency.Districts { if d.Id == districtId { districtIndex = i break } } if districtIndex == -1 { district := District{Id: districtId, Name: districtName, Villages: []Village{}} regency.Districts = append(regency.Districts, district) districtIndex = len(regency.Districts) - 1 } if villageId != 0 { village := Village{Id: villageId, Name: villageName} regency.Districts[districtIndex].Villages = append(regency.Districts[districtIndex].Villages, village) } regencies[regencyId] = regency } if err := rows.Err(); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } regencyList := make([]Regency, 0, len(regencies)) for _, v := range regencies { regencyList = append(regencyList, v) } c.JSON(http.StatusOK, regencyList) } // testWhatsAppHandler sends a test WhatsApp message using existing sendWhatsAppMessage function func testWhatsAppHandler(c *gin.Context) { var request struct { PhoneNumber string `json:"phone_number" binding:"required"` Message string `json:"message" binding:"required"` } if err := c.ShouldBindJSON(&request); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": "Invalid request body", "details": err.Error(), }) return } // Validate phone number if len(request.PhoneNumber) < 8 { c.JSON(http.StatusBadRequest, gin.H{ "error": "Phone number too short", }) return } // Check WhatsApp status first client, err := getWhatsAppClient() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "WhatsApp client error", "details": err.Error(), }) return } if client.Store.ID == nil { c.JSON(http.StatusPreconditionRequired, gin.H{ "error": "WhatsApp not authenticated", "message": "Please scan QR code first using /whatsapp-qr endpoint", }) return } // Send the message err = sendWhatsAppMessage(request.PhoneNumber, request.Message) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to send WhatsApp message", "details": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "WhatsApp message sent successfully", "phone_number": request.PhoneNumber, "sent_at": time.Now().Format("2006-01-02 15:04:05"), }) } func checkWhatsAppStatusHandler(c *gin.Context) { client, err := getWhatsAppClient() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to get WhatsApp client", "details": err.Error(), }) return } status := gin.H{ "client_initialized": true, "connected": client.IsConnected(), "authenticated": client.Store.ID != nil, } if client.Store.ID != nil { status["device_id"] = client.Store.ID.String() status["device_jid"] = client.Store.ID.String() } c.JSON(http.StatusOK, status) } // generateQRHandler creates a QR code in ASCII format for WhatsApp authentication func generateQRHandler(c *gin.Context) { client, err := getWhatsAppClient() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to initialize WhatsApp client", "details": err.Error(), }) return } // Check if already authenticated if client.Store.ID != nil { c.JSON(http.StatusOK, gin.H{ "authenticated": true, "message": "Already authenticated", "device_id": client.Store.ID.String(), }) return } // If client is already connected, disconnect first if client.IsConnected() { client.Disconnect() } // Create context with longer timeout for QR generation ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) defer cancel() // Get QR channel before connecting qrChan, err := client.GetQRChannel(ctx) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to get QR channel", "details": err.Error(), }) return } // Connect the client err = client.Connect() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to connect client", "details": err.Error(), }) return } log.Printf("Waiting for QR code...") // Wait for events for { select { case evt := <-qrChan: switch evt.Event { case "code": log.Printf("Got QR code, generating response...") // Generate ASCII QR code asciiQR, err := generateASCIIQR(evt.Code) if err != nil { log.Printf("Failed to generate ASCII QR: %v", err) asciiQR = "Failed to generate ASCII QR code" } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "Scan this QR code with WhatsApp", "qr_ascii": asciiQR, "qr_data": evt.Code, "timeout": 60, // seconds "instructions": "1. Open WhatsApp on your phone\n2. Go to Settings > Linked Devices\n3. Tap 'Link a Device'\n4. Scan the QR code above", }) return case "success": log.Printf("QR code scanned successfully!") c.JSON(http.StatusOK, gin.H{ "success": true, "authenticated": true, "message": "QR code scanned successfully! Device is now linked.", "device_id": client.Store.ID.String(), }) return case "timeout": log.Printf("QR code timed out") c.JSON(http.StatusRequestTimeout, gin.H{ "success": false, "error": "QR code expired", "message": "The QR code has expired. Please try again.", }) return case "error": log.Printf("QR code error: %v", evt.Error) c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "error": "QR code error", "details": evt.Error.Error(), }) return } case <-ctx.Done(): c.JSON(http.StatusRequestTimeout, gin.H{ "success": false, "error": "Request timeout", "message": "QR code generation timed out. Please try again.", }) return } } } // Alternative simpler ASCII QR generator (if the above doesn't work well) func generateSimpleASCIIQR(data string) (string, error) { qr, err := qrcode.New(data, qrcode.Medium) if err != nil { return "", fmt.Errorf("failed to create QR code: %v", err) } bitmap := qr.Bitmap() var asciiQR strings.Builder for _, row := range bitmap { for _, pixel := range row { if pixel { asciiQR.WriteString("██") // Black pixel } else { asciiQR.WriteString(" ") // White pixel } } asciiQR.WriteString("\n") } return asciiQR.String(), nil }