From 2468437867dc9bc5b0471a0f42a1a6af97b1b77d Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Sat, 30 May 2026 13:44:29 +0800 Subject: [PATCH 1/3] Add admin sidebar template and refactor existing admin pages to use it - Created a new sidebar template for the admin dashboard to standardize navigation across admin pages. - Refactored `deactivateCard.html`, `hardware_registry.html`, and `system_settings.html` to utilize the new sidebar template, improving code maintainability and consistency. - Removed redundant sidebar code from the aforementioned pages, enhancing readability and reducing duplication. --- backend/internal/admin/admin_dashboard.go | 4 +- backend/internal/admin/super_admin_pages.go | 186 +++++++----- frontend/assets/admin/admin_dashboard.js | 27 +- frontend/assets/admin/admin_merchant.js | 61 +++- frontend/templates/admin/addCards.html | 89 +----- frontend/templates/admin/admin_dashboard.html | 133 +-------- frontend/templates/admin/admin_merchant.html | 280 ++++++------------ frontend/templates/admin/admin_sidebar.html | 128 ++++++++ frontend/templates/admin/deactivateCard.html | 89 +----- .../templates/admin/hardware_registry.html | 75 +---- frontend/templates/admin/system_settings.html | 75 +---- 11 files changed, 399 insertions(+), 748 deletions(-) create mode 100644 frontend/templates/admin/admin_sidebar.html 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/super_admin_pages.go b/backend/internal/admin/super_admin_pages.go index 4e839b6..f4f8969 100644 --- a/backend/internal/admin/super_admin_pages.go +++ b/backend/internal/admin/super_admin_pages.go @@ -80,74 +80,21 @@ func (h *Handler) SystemSettingsView(w http.ResponseWriter, r *http.Request) { } } -// AddMerchantHandler creates a new merchant and its corresponding owner user +// AddMerchantHandler creates new merchants and their corresponding owner users in bulk func (h *Handler) AddMerchantHandler(w http.ResponseWriter, r *http.Request) { - var req AddMerchantRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + 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", + Message: "Invalid JSON payload format. Expected array of merchants.", }) 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 - } - + if len(reqs) == 0 { 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", + Message: "No merchants provided.", }) return } @@ -162,55 +109,130 @@ func (h *Handler) AddMerchantHandler(w http.ResponseWriter, r *http.Request) { 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") + // 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 creating user: %v", err) - jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + log.Printf("Error preparing user stmt: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ Success: false, - Message: "Failed to create user account (email or phone might already exist)", + Message: "Database error", }) return } + defer userStmt.Close() - // Insert Merchant - merchStmt := `INSERT INTO merchants ( + 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` - - _, 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", - ) - + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) if err != nil { tx.Rollback() - log.Printf("Error creating merchant: %v", err) - jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + log.Printf("Error preparing merchant stmt: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ Success: false, - Message: "Failed to create merchant profile (registration num or email might exist)", + Message: "Database error", }) return } + defer merchStmt.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", + } + 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 + } + + _, 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 + } + } 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", + Message: "Failed to finalize batch creation", }) return } jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ Success: true, - Message: "Merchant onboarded successfully", + Message: fmt.Sprintf("Successfully onboarded %d merchant(s)", len(reqs)), }) } 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..99b7ef2 100644 --- a/frontend/assets/admin/admin_merchant.js +++ b/frontend/assets/admin/admin_merchant.js @@ -1,7 +1,7 @@ -let originalMerchants = []; -let allMerchants = []; -let currentPage = 1; -const itemsPerPage = 10; +let originalMerchants = []; // store all fetched merchants +let allMerchants = []; // store filtered merchants +let currentPage = 1; // current page +const itemsPerPage = 10; // items per page let currentSearchQuery = ''; let currentSortOrder = 'desc'; @@ -176,23 +176,68 @@ document.addEventListener('DOMContentLoaded', () => { } }); +document.getElementById('addAnotherMerchantBtn').addEventListener('click', () => { + const container = document.getElementById('merchantBlocksContainer'); + const firstBlock = container.querySelector('.merchant-block'); + const newBlock = firstBlock.cloneNode(true); + + // Clear inputs (keep default commission rate) + const inputs = newBlock.querySelectorAll('input, select'); + inputs.forEach(input => { + if (input.name !== 'commissionRate' && input.name !== 'businessType') { + input.value = ''; + } + }); + + const blockCount = container.querySelectorAll('.merchant-block').length + 1; + newBlock.querySelector('.merchant-title').textContent = `Merchant #${blockCount}`; + + const removeBtn = newBlock.querySelector('.remove-merchant-btn'); + removeBtn.classList.remove('hidden'); + removeBtn.addEventListener('click', function() { + newBlock.remove(); + updateMerchantTitles(); + }); + + container.appendChild(newBlock); +}); + +function updateMerchantTitles() { + const blocks = document.querySelectorAll('.merchant-block'); + blocks.forEach((block, index) => { + block.querySelector('.merchant-title').textContent = `Merchant #${index + 1}`; + }); +} + document.getElementById('onboardForm').addEventListener('submit', function (e) { e.preventDefault(); - const formData = new FormData(e.target); - const data = Object.fromEntries(formData.entries()); + + const blocks = document.querySelectorAll('.merchant-block'); + const merchantsData = []; + + blocks.forEach(block => { + const inputs = block.querySelectorAll('input, select'); + const merchantObj = {}; + inputs.forEach(input => { + if (input.name) merchantObj[input.name] = input.value; + }); + merchantsData.push(merchantObj); + }); + const alertBox = document.getElementById('formAlert'); + alertBox.classList.add('hidden'); fetch('/v1/admin/merchants/add', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) + body: JSON.stringify(merchantsData) }) .then(response => response.json()) .then(result => { alertBox.classList.remove('hidden', 'bg-red-50', 'text-red-600', 'bg-green-50', 'text-green-600'); if (result.success) { alertBox.classList.add('bg-green-50', 'text-green-600'); - alertBox.textContent = result.message || "Merchant onboarded successfully!"; + alertBox.textContent = result.message || "Merchants onboarded successfully!"; setTimeout(() => window.location.reload(), 1500); } else { alertBox.classList.add('bg-red-50', 'text-red-600'); diff --git a/frontend/templates/admin/addCards.html b/frontend/templates/admin/addCards.html index b0e2568..6429322 100644 --- a/frontend/templates/admin/addCards.html +++ b/frontend/templates/admin/addCards.html @@ -16,94 +16,7 @@| Merchant ID | -Business Name | +Merchant | Business Type | -Owner Name | -Owner/Contact | Phone | Status | Created At | diff --git a/frontend/templates/admin/admin_merchant.html b/frontend/templates/admin/admin_merchant.html index 439cda7..b74947f 100644 --- a/frontend/templates/admin/admin_merchant.html +++ b/frontend/templates/admin/admin_merchant.html @@ -15,132 +15,7 @@
|---|