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 = ` - ${m.merchant_id} - ${m.business_name} - ${m.business_type.replace(/_/g, ' ')} - ${m.owner_name} - ${m.business_email} - ${m.business_phone} + +
+
+ +
+
+
${m.business_name}
+
ID: ${m.merchant_id}
+
+
+ + ${m.business_type.replace(/_/g, ' ')} + +
${m.owner_name}
+
${m.business_email}
+ + ${m.business_phone} - + ${m.status.replace(/_/g, ' ')} @@ -47,7 +58,7 @@ document.addEventListener("DOMContentLoaded", function () { } else { tbody.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 @@
- + {{template "admin_sidebar" "addcard"}}
diff --git a/frontend/templates/admin/admin_dashboard.html b/frontend/templates/admin/admin_dashboard.html index fbf226b..c5f4f62 100644 --- a/frontend/templates/admin/admin_dashboard.html +++ b/frontend/templates/admin/admin_dashboard.html @@ -15,132 +15,7 @@
- + {{template "admin_sidebar" "dashboard"}}
@@ -282,11 +157,9 @@

Registered Merchants

- - + - - + 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 @@
- + {{template "admin_sidebar" "merchants"}}
@@ -251,78 +126,95 @@

Onboard New Merchant

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
+
+
+
+

Merchant #1

+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
-
-

Owner / Contact Information

-
-
- - -
-
- - -
-
- - +
+

Owner / Contact Information

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+

Settlement Details

+
+
+ + +
+
+ + +
+
+ + +
+
-
-

Settlement Details

-
-
- - -
-
- - -
-
- - -
+
+
diff --git a/frontend/templates/admin/admin_sidebar.html b/frontend/templates/admin/admin_sidebar.html new file mode 100644 index 0000000..3459840 --- /dev/null +++ b/frontend/templates/admin/admin_sidebar.html @@ -0,0 +1,128 @@ +{{define "admin_sidebar"}} + +{{end}} diff --git a/frontend/templates/admin/deactivateCard.html b/frontend/templates/admin/deactivateCard.html index e463ad7..769b3a1 100644 --- a/frontend/templates/admin/deactivateCard.html +++ b/frontend/templates/admin/deactivateCard.html @@ -16,94 +16,7 @@
- + {{template "admin_sidebar" "deactivatecard"}}
diff --git a/frontend/templates/admin/hardware_registry.html b/frontend/templates/admin/hardware_registry.html index 84dbf3e..fe84cfe 100644 --- a/frontend/templates/admin/hardware_registry.html +++ b/frontend/templates/admin/hardware_registry.html @@ -24,80 +24,7 @@
- + {{template "admin_sidebar" "terminals"}}
diff --git a/frontend/templates/admin/system_settings.html b/frontend/templates/admin/system_settings.html index 8dac720..d4c5284 100644 --- a/frontend/templates/admin/system_settings.html +++ b/frontend/templates/admin/system_settings.html @@ -28,80 +28,7 @@
- + {{template "admin_sidebar" "settings"}}
From 94aa5b5ffa8a36feb0c6a6bb1d618785d1bfbfc2 Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Sat, 30 May 2026 13:48:44 +0800 Subject: [PATCH 2/3] feat: enhance merchant management with pagination and search functionality --- backend/internal/admin/admin_merchant.go | 78 ++++++++++++++++++++++-- frontend/assets/admin/admin_merchant.js | 67 ++++++++++---------- 2 files changed, 104 insertions(+), 41 deletions(-) diff --git a/backend/internal/admin/admin_merchant.go b/backend/internal/admin/admin_merchant.go index 456f913..397ae38 100644 --- a/backend/internal/admin/admin_merchant.go +++ b/backend/internal/admin/admin_merchant.go @@ -3,6 +3,8 @@ package admin import ( "log" "net/http" + "strconv" + "strings" jsonwrite "unicard-go/backend/internal/pkg/handler" structs "unicard-go/backend/internal/pkg/structs" ) @@ -15,10 +17,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 +93,22 @@ func (h *Handler) MerchantManagementDataHandler(w http.ResponseWriter, r *http.R } merchants = append(merchants, m) } + if merchants == nil { + merchants = []structs.Merchant{} + } + + 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/frontend/assets/admin/admin_merchant.js b/frontend/assets/admin/admin_merchant.js index 99b7ef2..e91f09e 100644 --- a/frontend/assets/admin/admin_merchant.js +++ b/frontend/assets/admin/admin_merchant.js @@ -1,42 +1,33 @@ -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'; +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,9 @@ 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); - const queryTerms = currentSearchQuery ? currentSearchQuery.toLowerCase().trim().split(/\s+/) : []; - pageData.forEach(merchant => { + currentMerchants.forEach(merchant => { const tr = document.createElement('tr'); const highlightedBusinessName = highlightText(merchant.business_name, queryTerms); @@ -107,9 +94,12 @@ function renderTable() { tbody.appendChild(tr); }); - document.getElementById('pageStart').textContent = allMerchants.length > 0 ? startIndex + 1 : 0; + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = Math.min(startIndex + itemsPerPage, totalItemsCount); + + document.getElementById('pageStart').textContent = totalItemsCount > 0 ? startIndex + 1 : 0; document.getElementById('pageEnd').textContent = endIndex; - document.getElementById('totalItems').textContent = allMerchants.length; + document.getElementById('totalItems').textContent = totalItemsCount; renderPagination(); } @@ -118,7 +108,7 @@ function renderPagination() { const paginationControls = document.getElementById('paginationControls'); paginationControls.innerHTML = ''; - const totalPages = Math.ceil(allMerchants.length / itemsPerPage); + const totalPages = Math.ceil(totalItemsCount / itemsPerPage); if (totalPages <= 1) return; @@ -126,14 +116,14 @@ function renderPagination() { prevBtn.className = `px-3 py-1 rounded-md text-sm font-medium ${currentPage === 1 ? 'text-gray-400 cursor-not-allowed' : 'text-blue-600 hover:bg-blue-50'}`; prevBtn.textContent = 'Previous'; prevBtn.disabled = currentPage === 1; - prevBtn.onclick = () => { if (currentPage > 1) { currentPage--; renderTable(); } }; + prevBtn.onclick = () => { if (currentPage > 1) { currentPage--; fetchMerchants(); } }; paginationControls.appendChild(prevBtn); for (let i = 1; i <= totalPages; i++) { const btn = document.createElement('button'); btn.className = `px-3 py-1 rounded-md text-sm font-medium ${currentPage === i ? 'bg-blue-600 text-white' : 'text-gray-700 hover:bg-gray-50'}`; btn.textContent = i; - btn.onclick = () => { currentPage = i; renderTable(); }; + btn.onclick = () => { currentPage = i; fetchMerchants(); }; paginationControls.appendChild(btn); } @@ -141,7 +131,7 @@ function renderPagination() { nextBtn.className = `px-3 py-1 rounded-md text-sm font-medium ${currentPage === totalPages ? 'text-gray-400 cursor-not-allowed' : 'text-blue-600 hover:bg-blue-50'}`; nextBtn.textContent = 'Next'; nextBtn.disabled = currentPage === totalPages; - nextBtn.onclick = () => { if (currentPage < totalPages) { currentPage++; renderTable(); } }; + nextBtn.onclick = () => { if (currentPage < totalPages) { currentPage++; fetchMerchants(); } }; paginationControls.appendChild(nextBtn); } @@ -149,10 +139,15 @@ document.addEventListener('DOMContentLoaded', () => { fetchMerchants(); const searchInput = document.getElementById('searchInput'); + let searchTimeout = null; if (searchInput) { searchInput.addEventListener('input', (e) => { currentSearchQuery = e.target.value; - applyFiltersAndSort(); + // Debounce search + if (searchTimeout) clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + applyFiltersAndSort(); + }, 300); }); } From 3ba0efea0f124004a6e9ed5550ecb48baa1e9ad2 Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:30:01 +0800 Subject: [PATCH 3/3] Refactor dashboard and login functionalities; integrate terminal management - Removed redundant transaction and dashboard user structs from dashboard.go, replaced with database queries for user data. - Updated login.go to use username instead of user ID for login responses and adjusted redirect URLs based on user roles. - Enhanced jsonWrite.go to include username in login responses. - Added terminal struct and updated dashboard user struct in structs.go to accommodate recent transactions. - Modified dashboard handler in user/dashboard.go to fetch user ID from query parameters instead of session cookies. - Updated SQL schema to allow null merchant IDs in the terminals table. - Improved admin merchant and terminal management UI with new JavaScript functionalities for terminal registration and pagination. - Created new admin signup page and associated JavaScript for handling admin registrations. - Removed outdated hardware registry template and replaced it with a new terminal management template. --- backend/cmd/app/main.go | 4 + backend/internal/admin/add_merchant.go | 231 ++++++++++++++++++ backend/internal/admin/admin_merchant.go | 44 ++++ backend/internal/admin/admin_terminal.go | 168 +++++++++++++ backend/internal/admin/super_admin_pages.go | 181 +------------- backend/internal/auth/admin_signup.go | 89 +++++++ backend/internal/auth/dashboard.go | 74 +++--- backend/internal/auth/login.go | 21 +- backend/internal/pkg/handler/jsonWrite.go | 18 +- backend/internal/pkg/structs/struct.go | 52 +++- backend/internal/user/dashboard.go | 15 +- docs/unicardv1.sql | 2 +- frontend/assets/admin/admin_merchant.js | 14 ++ frontend/assets/admin/admin_terminal.js | 180 ++++++++++++++ frontend/assets/js/admin_signup.js | 41 ++++ frontend/assets/js/dashboard.js | 10 +- frontend/templates/admin/addCards.html | 2 +- frontend/templates/admin/admin_merchant.html | 18 ++ frontend/templates/admin/admin_terminal.html | 119 +++++++++ .../templates/admin/hardware_registry.html | 128 ---------- frontend/templates/auth/admin_signup.html | 40 +++ 21 files changed, 1068 insertions(+), 383 deletions(-) create mode 100644 backend/internal/admin/add_merchant.go create mode 100644 backend/internal/admin/admin_terminal.go create mode 100644 backend/internal/auth/admin_signup.go create mode 100644 frontend/assets/admin/admin_terminal.js create mode 100644 frontend/assets/js/admin_signup.js create mode 100644 frontend/templates/admin/admin_terminal.html delete mode 100644 frontend/templates/admin/hardware_registry.html create mode 100644 frontend/templates/auth/admin_signup.html 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_merchant.go b/backend/internal/admin/admin_merchant.go index 397ae38..87d93de 100644 --- a/backend/internal/admin/admin_merchant.go +++ b/backend/internal/admin/admin_merchant.go @@ -1,6 +1,7 @@ package admin import ( + "fmt" "log" "net/http" "strconv" @@ -95,6 +96,49 @@ func (h *Handler) MerchantManagementDataHandler(w http.ResponseWriter, r *http.R } 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 { 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 f4f8969..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,160 +57,3 @@ func (h *Handler) SystemSettingsView(w http.ResponseWriter, r *http.Request) { }) } } - -// 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() - - 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 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/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_merchant.js b/frontend/assets/admin/admin_merchant.js index e91f09e..c04f5b5 100644 --- a/frontend/assets/admin/admin_merchant.js +++ b/frontend/assets/admin/admin_merchant.js @@ -58,6 +58,15 @@ function renderTable() { const tbody = document.getElementById('merchantTableBody'); tbody.innerHTML = ''; + if (!currentMerchants || currentMerchants.length === 0) { + tbody.innerHTML = `
`; + document.getElementById('pageStart').textContent = '0'; + document.getElementById('pageEnd').textContent = '0'; + document.getElementById('totalItems').textContent = '0'; + document.getElementById('paginationControls').innerHTML = ''; + return; + } + const queryTerms = currentSearchQuery ? currentSearchQuery.toLowerCase().trim().split(/\s+/) : []; currentMerchants.forEach(merchant => { @@ -84,6 +93,11 @@ function renderTable() {
${highlightedOwnerName}
${merchant.business_email}
+ diff --git a/frontend/assets/admin/admin_terminal.js b/frontend/assets/admin/admin_terminal.js new file mode 100644 index 0000000..9fe70d7 --- /dev/null +++ b/frontend/assets/admin/admin_terminal.js @@ -0,0 +1,180 @@ +window.renderAssignedTerminals = function(terminals) { + if (!terminals || terminals.length === 0) { + return 'No terminals'; + } + return terminals.map(t => `
${t.terminal_sn} - ${t.device_name}
`).join(''); +}; + +document.addEventListener("DOMContentLoaded", () => { + const tableBody = document.getElementById("terminal-table-body"); + if (!tableBody) return; // Exit if not on the terminal page + + const searchInput = document.getElementById("searchInput"); + const startItemSpan = document.getElementById("start-item"); + const endItemSpan = document.getElementById("end-item"); + const totalItemsSpan = document.getElementById("total-items"); + const prevPageBtn = document.getElementById("prev-page"); + const nextPageBtn = document.getElementById("next-page"); + const pageInfoSpan = document.getElementById("page-info"); + + let currentPage = 1; + const limit = 10; + let currentSearch = ""; + + function fetchTerminals(page = 1, search = "") { + const url = `/v1/admin/terminals-data?page=${page}&limit=${limit}&search=${encodeURIComponent(search)}`; + + fetch(url) + .then(res => res.json()) + .then(data => { + if (data.success) { + renderTable(data.data.terminals || []); + updatePagination(data.data.totalItems, data.data.page, data.data.limit); + } else { + console.error("Failed to fetch terminals:", data.message); + } + }) + .catch(err => console.error("Error fetching terminals:", err)); + } + + function renderTable(terminals) { + tableBody.innerHTML = ""; + + if (terminals.length === 0) { + tableBody.innerHTML = ``; + return; + } + + terminals.forEach(terminal => { + const statusClass = terminal.status === 'active' || terminal.status === 'Online' + ? 'bg-green-100 text-green-800' + : terminal.status === 'offline' || terminal.status === 'Offline' + ? 'bg-red-100 text-red-800' + : 'bg-yellow-100 text-yellow-800'; + + const statusText = terminal.status.charAt(0).toUpperCase() + terminal.status.slice(1); + + // simple icon logic based on device name or type if available, else default to desktop + const icon = terminal.device_name.toLowerCase().includes('rfid') || terminal.device_name.toLowerCase().includes('turnstile') + ? 'fa-door-open' : 'fa-desktop'; + + const iconBg = statusText === 'Online' || statusText === 'Active' ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 text-gray-600'; + + const row = document.createElement('tr'); + row.innerHTML = ` + + + + + + `; + tableBody.appendChild(row); + }); + } + + function updatePagination(totalItems, page, currentLimit) { + currentPage = page; + + const totalPages = Math.ceil(totalItems / currentLimit); + const startItem = totalItems === 0 ? 0 : (page - 1) * currentLimit + 1; + const endItem = Math.min(page * currentLimit, totalItems); + + startItemSpan.textContent = startItem; + endItemSpan.textContent = endItem; + totalItemsSpan.textContent = totalItems; + + if (totalItems === 0) { + prevPageBtn.style.display = 'none'; + nextPageBtn.style.display = 'none'; + pageInfoSpan.style.display = 'none'; + } else { + prevPageBtn.style.display = ''; + nextPageBtn.style.display = ''; + pageInfoSpan.style.display = ''; + pageInfoSpan.textContent = `Page ${page} of ${totalPages}`; + prevPageBtn.disabled = page <= 1; + nextPageBtn.disabled = page >= totalPages; + } + } + + prevPageBtn.addEventListener("click", () => { + if (currentPage > 1) { + fetchTerminals(currentPage - 1, currentSearch); + } + }); + + nextPageBtn.addEventListener("click", () => { + fetchTerminals(currentPage + 1, currentSearch); + }); + + // Debounce search input + let searchTimeout; + searchInput.addEventListener("input", (e) => { + clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + currentSearch = e.target.value.trim(); + fetchTerminals(1, currentSearch); + }, 300); + }); + + const addTerminalForm = document.getElementById("addTerminalForm"); + if (addTerminalForm) { + addTerminalForm.addEventListener("submit", (e) => { + e.preventDefault(); + const formData = new FormData(addTerminalForm); + const data = { + terminalSn: formData.get("terminalSn"), + deviceName: formData.get("deviceName") + }; + + const alertBox = document.getElementById("terminalFormAlert"); + alertBox.classList.add("hidden"); + + fetch('/v1/admin/terminals/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data) + }) + .then(res => res.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 || "Terminal registered successfully!"; + setTimeout(() => { + document.getElementById('addTerminalModal').classList.add('hidden'); + addTerminalForm.reset(); + alertBox.classList.add("hidden"); + fetchTerminals(1, currentSearch); + }, 1500); + } else { + alertBox.classList.add("bg-red-50", "text-red-600"); + alertBox.textContent = result.message || "An error occurred."; + } + }) + .catch(err => { + alertBox.classList.remove("hidden", "bg-green-50", "text-green-600"); + alertBox.classList.add("bg-red-50", "text-red-600"); + alertBox.textContent = "Network error. Please try again."; + }); + }); + } + + // Initial fetch + fetchTerminals(); +}); diff --git a/frontend/assets/js/admin_signup.js b/frontend/assets/js/admin_signup.js new file mode 100644 index 0000000..71549a8 --- /dev/null +++ b/frontend/assets/js/admin_signup.js @@ -0,0 +1,41 @@ +document.addEventListener("DOMContentLoaded", () => { + const form = document.getElementById("adminSignupForm"); + const alertBox = document.getElementById("formAlert"); + + form.addEventListener("submit", (e) => { + e.preventDefault(); + alertBox.classList.add("hidden"); + alertBox.classList.remove("bg-red-100", "text-red-700", "bg-green-100", "text-green-700"); + + const name = document.getElementById("name").value; + const username = document.getElementById("username").value; + const email = document.getElementById("email").value; + const password = document.getElementById("password").value; + + fetch("/v1/admin-signup", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, username, email, password }) + }) + .then(res => res.json()) + .then(data => { + alertBox.classList.remove("hidden"); + if (data.success) { + alertBox.classList.add("bg-green-100", "text-green-700"); + alertBox.textContent = data.message; + setTimeout(() => { + window.location.href = "/login"; + }, 2000); + } else { + alertBox.classList.add("bg-red-100", "text-red-700"); + alertBox.textContent = data.message || "An error occurred."; + } + }) + .catch(err => { + alertBox.classList.remove("hidden"); + alertBox.classList.add("bg-red-100", "text-red-700"); + alertBox.textContent = "Network error. Please try again."; + console.error("Error signing up admin:", err); + }); + }); +}); diff --git a/frontend/assets/js/dashboard.js b/frontend/assets/js/dashboard.js index e6310e7..0f83626 100644 --- a/frontend/assets/js/dashboard.js +++ b/frontend/assets/js/dashboard.js @@ -147,7 +147,15 @@ document.addEventListener("DOMContentLoaded", function () { // --- Fetch Dashboard Data --- function fetchDashboardData() { - fetch("/v1/user/dashboard") + const urlParams = new URLSearchParams(window.location.search); + const userId = urlParams.get('user'); + + let endpoint = "/v1/user/dashboard"; + if (userId) { + endpoint += "?user=" + encodeURIComponent(userId); + } + + fetch(endpoint) .then(response => { if (response.status === 401) { window.location.href = "/login"; diff --git a/frontend/templates/admin/addCards.html b/frontend/templates/admin/addCards.html index 6429322..3c05a11 100644 --- a/frontend/templates/admin/addCards.html +++ b/frontend/templates/admin/addCards.html @@ -175,7 +175,7 @@

Logout Confirmation

- + \ No newline at end of file diff --git a/frontend/templates/admin/admin_merchant.html b/frontend/templates/admin/admin_merchant.html index b74947f..1bf0bc2 100644 --- a/frontend/templates/admin/admin_merchant.html +++ b/frontend/templates/admin/admin_merchant.html @@ -83,6 +83,9 @@

Client Businesses

+ @@ -207,6 +210,21 @@

Settlement Details

class="mt-1 w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500"> + +
+

Terminal Information

+
+
+ + +
+
+ + +
+
diff --git a/frontend/templates/admin/admin_terminal.html b/frontend/templates/admin/admin_terminal.html new file mode 100644 index 0000000..008b6ea --- /dev/null +++ b/frontend/templates/admin/admin_terminal.html @@ -0,0 +1,119 @@ + + + + + + Super Admin - Terminal Registry + + + + + + + +
+ + {{template "admin_sidebar" "terminals"}} + + +
+
+

Terminal Registry

+
+ Welcome, Super Admin +
+ SA +
+
+
+ +
+
+
+
+

Hardware Inventory

+

Master list of deployed RFID/POS terminal machines and their live connection status.

+
+ +
+ +
+
+
+ + + + +
+
+ +
+
Merchant IDBusiness NameMerchant Business TypeOwner NameEmailOwner/Contact Phone Status Created At
No merchants found matching your criteria.
+
+ ${window.renderAssignedTerminals ? window.renderAssignedTerminals(merchant.terminals) : 'No terminals'} +
+
${merchant.status}
No terminals found matching your criteria.
+
+
+ +
+
+
${terminal.terminal_id}
+
SN: ${terminal.terminal_sn}
+
+
+
${terminal.assigned_merchant}${terminal.device_name} + ${statusText} + + + + Owner / Contact + Assigned Terminals Status
+ + + + + + + + + + + + +
Device ID / S/NAssigned MerchantDevice TypeLive StatusActions
+
+ + +
+
+ Showing 0 to + 0 of + 0 results +
+
+ + Page 1 + +
+
+ +
+ +
+
+ + + + + + + diff --git a/frontend/templates/admin/hardware_registry.html b/frontend/templates/admin/hardware_registry.html deleted file mode 100644 index fe84cfe..0000000 --- a/frontend/templates/admin/hardware_registry.html +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - Super Admin - Terminal Registry - - - - - - - - -
- - {{template "admin_sidebar" "terminals"}} - - -
-
-

Terminal Registry

-
- Welcome, Super Admin -
- SA -
-
-
- -
-
-
-
-

Hardware Inventory

-

Master list of deployed RFID/POS terminal machines and their live connection status.

-
- -
- -
-
-
- - - - -
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - -
Device ID / S/NAssigned MerchantDevice TypeLive StatusActions
-
-
- -
-
-
TRM-892A41
-
SN: PX-2910-4412
-
-
-
Alfamart Phils.Standard POS - Online - - - -
-
-
- -
-
-
RST-1004B2
-
SN: RF-9921-8843
-
-
-
Metro Transport CoopRFID Turnstile - Offline - - - -
-
-
-
-
-
-
- - diff --git a/frontend/templates/auth/admin_signup.html b/frontend/templates/auth/admin_signup.html new file mode 100644 index 0000000..cbb328f --- /dev/null +++ b/frontend/templates/auth/admin_signup.html @@ -0,0 +1,40 @@ + + + + + + Admin Registration + + + +
+

Create Admin Account

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + + + +
+
+ + + +