diff --git a/backend/cmd/app/main.go b/backend/cmd/app/main.go index 467de5b..9a2b7c1 100644 --- a/backend/cmd/app/main.go +++ b/backend/cmd/app/main.go @@ -73,6 +73,8 @@ func main() { mux.HandleFunc("GET /signup", authHandler.SignupView) mux.HandleFunc("POST /v1/loginauth", authHandler.LoginAuthHandler) // Login authentication endpoint mux.HandleFunc("POST /v1/signupauth", authHandler.SignupHandler) + mux.HandleFunc("GET /admin-signup", authHandler.AdminSignupView) + mux.HandleFunc("POST /v1/admin-signup", authHandler.AdminSignupHandler) mux.HandleFunc("POST /v1/signup/check-details", authHandler.CheckDetailsHandler) mux.HandleFunc("POST /v1/signup/check-card", authHandler.CheckCardHandler) mux.HandleFunc("GET /forgot-password", authHandler.ForgotPasswordView) @@ -95,6 +97,8 @@ func main() { mux.HandleFunc("GET /admin/merchants", adminHanlder.MerchantManagementView) mux.HandleFunc("GET /v1/admin/merchants-data", adminHanlder.MerchantManagementDataHandler) mux.HandleFunc("GET /admin/terminals", adminHanlder.TerminalRegistryView) + mux.HandleFunc("GET /v1/admin/terminals-data", adminHanlder.TerminalRegistryDataHandler) + mux.HandleFunc("POST /v1/admin/terminals/add", adminHanlder.AddTerminalHandler) mux.HandleFunc("GET /admin/settings", adminHanlder.SystemSettingsView) mux.HandleFunc("POST /v1/admin/merchants/add", adminHanlder.AddMerchantHandler) mux.HandleFunc("GET /admin/card-inventory", adminHanlder.CardInventoryView) diff --git a/backend/internal/admin/add_merchant.go b/backend/internal/admin/add_merchant.go new file mode 100644 index 0000000..a4bcf18 --- /dev/null +++ b/backend/internal/admin/add_merchant.go @@ -0,0 +1,231 @@ +package admin + +import ( + "crypto/rand" + "encoding/json" + "errors" + "fmt" + "log" + "math/big" + "net/http" + "time" + jsonwrite "unicard-go/backend/internal/pkg/handler" + + "github.com/go-playground/validator/v10" + "golang.org/x/crypto/bcrypt" +) + +// AddMerchantRequest represents the payload for adding a new merchant +type AddMerchantRequest struct { + BusinessName string `json:"businessName" validate:"required" db:"business_name"` + BusinessType string `json:"businessType" validate:"required" db:"business_type"` + RegistrationNum string `json:"registrationNum" validate:"required" db:"registration_num"` + BusinessAddress string `json:"businessAddress" validate:"required" db:"business_address"` + OwnerName string `json:"ownerName" validate:"required" db:"owner_name"` + BusinessEmail string `json:"businessEmail" validate:"required,email" db:"business_email"` + BusinessPhone string `json:"businessPhone" validate:"required" db:"business_phone"` + CommissionRate string `json:"commissionRate" validate:"required" db:"commission_rate"` + SettlementName string `json:"settlementName" validate:"required" db:"settlement_name"` + SettlementAccount string `json:"settlementAccount" validate:"required" db:"settlement_account_number"` + SettlementBank string `json:"settlementBank" validate:"required" db:"settlement_bank_name"` + TerminalSN string `json:"terminalSn" validate:"required" db:"terminal_sn"` + DeviceName string `json:"deviceName" validate:"required" db:"device_name"` +} + +// AddMerchantHandler creates new merchants and their corresponding owner users in bulk +func (h *Handler) AddMerchantHandler(w http.ResponseWriter, r *http.Request) { + var reqs []AddMerchantRequest + if err := json.NewDecoder(r.Body).Decode(&reqs); err != nil { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Invalid JSON payload format. Expected array of merchants.", + }) + return + } + + if len(reqs) == 0 { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "No merchants provided.", + }) + return + } + + tx, err := h.DB.Begin() + if err != nil { + log.Printf("Error starting tx: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Database error", + }) + return + } + + // Prepare statements + userStmt, err := tx.Prepare(`INSERT INTO users (user_id, username, name, email, phone_number, password_hash, role, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`) + if err != nil { + tx.Rollback() + log.Printf("Error preparing user stmt: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Database error", + }) + return + } + defer userStmt.Close() + + merchStmt, err := tx.Prepare(`INSERT INTO merchants ( + merchant_id, business_name, business_type, business_registration_number, business_address, + owner_user_id, owner_name, business_email, business_phone, commission_rate, + settlement_account_name, settlement_account_number, settlement_bank_name, status + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) + if err != nil { + tx.Rollback() + log.Printf("Error preparing merchant stmt: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Database error", + }) + return + } + defer merchStmt.Close() + + termStmt, err := tx.Prepare(`INSERT INTO terminals ( + terminal_id, terminal_sn, merchant_id, device_name, status + ) VALUES (?, ?, ?, ?, ?)`) + if err != nil { + tx.Rollback() + log.Printf("Error preparing terminal stmt: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Database error", + }) + return + } + defer termStmt.Close() + + for i, req := range reqs { + err := Validate.Struct(req) + if err != nil { + tx.Rollback() + log.Printf("Validation error on merchant %d: %v", i+1, err) + var validationErrs validator.ValidationErrors + msg := fmt.Sprintf("Validation failed on merchant #%d", i+1) + if errors.As(err, &validationErrs) { + firstErr := validationErrs[0] + fieldMessages := map[string]string{ + "BusinessName": "Business name is required", + "BusinessType": "Business type is required", + "RegistrationNum": "Registration number is required", + "BusinessAddress": "Business address is required", + "OwnerName": "Owner name is required", + "BusinessEmail": "A valid business email is required", + "BusinessPhone": "Business phone number is required", + "CommissionRate": "Commission rate is required", + "SettlementName": "Settlement name is required", + "SettlementAccount": "Settlement account number is required", + "SettlementBank": "Settlement bank name is required", + "TerminalSN": "Terminal serial number is required", + "DeviceName": "Device name is required", + } + if customMsg, ok := fieldMessages[firstErr.Field()]; ok { + msg = fmt.Sprintf("Merchant #%d: %s", i+1, customMsg) + } + } + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: msg, + }) + return + } + + // Generate IDs (Format: YYMMminsecxxxxx where xxxxx is 5 random numbers) + timestamp := time.Now().Format("06010405") // YYMMDDHH + + nUser, _ := rand.Int(rand.Reader, big.NewInt(100000)) + userID := fmt.Sprintf("UNI-%s%04d", timestamp, nUser.Int64()) + + nMerchant, _ := rand.Int(rand.Reader, big.NewInt(100000)) + merchantID := fmt.Sprintf("MCH-%s%04d", timestamp, nMerchant.Int64()) + + // Create user for the merchant owner + hashedPassword, err := bcrypt.GenerateFromPassword([]byte("TempPass123!"), bcrypt.DefaultCost) + if err != nil { + tx.Rollback() + log.Printf("Error hashing password: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Failed to secure user credentials", + }) + return + } + + // Using business email as username for simplicity + username := req.BusinessEmail + _, err = userStmt.Exec(userID, username, req.OwnerName, req.BusinessEmail, req.BusinessPhone, string(hashedPassword), "merchant_admin", "active") + if err != nil { + tx.Rollback() + log.Printf("Error creating user %d: %v", i+1, err) + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: fmt.Sprintf("Failed to create user account for Merchant #%d (email or phone might already exist)", i+1), + }) + return + } + + res, err := merchStmt.Exec( + merchantID, req.BusinessName, req.BusinessType, req.RegistrationNum, req.BusinessAddress, + userID, req.OwnerName, req.BusinessEmail, req.BusinessPhone, req.CommissionRate, + req.SettlementName, req.SettlementAccount, req.SettlementBank, "active", + ) + + if err != nil { + tx.Rollback() + log.Printf("Error creating merchant %d: %v", i+1, err) + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: fmt.Sprintf("Failed to create profile for Merchant #%d (registration num or email might exist)", i+1), + }) + return + } + + internalMerchantID, err := res.LastInsertId() + if err != nil { + tx.Rollback() + log.Printf("Error getting last insert ID for merchant %d: %v", i+1, err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Database error", + }) + return + } + + nTerminal, _ := rand.Int(rand.Reader, big.NewInt(100000)) + terminalID := fmt.Sprintf("TRM-%s%04d", timestamp, nTerminal.Int64()) + + _, err = termStmt.Exec(terminalID, req.TerminalSN, internalMerchantID, req.DeviceName, "active") + if err != nil { + tx.Rollback() + log.Printf("Error creating terminal %d: %v", i+1, err) + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: fmt.Sprintf("Failed to register terminal for Merchant #%d (serial number might exist)", i+1), + }) + return + } + } + + if err := tx.Commit(); err != nil { + log.Printf("Error committing tx: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Failed to finalize batch creation", + }) + return + } + + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: true, + Message: fmt.Sprintf("Successfully onboarded %d merchant(s)", len(reqs)), + }) +} diff --git a/backend/internal/admin/admin_dashboard.go b/backend/internal/admin/admin_dashboard.go index b89f7cd..2980524 100644 --- a/backend/internal/admin/admin_dashboard.go +++ b/backend/internal/admin/admin_dashboard.go @@ -117,8 +117,8 @@ func (h *Handler) AdminDashboardDataHandler(w http.ResponseWriter, r *http.Reque } log.Println("Total terminals row:", totalTerminals) - // Fetch all merchants for the table - merchantQuery := "SELECT merchant_id, business_name, business_type, owner_name, business_email, business_phone, status, created_at FROM merchants" + // Fetch recent merchants for the table (limit 5) + merchantQuery := "SELECT merchant_id, business_name, business_type, owner_name, business_email, business_phone, status, created_at FROM merchants ORDER BY created_at DESC LIMIT 5" rows, err := h.DB.Query(merchantQuery) if err != nil { log.Println("Error querying merchants:", err) diff --git a/backend/internal/admin/admin_merchant.go b/backend/internal/admin/admin_merchant.go index 456f913..87d93de 100644 --- a/backend/internal/admin/admin_merchant.go +++ b/backend/internal/admin/admin_merchant.go @@ -1,8 +1,11 @@ package admin import ( + "fmt" "log" "net/http" + "strconv" + "strings" jsonwrite "unicard-go/backend/internal/pkg/handler" structs "unicard-go/backend/internal/pkg/structs" ) @@ -15,10 +18,63 @@ func (h *Handler) MerchantManagementView(w http.ResponseWriter, r *http.Request) func (h *Handler) MerchantManagementDataHandler(w http.ResponseWriter, r *http.Request) { log.Println("MerchantManagementDataHandler running...") - //var dashboardData structure.AdminDashboardData + // Parse pagination parameters + pageStr := r.URL.Query().Get("page") + limitStr := r.URL.Query().Get("limit") + search := r.URL.Query().Get("search") + sortOrder := r.URL.Query().Get("sort") // desc or asc - query := `SELECT merchant_id, business_name, business_type, owner_name, business_email, business_phone, status, created_at FROM merchants` - rows, err := h.DB.Query(query) + page := 1 + limit := 10 + if p, err := strconv.Atoi(pageStr); err == nil && p > 0 { + page = p + } + if l, err := strconv.Atoi(limitStr); err == nil && l > 0 { + limit = l + } + + offset := (page - 1) * limit + + // Build query + baseQuery := `FROM merchants` + var args []interface{} + var conditions []string + + if search != "" { + conditions = append(conditions, `(business_name LIKE ? OR owner_name LIKE ? OR merchant_id LIKE ?)`) + searchPattern := "%" + search + "%" + args = append(args, searchPattern, searchPattern, searchPattern) + } + + whereClause := "" + if len(conditions) > 0 { + whereClause = " WHERE " + strings.Join(conditions, " AND ") + } + + // Count total items + countQuery := `SELECT COUNT(*) ` + baseQuery + whereClause + var totalItems int + if err := h.DB.QueryRow(countQuery, args...).Scan(&totalItems); err != nil { + log.Println("Error counting merchants:", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Error counting merchants", + }) + return + } + + // Get paginated data + orderClause := " ORDER BY created_at DESC" + if strings.ToLower(sortOrder) == "asc" { + orderClause = " ORDER BY created_at ASC" + } + + query := `SELECT merchant_id, business_name, business_type, owner_name, business_email, business_phone, status, created_at ` + + baseQuery + whereClause + orderClause + ` LIMIT ? OFFSET ?` + + args = append(args, limit, offset) + + rows, err := h.DB.Query(query, args...) if err != nil { log.Println("Error querying merchants:", err) jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ @@ -38,9 +94,65 @@ func (h *Handler) MerchantManagementDataHandler(w http.ResponseWriter, r *http.R } merchants = append(merchants, m) } + if merchants == nil { + merchants = []structs.Merchant{} + } else { + // Fetch terminals for these merchants + var merchantIDs []string + for _, m := range merchants { + merchantIDs = append(merchantIDs, m.MerchantID) + } + + placeholders := make([]string, len(merchantIDs)) + termArgs := make([]interface{}, len(merchantIDs)) + for i, id := range merchantIDs { + placeholders[i] = "?" + termArgs[i] = id + } + + termQuery := fmt.Sprintf(` + SELECT m.merchant_id, t.terminal_id, t.terminal_sn, t.device_name, t.status + FROM terminals t + JOIN merchants m ON t.merchant_id = m.id + WHERE m.merchant_id IN (%s)`, strings.Join(placeholders, ",")) + + termRows, err := h.DB.Query(termQuery, termArgs...) + if err == nil { + defer termRows.Close() + termMap := make(map[string][]structs.Terminal) + for termRows.Next() { + var mID string + var t structs.Terminal + if err := termRows.Scan(&mID, &t.TerminalID, &t.TerminalSN, &t.DeviceName, &t.Status); err == nil { + termMap[mID] = append(termMap[mID], t) + } + } + for i := range merchants { + merchants[i].Terminals = termMap[merchants[i].MerchantID] + if merchants[i].Terminals == nil { + merchants[i].Terminals = []structs.Terminal{} + } + } + } else { + log.Println("Error fetching terminals for merchants:", err) + for i := range merchants { + merchants[i].Terminals = []structs.Terminal{} + } + } + } + + type PaginatedMerchantResponse struct { + Merchants []structs.Merchant `json:"merchants"` + TotalItems int `json:"totalItems"` + Page int `json:"page"` + Limit int `json:"limit"` + } - merchantData := structs.AdminDashboardData{ - Merchants: merchants, + merchantData := PaginatedMerchantResponse{ + Merchants: merchants, + TotalItems: totalItems, + Page: page, + Limit: limit, } jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ diff --git a/backend/internal/admin/admin_terminal.go b/backend/internal/admin/admin_terminal.go new file mode 100644 index 0000000..c7ab7b7 --- /dev/null +++ b/backend/internal/admin/admin_terminal.go @@ -0,0 +1,168 @@ +package admin + +import ( + "crypto/rand" + "encoding/json" + "fmt" + "log" + "math/big" + "net/http" + "strconv" + "strings" + "time" + jsonwrite "unicard-go/backend/internal/pkg/handler" + structs "unicard-go/backend/internal/pkg/structs" +) + +func (h *Handler) TerminalRegistryView(w http.ResponseWriter, r *http.Request) { + log.Println("TerminalRegistryView running...") + h.Tpl.ExecuteTemplate(w, "admin_terminal.html", nil) +} + +func (h *Handler) TerminalRegistryDataHandler(w http.ResponseWriter, r *http.Request) { + log.Println("TerminalRegistryDataHandler running...") + + // Parse pagination parameters + pageStr := r.URL.Query().Get("page") + limitStr := r.URL.Query().Get("limit") + search := r.URL.Query().Get("search") + + page := 1 + limit := 10 + if p, err := strconv.Atoi(pageStr); err == nil && p > 0 { + page = p + } + if l, err := strconv.Atoi(limitStr); err == nil && l > 0 { + limit = l + } + + offset := (page - 1) * limit + + // Build query + baseQuery := `FROM terminals t LEFT JOIN merchants m ON t.merchant_id = m.id` + var args []interface{} + var conditions []string + + if search != "" { + conditions = append(conditions, `(t.terminal_id LIKE ? OR t.terminal_sn LIKE ? OR m.business_name LIKE ?)`) + searchPattern := "%" + search + "%" + args = append(args, searchPattern, searchPattern, searchPattern) + } + + whereClause := "" + if len(conditions) > 0 { + whereClause = " WHERE " + strings.Join(conditions, " AND ") + } + + // Count total items + countQuery := `SELECT COUNT(*) ` + baseQuery + whereClause + var totalItems int + if err := h.DB.QueryRow(countQuery, args...).Scan(&totalItems); err != nil { + log.Println("Error counting terminals:", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Error counting terminals", + }) + return + } + + // Get paginated data + orderClause := " ORDER BY t.created_at DESC" + query := `SELECT t.terminal_id, t.terminal_sn, COALESCE(m.business_name, 'Unassigned / Inventory'), t.device_name, t.status ` + + baseQuery + whereClause + orderClause + ` LIMIT ? OFFSET ?` + + args = append(args, limit, offset) + + rows, err := h.DB.Query(query, args...) + if err != nil { + log.Println("Error querying terminals:", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Error querying terminals", + }) + return + } + defer rows.Close() + + var terminals []structs.Terminal + for rows.Next() { + var t structs.Terminal + if err := rows.Scan(&t.TerminalID, &t.TerminalSN, &t.AssignedMerch, &t.DeviceName, &t.Status); err != nil { + log.Println("Error scanning terminal:", err) + continue + } + terminals = append(terminals, t) + } + if terminals == nil { + terminals = []structs.Terminal{} + } + + type PaginatedTerminalResponse struct { + Terminals []structs.Terminal `json:"terminals"` + TotalItems int `json:"totalItems"` + Page int `json:"page"` + Limit int `json:"limit"` + } + + terminalData := PaginatedTerminalResponse{ + Terminals: terminals, + TotalItems: totalItems, + Page: page, + Limit: limit, + } + + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: true, + Message: "Terminals retrieved successfully", + Data: terminalData, + }) + log.Println("TerminalRegistryDataHandler finished") +} + +// AddTerminalRequest payload +type AddTerminalRequest struct { + TerminalSN string `json:"terminalSn" validate:"required"` + DeviceName string `json:"deviceName" validate:"required"` +} + +// AddTerminalHandler registers a new standalone terminal +func (h *Handler) AddTerminalHandler(w http.ResponseWriter, r *http.Request) { + var req AddTerminalRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Invalid JSON payload", + }) + return + } + + if req.TerminalSN == "" || req.DeviceName == "" { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Terminal SN and Device Name are required", + }) + return + } + + // Generate a unique terminal ID + timestamp := time.Now().Format("06010405") + nTerminal, _ := rand.Int(rand.Reader, big.NewInt(100000)) + terminalID := fmt.Sprintf("TRM-%s%04d", timestamp, nTerminal.Int64()) + + // Insert into DB with NULL merchant_id + query := `INSERT INTO terminals (terminal_id, terminal_sn, merchant_id, device_name, status) VALUES (?, ?, NULL, ?, 'active')` + _, err := h.DB.Exec(query, terminalID, req.TerminalSN, req.DeviceName) + if err != nil { + log.Printf("Error inserting standalone terminal: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Failed to register terminal (SN might already exist)", + }) + return + } + + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: true, + Message: "Terminal registered to inventory successfully", + }) +} diff --git a/backend/internal/admin/super_admin_pages.go b/backend/internal/admin/super_admin_pages.go index 4e839b6..f8b7b45 100644 --- a/backend/internal/admin/super_admin_pages.go +++ b/backend/internal/admin/super_admin_pages.go @@ -1,35 +1,13 @@ package admin import ( - "crypto/rand" - "encoding/json" - "errors" - "fmt" "log" - "math/big" "net/http" - "time" jsonwrite "unicard-go/backend/internal/pkg/handler" "github.com/go-playground/validator/v10" - "golang.org/x/crypto/bcrypt" ) -// AddMerchantRequest represents the payload for adding a new merchant -type AddMerchantRequest struct { - BusinessName string `json:"businessName" validate:"required" db:"business_name"` - BusinessType string `json:"businessType" validate:"required" db:"business_type"` - RegistrationNum string `json:"registrationNum" validate:"required" db:"registration_num"` - BusinessAddress string `json:"businessAddress" validate:"required" db:"business_address"` - OwnerName string `json:"ownerName" validate:"required" db:"owner_name"` - BusinessEmail string `json:"businessEmail" validate:"required,email" db:"business_email"` - BusinessPhone string `json:"businessPhone" validate:"required" db:"business_phone"` - CommissionRate string `json:"commissionRate" validate:"required" db:"commission_rate"` - SettlementName string `json:"settlementName" validate:"required" db:"settlement_name"` - SettlementAccount string `json:"settlementAccount" validate:"required" db:"settlement_account_number"` - SettlementBank string `json:"settlementBank" validate:"required" db:"settlement_bank_name"` -} - var Validate = validator.New() // PlatformOverviewView serves the new Super Admin Platform Overview @@ -57,7 +35,7 @@ func (h *Handler) MerchantManagementViews(w http.ResponseWriter, r *http.Request } // TerminalRegistryView serves the Hardware Registry page -func (h *Handler) TerminalRegistryView(w http.ResponseWriter, r *http.Request) { +func (h *Handler) TerminalRegistryViews(w http.ResponseWriter, r *http.Request) { err := h.Tpl.ExecuteTemplate(w, "hardware_registry.html", nil) if err != nil { log.Printf("Template execution error: %v", err) @@ -79,138 +57,3 @@ func (h *Handler) SystemSettingsView(w http.ResponseWriter, r *http.Request) { }) } } - -// AddMerchantHandler creates a new merchant and its corresponding owner user -func (h *Handler) AddMerchantHandler(w http.ResponseWriter, r *http.Request) { - var req AddMerchantRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ - Success: false, - Message: "Invalid JSON payload", - }) - return - } - - err := Validate.Struct(req) - if err != nil { - log.Printf("Validation error: %v", err) - - var validationErrs validator.ValidationErrors - if errors.As(err, &validationErrs) { - firstErr := validationErrs[0] - - fieldMessages := map[string]string{ - "BusinessName": "Business name is required", - "BusinessType": "Business type is required", - "RegistrationNum": "Registration number is required", - "BusinessAddress": "Business address is required", - "OwnerName": "Owner name is required", - "BusinessEmail": "A valid business email is required", - "BusinessPhone": "Business phone number is required", - "CommissionRate": "Commission rate is required", - "SettlementName": "Settlement name is required", - "SettlementAccount": "Settlement account number is required", - "SettlementBank": "Settlement bank name is required", - } - - msg := "Validation failed on field: " + firstErr.Field() - if customMsg, ok := fieldMessages[firstErr.Field()]; ok { - msg = customMsg - } - - jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ - Success: false, - Message: msg, - }) - return - } - - jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ - Success: false, - Message: "Validation error", - }) - return - } - - // Generate IDs (Format: YYMMminsecxxxxx where xxxxx is 5 random numbers) - timestamp := time.Now().Format("06010405") // YYMMDDHH - - nUser, _ := rand.Int(rand.Reader, big.NewInt(100000)) - userID := fmt.Sprintf("UNI-%s%04d", timestamp, nUser.Int64()) - - nMerchant, _ := rand.Int(rand.Reader, big.NewInt(100000)) - merchantID := fmt.Sprintf("MCH-%s%04d", timestamp, nMerchant.Int64()) - - // Create user for the merchant owner - hashedPassword, err := bcrypt.GenerateFromPassword([]byte("TempPass123!"), bcrypt.DefaultCost) - if err != nil { - log.Printf("Error hashing password: %v", err) - jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ - Success: false, - Message: "Failed to secure user credentials", - }) - return - } - - tx, err := h.DB.Begin() - if err != nil { - log.Printf("Error starting tx: %v", err) - jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ - Success: false, - Message: "Database error", - }) - return - } - - // Insert User - userStmt := `INSERT INTO users (user_id, username, name, email, phone_number, password_hash, role, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` - // Using business email as username for simplicity, you could use a dedicated username field - username := req.BusinessEmail - _, err = tx.Exec(userStmt, userID, username, req.OwnerName, req.BusinessEmail, req.BusinessPhone, string(hashedPassword), "merchant_admin", "active") - if err != nil { - tx.Rollback() - log.Printf("Error creating user: %v", err) - jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ - Success: false, - Message: "Failed to create user account (email or phone might already exist)", - }) - return - } - - // Insert Merchant - merchStmt := `INSERT INTO merchants ( - merchant_id, business_name, business_type, business_registration_number, business_address, - owner_user_id, owner_name, business_email, business_phone, commission_rate, - settlement_account_name, settlement_account_number, settlement_bank_name, status - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` - - _, err = tx.Exec(merchStmt, - merchantID, req.BusinessName, req.BusinessType, req.RegistrationNum, req.BusinessAddress, - userID, req.OwnerName, req.BusinessEmail, req.BusinessPhone, req.CommissionRate, - req.SettlementName, req.SettlementAccount, req.SettlementBank, "active", - ) - - if err != nil { - tx.Rollback() - log.Printf("Error creating merchant: %v", err) - jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ - Success: false, - Message: "Failed to create merchant profile (registration num or email might exist)", - }) - return - } - - if err := tx.Commit(); err != nil { - log.Printf("Error committing tx: %v", err) - jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ - Success: false, - Message: "Failed to finalize creation", - }) - return - } - - jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ - Success: true, - Message: "Merchant onboarded successfully", - }) -} diff --git a/backend/internal/auth/admin_signup.go b/backend/internal/auth/admin_signup.go new file mode 100644 index 0000000..5b995b3 --- /dev/null +++ b/backend/internal/auth/admin_signup.go @@ -0,0 +1,89 @@ +package authentication + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "strings" + "unicard-go/backend/internal/pkg/account" + jsonwrite "unicard-go/backend/internal/pkg/handler" +) + +type AdminSignupRequest struct { + Name string `json:"name"` + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` +} + +func (h *Handler) AdminSignupView(w http.ResponseWriter, r *http.Request) { + log.Printf("Admin Signup view is running...") + h.Tpl.ExecuteTemplate(w, "admin_signup.html", nil) +} + +func (h *Handler) AdminSignupHandler(w http.ResponseWriter, r *http.Request) { + log.Printf("Admin Signup Handler is running...") + + ctx := r.Context() + + var req AdminSignupRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Invalid request format", + }) + return + } + + req.Name = strings.TrimSpace(req.Name) + req.Username = strings.TrimSpace(req.Username) + req.Email = strings.TrimSpace(req.Email) + req.Password = strings.TrimSpace(req.Password) + + if req.Name == "" || req.Username == "" || req.Email == "" || req.Password == "" { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "All fields are required", + }) + return + } + + hashedPassword, err := account.HashPassword(req.Password) + if err != nil { + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Error processing password", + }) + return + } + + generateUserId, err := h.GenerateUserID() + if err != nil { + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Error generating User ID", + }) + return + } + userIDStr := fmt.Sprintf("%d", generateUserId) + + insertQuery := `INSERT INTO users + (user_id, username, name, email, password_hash, role, status) + VALUES (?, ?, ?, ?, ?, 'super_admin', 'active')` + + _, err = h.DB.ExecContext(ctx, insertQuery, userIDStr, req.Username, req.Name, req.Email, hashedPassword) + if err != nil { + log.Printf("Error creating admin user: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Database error creating account. Email or Username might be taken.", + }) + return + } + + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: true, + Message: "Super Admin account created successfully!", + }) +} diff --git a/backend/internal/auth/dashboard.go b/backend/internal/auth/dashboard.go index f540620..73f5e93 100644 --- a/backend/internal/auth/dashboard.go +++ b/backend/internal/auth/dashboard.go @@ -3,29 +3,9 @@ package authentication import ( "fmt" "net/http" + "unicard-go/backend/internal/pkg/structs" ) -// Transaction struct represents a user's transaction for the dashboard view -type Transaction struct { - Date string `db:"date" json:"date"` - Description string `db:"description" json:"description"` - Type string `db:"transaction_type" json:"type"` - Amount float64 `db:"transaction_amount" json:"amount"` -} - -// DashboardUser info struct for the user dashboard view -type DashboardUser struct { - ID int `db:"id" json:"id,omitempty"` - UserID string `db:"user_id" json:"user_id,omitempty"` - Username string `db:"username" json:"username"` - Name string `db:"name" json:"name"` - Transaction string `db:"transaction" json:"transaction"` - Balance float64 `db:"balance" json:"balance"` - LoyaltyPoints int `db:"loyalty_points" json:"loyalty_points"` - AccountType string `db:"account_type" json:"account_type"` - RecentTransactions []Transaction `json:"recent_transactions"` // Add recent transactions to the dashboard response -} - func (h *Handler) DashboardView(w http.ResponseWriter, r *http.Request) { fmt.Println("Dashboard view is running...") h.Tpl.ExecuteTemplate(w, "dashboard.html", nil) @@ -34,21 +14,39 @@ func (h *Handler) DashboardView(w http.ResponseWriter, r *http.Request) { func (h *Handler) DashboardHandler(w http.ResponseWriter, r *http.Request) { fmt.Println("Dashboard handler is running...") -// For demonstration, we'll create a dummy user with some transactions - dashboardUser := DashboardUser{ - ID: 1, - UserID: "user123", - Username: "johndoe", - Name: "John Doe", - Transaction: "Transaction", - Balance: 150.75, - LoyaltyPoints: 200, - AccountType: "Premium", - RecentTransactions: []Transaction{ - {Date: "2024-06-01", Description: "Grocery Store", Type: "Debit", Amount: 50.25}, - {Date: "2024-06-03", Description: "Salary", Type: "Credit", Amount: 2000.00}, - {Date: "2024-06-05", Description: "Online Shopping", Type: "Debit", Amount: 75.50}, - }, + // Select user and card details from database + query := ` + SELECT + u.id, + u.user_id, + u.username, + u.name, + COALESCE(c.balance, 0), + COALESCE(c.loyalty_points, 0), + COALESCE(c.card_type, 'Regular') + FROM users u + LEFT JOIN cards c ON u.user_id = c.user_id + WHERE u.user_id = ? + ` + rows, err := h.DB.Query(query, r.URL.Query().Get("user_id")) + if err != nil { + fmt.Println(err) + return } - h.Tpl.ExecuteTemplate(w, "dashboard.html", dashboardUser) -} + defer rows.Close() + + var user structure.DashboardUser + if rows.Next() { + if err := rows.Scan(&user.ID, &user.UserID, &user.Username, &user.Name, &user.Balance, &user.LoyaltyPoints, &user.AccountType); err != nil { + fmt.Println(err) + return + } + } + + if user.ID == 0 { + fmt.Println("User not found") + return + } + + h.Tpl.ExecuteTemplate(w, "dashboard.html", user) +} \ No newline at end of file diff --git a/backend/internal/auth/login.go b/backend/internal/auth/login.go index 3a328d7..39475c4 100644 --- a/backend/internal/auth/login.go +++ b/backend/internal/auth/login.go @@ -75,14 +75,14 @@ func (h *Handler) LoginAuthHandler(w http.ResponseWriter, r *http.Request) { } var ( - hash string // Store the password hash from the database - ID string // Store the ID - userID string // Store the user ID for successful login response - role string // Store the role + hash string // Store the password hash from the database + ID string // Store the ID + userName string // Store the user ID for successful login response + role string // Store the role ) - stmt := "SELECT id, user_id, password_hash, role FROM users WHERE email = ? OR username = ? OR phone_number = ?" - err = h.DB.QueryRow(stmt, loginReq.Identifier, loginReq.Identifier, loginReq.Identifier).Scan(&ID, &userID, &hash, &role) + stmt := "SELECT id, username, password_hash, role FROM users WHERE email = ? OR username = ? OR phone_number = ?" + err = h.DB.QueryRow(stmt, loginReq.Identifier, loginReq.Identifier, loginReq.Identifier).Scan(&ID, &userName, &hash, &role) // User not found if err != nil { @@ -106,10 +106,11 @@ func (h *Handler) LoginAuthHandler(w http.ResponseWriter, r *http.Request) { } // Determine redirect based on role - redirectURL := "/dashboard" // Default for customer - if role == "super_admin" { + redirectURL := "/dashboard?user=" + userName // Default for customer + switch role { + case "super_admin": redirectURL = "/admin/platform-overview" // Super admin dashboard - } else if role == "merchant_admin" || role == "merchant_staff" { + case "merchant_admin", "merchant_staff": redirectURL = "/merchant/dashboard" // Merchant dashboard } @@ -119,7 +120,7 @@ func (h *Handler) LoginAuthHandler(w http.ResponseWriter, r *http.Request) { Success: true, Message: "Login successful", ID: ID, - UserID: userID, + Username: userName, RedirectURL: redirectURL, }) } diff --git a/backend/internal/pkg/handler/jsonWrite.go b/backend/internal/pkg/handler/jsonWrite.go index 1498534..bf2414b 100644 --- a/backend/internal/pkg/handler/jsonWrite.go +++ b/backend/internal/pkg/handler/jsonWrite.go @@ -10,21 +10,21 @@ type APIResponse struct { Success bool `json:"success"` Message string `json:"message"` Field string `json:"field,omitempty"` - Data any `json:"data,omitempty"` // Optional field for additional data + Data any `json:"data,omitempty"` // Optional field for additional data } // Login specific response — returns user data after login type LoginResponse struct { - Success bool `json:"success"` - Message string `json:"message"` - ID string `json:"id,omitempty"` // Optional: include user ID in response - UserID string `json:"userid,omitempty"` // Optional: include user ID in response - RedirectURL string `json:"redirect_url,omitempty"` + Success bool `json:"success"` + Message string `json:"message"` + ID string `json:"id,omitempty"` // Optional: include user ID in response + Username string `json:"username"` // Optional: include user ID in response + RedirectURL string `json:"redirect_url,omitempty"` } // Auth Handler (POST) - Converted to JSON API func WriteJSON(w http.ResponseWriter, status int, resp any) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - json.NewEncoder(w).Encode(resp) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(resp) } diff --git a/backend/internal/pkg/structs/struct.go b/backend/internal/pkg/structs/struct.go index 6405c25..5ee8f8c 100644 --- a/backend/internal/pkg/structs/struct.go +++ b/backend/internal/pkg/structs/struct.go @@ -3,23 +3,24 @@ package structure // CardData struct represents the data required to create a new card type CardData struct { CardUID string `json:"card_uid" db:"card_uid" validate:"required"` - CardNumber string `json:"cardNumber" db:"card_number" validate:"required"` - CardHolder string `json:"cardHolder" db:"user_id" validate:"required"` - CardType string `json:"cardType" db:"card_type" validate:"required"` + CardNumber string `json:"cardNumber" db:"card_number"` + CardHolder string `json:"cardHolder" db:"user_id"` + CardType string `json:"cardType" db:"card_type"` Balance float64 `json:"initial_amount" db:"balance" validate:"required,min=0"` } // List of all merchants // Merchant represents a single business tenant to be displayed in the data table type Merchant struct { - MerchantID string `json:"merchant_id"` - BusinessName string `json:"business_name"` - BusinessType string `json:"business_type"` - OwnerName string `json:"owner_name"` - Email string `json:"business_email"` - Phone string `json:"business_phone"` - Status string `json:"status"` - CreatedAt string `json:"created_at"` + MerchantID string `json:"merchant_id"` + BusinessName string `json:"business_name"` + BusinessType string `json:"business_type"` + OwnerName string `json:"owner_name"` + Email string `json:"business_email"` + Phone string `json:"business_phone"` + Status string `json:"status"` + CreatedAt string `json:"created_at"` + Terminals []Terminal `json:"terminals,omitempty"` } // AdminDashboardData struct represents the data to be displayed on the admin dashboard @@ -31,4 +32,33 @@ type AdminDashboardData struct { ActiveMerchants int `json:"activeMerchants"` ActiveTerminals int `json:"activeTerminals"` Merchants []Merchant `json:"merchants"` +} + +// Terminal represents a hardware device in the terminal registry +type Terminal struct { + TerminalID string `json:"terminal_id"` + TerminalSN string `json:"terminal_sn"` + AssignedMerch string `json:"assigned_merchant"` + DeviceName string `json:"device_name"` + Status string `json:"status"` +} + +// Transaction struct represents a user's transaction for the dashboard view +type Transaction struct { + Date string `db:"date" json:"date"` + Description string `db:"description" json:"description"` + Type string `db:"transaction_type" json:"type"` + Amount float64 `db:"transaction_amount" json:"amount"` +} + +// DashboardUser info struct for the user dashboard view +type DashboardUser struct { + ID int `db:"id" json:"id,omitempty"` + UserID string `db:"user_id" json:"user_id,omitempty"` + Username string `db:"username" json:"username"` + Name string `db:"name" json:"name"` + Balance float64 `db:"balance" json:"balance"` + LoyaltyPoints int `db:"loyalty_points" json:"loyalty_points"` + AccountType string `db:"account_type" json:"account_type"` + RecentTransactions []Transaction `json:"transactions"` // Add recent transactions to the dashboard response } \ No newline at end of file diff --git a/backend/internal/user/dashboard.go b/backend/internal/user/dashboard.go index 46a21ea..3fb2abd 100644 --- a/backend/internal/user/dashboard.go +++ b/backend/internal/user/dashboard.go @@ -45,9 +45,16 @@ func (h *Handler) DashboardView(w http.ResponseWriter, r *http.Request) { func (h *Handler) DashboardHandler(w http.ResponseWriter, r *http.Request) { fmt.Println("Dashboard JSON handler is running...") - - // Get session cookie (Removed) - userID := "UNI-060104051234" // Dummy user ID for testing + + // Get user ID from query param (No cookies for now) + userID := r.URL.Query().Get("user") + if userID == "" { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "user is required", + }) + return + } // Fetch user and card details var ( @@ -79,7 +86,7 @@ func (h *Handler) DashboardHandler(w http.ResponseWriter, r *http.Request) { FROM users u LEFT JOIN cards c ON u.user_id = c.user_id - WHERE u.user_id = ? + WHERE u.username = ? ` err := h.DB.QueryRow(stmt, userID).Scan(&id, &username, &fullName, &email, &phone, &userType, &balance, &loyaltyPoints, &cardNumber, &expiryDate, &cardStatus) if err != nil { diff --git a/docs/unicardv1.sql b/docs/unicardv1.sql index 7942046..041e5b5 100644 --- a/docs/unicardv1.sql +++ b/docs/unicardv1.sql @@ -78,7 +78,7 @@ CREATE TABLE terminals ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal hardware registry auto-increment row index', terminal_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom public hardware identifier (e.g., TRM-2026-0001) used in API payloads', terminal_sn VARCHAR(50) UNIQUE NOT NULL COMMENT 'Physical factory-assigned unique serial number or MAC address of the ESP32 board', - merchant_id INT NOT NULL COMMENT 'Links to the internal auto-increment id of the managing merchant entity', + merchant_id INT NULL COMMENT 'Links to the internal auto-increment id of the managing merchant entity', device_name VARCHAR(100) NOT NULL COMMENT 'Human-readable descriptor identifying placement (e.g., Counter 1, Jeepney Plate # ABC-123)', location_details VARCHAR(255) NULL COMMENT 'Optional physical sector data, such as a branch route path or stall number designation', status ENUM('active', 'suspended', 'offline') DEFAULT 'active' COMMENT 'Operational network connectivity state of the edge node hardware', diff --git a/frontend/assets/admin/admin_dashboard.js b/frontend/assets/admin/admin_dashboard.js index 72fb132..530231f 100644 --- a/frontend/assets/admin/admin_dashboard.js +++ b/frontend/assets/admin/admin_dashboard.js @@ -29,14 +29,25 @@ document.addEventListener("DOMContentLoaded", function () { } tr.innerHTML = ` -
No merchants registered yet
diff --git a/frontend/assets/admin/admin_merchant.js b/frontend/assets/admin/admin_merchant.js index 271b83e..c04f5b5 100644 --- a/frontend/assets/admin/admin_merchant.js +++ b/frontend/assets/admin/admin_merchant.js @@ -1,42 +1,33 @@ -let originalMerchants = []; -let allMerchants = []; -let currentPage = 1; -const itemsPerPage = 10; +let currentPage = 1; // current page +const itemsPerPage = 10; // items per page let currentSearchQuery = ''; let currentSortOrder = 'desc'; +let totalItemsCount = 0; +let currentMerchants = []; function fetchMerchants() { - fetch('/v1/admin/merchants-data') + const queryParams = new URLSearchParams({ + page: currentPage, + limit: itemsPerPage, + search: currentSearchQuery, + sort: currentSortOrder + }); + + fetch(`/v1/admin/merchants-data?${queryParams.toString()}`) .then(response => response.json()) .then(result => { - if (result.success && result.data && result.data.merchants) { - originalMerchants = result.data.merchants; - applyFiltersAndSort(); + if (result.success && result.data) { + currentMerchants = result.data.merchants || []; + totalItemsCount = result.data.totalItems || 0; + renderTable(); } }) .catch(error => console.error('Error fetching merchants:', error)); } function applyFiltersAndSort() { - let filtered = originalMerchants; - - if (currentSearchQuery) { - const queryTerms = currentSearchQuery.toLowerCase().trim().split(/\s+/); - filtered = filtered.filter(m => { - const searchableText = `${m.business_name || ''} ${m.owner_name || ''} ${m.merchant_id || ''}`.toLowerCase(); - return queryTerms.every(term => searchableText.includes(term)); - }); - } - - filtered.sort((a, b) => { - const dateA = new Date(a.created_at || 0); - const dateB = new Date(b.created_at || 0); - return currentSortOrder === 'desc' ? dateB - dateA : dateA - dateB; - }); - - allMerchants = filtered; currentPage = 1; - renderTable(); + fetchMerchants(); } function highlightText(text, queryTerms) { @@ -67,13 +58,18 @@ function renderTable() { const tbody = document.getElementById('merchantTableBody'); tbody.innerHTML = ''; - const startIndex = (currentPage - 1) * itemsPerPage; - const endIndex = Math.min(startIndex + itemsPerPage, allMerchants.length); - const pageData = allMerchants.slice(startIndex, endIndex); + if (!currentMerchants || currentMerchants.length === 0) { + tbody.innerHTML = `