diff --git a/backend/cmd/app/main.go b/backend/cmd/app/main.go index 9a2b7c1..a740a94 100644 --- a/backend/cmd/app/main.go +++ b/backend/cmd/app/main.go @@ -81,8 +81,8 @@ func main() { mux.HandleFunc("POST /v1/forgot-password/send-otp", authHandler.ForgotPasswordSendOTP) mux.HandleFunc("POST /v1/forgot-password/verify-otp", authHandler.ForgotPasswordVerifyOTP) mux.HandleFunc("POST /v1/reset-password", authHandler.ResetPassword) - mux.HandleFunc("GET /dashboard", userHandler.DashboardView) - mux.HandleFunc("GET /v1/user/dashboard", userHandler.DashboardHandler) + mux.HandleFunc("GET /{username}", userHandler.DashboardView) + mux.HandleFunc("GET /v1/user/{username}", userHandler.DashboardHandler) //mux.HandleFunc("GET /transaction", userHandler.TransactionView) //mux.HandleFunc("GET /topup", userHandler.TopupView) //mux.HandleFunc("GET /profile", userHandler.ProfileView) @@ -92,23 +92,24 @@ func main() { //mux.HandleFunc("GET /logout",) // super admin endpoints - mux.HandleFunc("GET /admin/dashboard", adminHanlder.AdminDashboardView) - mux.HandleFunc("GET /v1/admin/dashboard-data", adminHanlder.AdminDashboardDataHandler) - 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) - mux.HandleFunc("GET /v1/admin/card-inventory-data", adminHanlder.CardInventoryDataHandler) - mux.HandleFunc("GET /admin/addcard", adminHanlder.AddCardsView) - mux.HandleFunc("GET /admin/deactivatecard", adminHanlder.DeactivateView) - mux.HandleFunc("POST /v1/admin/addcardauth", adminHanlder.AddCardHandler) - mux.HandleFunc("POST /v1/admin/deactivatecardauth", adminHanlder.DeactivateCardHanlder) - mux.HandleFunc("POST /v1/admin/deletecardauth", adminHanlder.DeleteCardHandler) - mux.HandleFunc("GET /admin/delete-cards", adminHanlder.DeleteCardView) + mux.HandleFunc("GET /admin/{username}", adminHanlder.AdminDashboardView) + mux.HandleFunc("GET /v1/admin/{username}/dashboard-data", adminHanlder.AdminDashboardDataHandler) + mux.HandleFunc("GET /admin/{username}/merchants", adminHanlder.MerchantManagementView) + mux.HandleFunc("GET /v1/admin/{username}/merchants-data", adminHanlder.MerchantManagementDataHandler) + mux.HandleFunc("GET /admin/{username}/terminals", adminHanlder.TerminalRegistryView) + mux.HandleFunc("GET /v1/admin/{username}/terminals-data", adminHanlder.TerminalRegistryDataHandler) + mux.HandleFunc("GET /v1/admin/{username}/terminals/unassigned", adminHanlder.GetUnassignedTerminalsHandler) + mux.HandleFunc("POST /v1/admin/{username}/terminals/add", adminHanlder.AddTerminalHandler) + mux.HandleFunc("GET /admin/{username}/settings", adminHanlder.SystemSettingsView) + mux.HandleFunc("POST /v1/admin/{username}/merchants/add", adminHanlder.AddMerchantHandler) + mux.HandleFunc("GET /admin/{username}/card-inventory", adminHanlder.CardInventoryView) + mux.HandleFunc("GET /v1/admin/{username}/card-inventory-data", adminHanlder.CardInventoryDataHandler) + mux.HandleFunc("GET /admin/{username}/addcard", adminHanlder.AddCardsView) + mux.HandleFunc("GET /admin/{username}/deactivatecard", adminHanlder.DeactivateView) + mux.HandleFunc("POST /v1/admin/{username}/addcardauth", adminHanlder.AddCardHandler) + mux.HandleFunc("POST /v1/admin/{username}/deactivatecardauth", adminHanlder.DeactivateCardHanlder) + mux.HandleFunc("POST /v1/admin/{username}/deletecardauth", adminHanlder.DeleteCardHandler) + mux.HandleFunc("GET /admin/{username}/delete-cards", adminHanlder.DeleteCardView) // Wrap mux with custom handler for root redirect diff --git a/backend/internal/admin/add_card.go b/backend/internal/admin/add_card.go index 5205634..39ff2b9 100644 --- a/backend/internal/admin/add_card.go +++ b/backend/internal/admin/add_card.go @@ -20,7 +20,11 @@ import ( // AddCardsView renders the addCards.html template after checking the admin session. func (h *Handler) AddCardsView(w http.ResponseWriter, r *http.Request) { fmt.Println("AddCardsView running...") - h.Tpl.ExecuteTemplate(w, "addCards.html", nil) + data := AdminPageData{ + Page: "addcard", + Username: r.PathValue("username"), + } + h.Tpl.ExecuteTemplate(w, "addCards.html", data) } // AddCardHandler handles card creation and returns JSON response. diff --git a/backend/internal/admin/add_merchant.go b/backend/internal/admin/add_merchant.go index a4bcf18..999f338 100644 --- a/backend/internal/admin/add_merchant.go +++ b/backend/internal/admin/add_merchant.go @@ -8,6 +8,7 @@ import ( "log" "math/big" "net/http" + "strings" "time" jsonwrite "unicard-go/backend/internal/pkg/handler" @@ -19,17 +20,17 @@ import ( 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"` + RegistrationNum string `json:"registrationNum" 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"` + CommissionRate string `json:"commissionRate" 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"` + DeviceName string `json:"deviceName" db:"device_name"` } // AddMerchantHandler creates new merchants and their corresponding owner users in bulk @@ -76,7 +77,7 @@ func (h *Handler) AddMerchantHandler(w http.ResponseWriter, r *http.Request) { 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, + user_id, owner_name, business_email, business_phone, commission_rate, settlement_account_name, settlement_account_number, settlement_bank_name, status ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) if err != nil { @@ -90,9 +91,8 @@ func (h *Handler) AddMerchantHandler(w http.ResponseWriter, r *http.Request) { } defer merchStmt.Close() - termStmt, err := tx.Prepare(`INSERT INTO terminals ( - terminal_id, terminal_sn, merchant_id, device_name, status - ) VALUES (?, ?, ?, ?, ?)`) + // Update terminal status to active and assign merchant_id + termStmt, err := tx.Prepare(`UPDATE terminals SET merchant_id = ?, location_details = ?, status = 'active' WHERE terminal_sn = ?`) if err != nil { tx.Rollback() log.Printf("Error preparing terminal stmt: %v", err) @@ -105,6 +105,20 @@ func (h *Handler) AddMerchantHandler(w http.ResponseWriter, r *http.Request) { defer termStmt.Close() for i, req := range reqs { + // Clean and format string fields + req.BusinessName = strings.Title(strings.ToLower(strings.TrimSpace(req.BusinessName))) + req.BusinessAddress = strings.Title(strings.ToLower(strings.TrimSpace(req.BusinessAddress))) + req.OwnerName = strings.Title(strings.ToLower(strings.TrimSpace(req.OwnerName))) + req.SettlementName = strings.Title(strings.ToLower(strings.TrimSpace(req.SettlementName))) + + // Some fields don't need title case but should be trimmed + req.BusinessEmail = strings.ToLower(strings.TrimSpace(req.BusinessEmail)) + req.BusinessPhone = strings.TrimSpace(req.BusinessPhone) + req.TerminalSN = strings.TrimSpace(req.TerminalSN) + req.DeviceName = strings.TrimSpace(req.DeviceName) + + reqs[i] = req // update back to slice + err := Validate.Struct(req) if err != nil { tx.Rollback() @@ -116,17 +130,14 @@ func (h *Handler) AddMerchantHandler(w http.ResponseWriter, r *http.Request) { 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) @@ -140,12 +151,12 @@ func (h *Handler) AddMerchantHandler(w http.ResponseWriter, r *http.Request) { } // Generate IDs (Format: YYMMminsecxxxxx where xxxxx is 5 random numbers) - timestamp := time.Now().Format("06010405") // YYMMDDHH + timestamp := time.Now().Format("01020605") // MMDDYYss - nUser, _ := rand.Int(rand.Reader, big.NewInt(100000)) + nUser, _ := rand.Int(rand.Reader, big.NewInt(10000)) // max 9999 userID := fmt.Sprintf("UNI-%s%04d", timestamp, nUser.Int64()) - nMerchant, _ := rand.Int(rand.Reader, big.NewInt(100000)) + nMerchant, _ := rand.Int(rand.Reader, big.NewInt(10000)) merchantID := fmt.Sprintf("MCH-%s%04d", timestamp, nMerchant.Int64()) // Create user for the merchant owner @@ -173,9 +184,16 @@ func (h *Handler) AddMerchantHandler(w http.ResponseWriter, r *http.Request) { return } - res, err := merchStmt.Exec( - merchantID, req.BusinessName, req.BusinessType, req.RegistrationNum, req.BusinessAddress, - userID, req.OwnerName, req.BusinessEmail, req.BusinessPhone, req.CommissionRate, + // Generate registration number (UCBZ-MMDDss-xxxxxxxxxx) + nReg, _ := rand.Int(rand.Reader, big.NewInt(10000000000)) + regNum := fmt.Sprintf("UCBZ-%s-%010d", time.Now().Format("010205"), nReg.Int64()) + + // Set commission rate + fixedCommissionRate := 2.00 + + _, err = merchStmt.Exec( + merchantID, req.BusinessName, req.BusinessType, regNum, req.BusinessAddress, + userID, req.OwnerName, req.BusinessEmail, req.BusinessPhone, fixedCommissionRate, req.SettlementName, req.SettlementAccount, req.SettlementBank, "active", ) @@ -189,21 +207,8 @@ func (h *Handler) AddMerchantHandler(w http.ResponseWriter, r *http.Request) { 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") + // Update the existing terminal + _, err = termStmt.Exec(userID, req.BusinessAddress, req.TerminalSN) if err != nil { tx.Rollback() log.Printf("Error creating terminal %d: %v", i+1, err) diff --git a/backend/internal/admin/admin_dashboard.go b/backend/internal/admin/admin_dashboard.go index 2980524..65d9db6 100644 --- a/backend/internal/admin/admin_dashboard.go +++ b/backend/internal/admin/admin_dashboard.go @@ -7,10 +7,19 @@ import ( structs "unicard-go/backend/internal/pkg/structs" ) +type AdminPageData struct { + Page string + Username string +} + // AdminDashboardView renders the platform_overview.html template after checking the admin session. func (h *Handler) AdminDashboardView(w http.ResponseWriter, r *http.Request) { log.Println("AdminDashboardView running...") - h.Tpl.ExecuteTemplate(w, "admin_dashboard.html", nil) + data := AdminPageData{ + Page: "dashboard", + Username: r.PathValue("username"), + } + h.Tpl.ExecuteTemplate(w, "admin_dashboard.html", data) } // AdminDashboardDataHandler handles the request for admin dashboard data and returns JSON response. diff --git a/backend/internal/admin/admin_merchant.go b/backend/internal/admin/admin_merchant.go index 87d93de..a18ce76 100644 --- a/backend/internal/admin/admin_merchant.go +++ b/backend/internal/admin/admin_merchant.go @@ -12,7 +12,11 @@ import ( func (h *Handler) MerchantManagementView(w http.ResponseWriter, r *http.Request) { log.Println("MerchantManagementView running...") - h.Tpl.ExecuteTemplate(w, "admin_merchant.html", nil) + data := AdminPageData{ + Page: "merchants", + Username: r.PathValue("username"), + } + h.Tpl.ExecuteTemplate(w, "admin_merchant.html", data) } func (h *Handler) MerchantManagementDataHandler(w http.ResponseWriter, r *http.Request) { @@ -23,6 +27,8 @@ func (h *Handler) MerchantManagementDataHandler(w http.ResponseWriter, r *http.R limitStr := r.URL.Query().Get("limit") search := r.URL.Query().Get("search") sortOrder := r.URL.Query().Get("sort") // desc or asc + category := r.URL.Query().Get("category") + status := r.URL.Query().Get("status") page := 1 limit := 10 @@ -46,6 +52,16 @@ func (h *Handler) MerchantManagementDataHandler(w http.ResponseWriter, r *http.R args = append(args, searchPattern, searchPattern, searchPattern) } + if category != "" { + conditions = append(conditions, `business_type = ?`) + args = append(args, category) + } + + if status != "" { + conditions = append(conditions, `status = ?`) + args = append(args, status) + } + whereClause := "" if len(conditions) > 0 { whereClause = " WHERE " + strings.Join(conditions, " AND ") @@ -113,7 +129,7 @@ func (h *Handler) MerchantManagementDataHandler(w http.ResponseWriter, r *http.R 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 + JOIN merchants m ON t.merchant_id = m.user_id WHERE m.merchant_id IN (%s)`, strings.Join(placeholders, ",")) termRows, err := h.DB.Query(termQuery, termArgs...) diff --git a/backend/internal/admin/admin_terminal.go b/backend/internal/admin/admin_terminal.go index c7ab7b7..a62c910 100644 --- a/backend/internal/admin/admin_terminal.go +++ b/backend/internal/admin/admin_terminal.go @@ -16,7 +16,11 @@ import ( func (h *Handler) TerminalRegistryView(w http.ResponseWriter, r *http.Request) { log.Println("TerminalRegistryView running...") - h.Tpl.ExecuteTemplate(w, "admin_terminal.html", nil) + data := AdminPageData{ + Page: "terminals", + Username: r.PathValue("username"), + } + h.Tpl.ExecuteTemplate(w, "admin_terminal.html", data) } func (h *Handler) TerminalRegistryDataHandler(w http.ResponseWriter, r *http.Request) { @@ -26,6 +30,8 @@ func (h *Handler) TerminalRegistryDataHandler(w http.ResponseWriter, r *http.Req pageStr := r.URL.Query().Get("page") limitStr := r.URL.Query().Get("limit") search := r.URL.Query().Get("search") + status := r.URL.Query().Get("status") + sortOrder := r.URL.Query().Get("sort") page := 1 limit := 10 @@ -39,7 +45,7 @@ func (h *Handler) TerminalRegistryDataHandler(w http.ResponseWriter, r *http.Req offset := (page - 1) * limit // Build query - baseQuery := `FROM terminals t LEFT JOIN merchants m ON t.merchant_id = m.id` + baseQuery := `FROM terminals t LEFT JOIN merchants m ON t.merchant_id = m.user_id` var args []interface{} var conditions []string @@ -49,6 +55,11 @@ func (h *Handler) TerminalRegistryDataHandler(w http.ResponseWriter, r *http.Req args = append(args, searchPattern, searchPattern, searchPattern) } + if status != "" { + conditions = append(conditions, `t.status = ?`) + args = append(args, status) + } + whereClause := "" if len(conditions) > 0 { whereClause = " WHERE " + strings.Join(conditions, " AND ") @@ -68,7 +79,10 @@ func (h *Handler) TerminalRegistryDataHandler(w http.ResponseWriter, r *http.Req // 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 ` + + if strings.ToLower(sortOrder) == "asc" { + orderClause = " ORDER BY t.created_at ASC" + } + query := `SELECT t.terminal_id, t.terminal_sn, COALESCE(m.business_name, 'Unassigned / Inventory'), t.device_name, COALESCE(t.location_details, 'Not Set'), t.status ` + baseQuery + whereClause + orderClause + ` LIMIT ? OFFSET ?` args = append(args, limit, offset) @@ -87,7 +101,7 @@ func (h *Handler) TerminalRegistryDataHandler(w http.ResponseWriter, r *http.Req 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 { + if err := rows.Scan(&t.TerminalID, &t.TerminalSN, &t.AssignedMerch, &t.DeviceName, &t.LocationDetails, &t.Status); err != nil { log.Println("Error scanning terminal:", err) continue } @@ -97,18 +111,26 @@ func (h *Handler) TerminalRegistryDataHandler(w http.ResponseWriter, r *http.Req terminals = []structs.Terminal{} } + var activeCount, inactiveCount int + h.DB.QueryRow(`SELECT COUNT(*) FROM terminals WHERE status IN ('active', 'online')`).Scan(&activeCount) + h.DB.QueryRow(`SELECT COUNT(*) FROM terminals WHERE status IN ('inactive', 'offline')`).Scan(&inactiveCount) + type PaginatedTerminalResponse struct { - Terminals []structs.Terminal `json:"terminals"` - TotalItems int `json:"totalItems"` - Page int `json:"page"` - Limit int `json:"limit"` + Terminals []structs.Terminal `json:"terminals"` + TotalItems int `json:"totalItems"` + Page int `json:"page"` + Limit int `json:"limit"` + ActiveCount int `json:"activeCount"` + InactiveCount int `json:"inactiveCount"` } terminalData := PaginatedTerminalResponse{ - Terminals: terminals, - TotalItems: totalItems, - Page: page, - Limit: limit, + Terminals: terminals, + TotalItems: totalItems, + Page: page, + Limit: limit, + ActiveCount: activeCount, + InactiveCount: inactiveCount, } jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ @@ -145,12 +167,12 @@ func (h *Handler) AddTerminalHandler(w http.ResponseWriter, r *http.Request) { } // Generate a unique terminal ID - timestamp := time.Now().Format("06010405") - nTerminal, _ := rand.Int(rand.Reader, big.NewInt(100000)) + timestamp := time.Now().Format("01020605") // MMDDYYss + nTerminal, _ := rand.Int(rand.Reader, big.NewInt(10000)) // max 9999 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')` + query := `INSERT INTO terminals (terminal_id, terminal_sn, merchant_id, device_name, status) VALUES (?, ?, NULL, ?, 'inactive')` _, err := h.DB.Exec(query, terminalID, req.TerminalSN, req.DeviceName) if err != nil { log.Printf("Error inserting standalone terminal: %v", err) @@ -166,3 +188,41 @@ func (h *Handler) AddTerminalHandler(w http.ResponseWriter, r *http.Request) { Message: "Terminal registered to inventory successfully", }) } + +type UnassignedTerminalData struct { + TerminalSN string `json:"terminal_sn"` + DeviceName string `json:"device_name"` + Status string `json:"status"` +} + +func (h *Handler) GetUnassignedTerminalsHandler(w http.ResponseWriter, r *http.Request) { + rows, err := h.DB.Query(` + SELECT terminal_sn, device_name, status + FROM terminals + WHERE merchant_id IS NULL AND status = 'inactive' + `) + if err != nil { + log.Printf("Error fetching unassigned terminals: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Database error", + }) + return + } + defer rows.Close() + + var terminals []UnassignedTerminalData + for rows.Next() { + var t UnassignedTerminalData + if err := rows.Scan(&t.TerminalSN, &t.DeviceName, &t.Status); err != nil { + log.Printf("Row scan error: %v", err) + continue + } + terminals = append(terminals, t) + } + + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: true, + Data: terminals, + }) +} diff --git a/backend/internal/admin/card_inventory.go b/backend/internal/admin/card_inventory.go index 4a9f641..7b2c8e1 100644 --- a/backend/internal/admin/card_inventory.go +++ b/backend/internal/admin/card_inventory.go @@ -29,8 +29,11 @@ type AdminCardInventoryStats struct { // CardInventoryView handles rendering the static admin dashboard view func (h *Handler) CardInventoryView(w http.ResponseWriter, r *http.Request) { fmt.Println("CardInventoryView running...") - - err := h.Tpl.ExecuteTemplate(w, "admin_dashboard.html", nil) + data := AdminPageData{ + Page: "card-inventory", + Username: r.PathValue("username"), + } + err := h.Tpl.ExecuteTemplate(w, "admin_dashboard.html", data) if err != nil { fmt.Printf("Template execution error: %v\n", err) } diff --git a/backend/internal/admin/deactivate_card.go b/backend/internal/admin/deactivate_card.go index 70a309b..37dc142 100644 --- a/backend/internal/admin/deactivate_card.go +++ b/backend/internal/admin/deactivate_card.go @@ -18,8 +18,11 @@ type CardData = structure.CardData func (h *Handler) DeactivateView(w http.ResponseWriter, r *http.Request) { fmt.Println("DeactivateView running...") - - h.Tpl.ExecuteTemplate(w, "deactivateCard.html", nil) + data := AdminPageData{ + Page: "deactivatecard", + Username: r.PathValue("username"), + } + h.Tpl.ExecuteTemplate(w, "deactivateCard.html", data) } // DeactivateCardHanlder handles deactivating a card and returns a JSON response. diff --git a/backend/internal/admin/delete_card.go b/backend/internal/admin/delete_card.go index b866e9a..a07669d 100644 --- a/backend/internal/admin/delete_card.go +++ b/backend/internal/admin/delete_card.go @@ -14,8 +14,11 @@ import ( func (h *Handler) DeleteCardView(w http.ResponseWriter, r *http.Request) { fmt.Println("DeleteCardView running...") - - h.Tpl.ExecuteTemplate(w, "delete_card.html", nil) + data := AdminPageData{ + Page: "delete-cards", + Username: r.PathValue("username"), + } + h.Tpl.ExecuteTemplate(w, "delete_card.html", data) } // DeleteCardHandler handles deleting a card by card_number and returns JSON. diff --git a/backend/internal/admin/super_admin_pages.go b/backend/internal/admin/super_admin_pages.go index f8b7b45..a8b0929 100644 --- a/backend/internal/admin/super_admin_pages.go +++ b/backend/internal/admin/super_admin_pages.go @@ -12,7 +12,8 @@ var Validate = validator.New() // PlatformOverviewView serves the new Super Admin Platform Overview func (h *Handler) PlatformOverviewView(w http.ResponseWriter, r *http.Request) { - err := h.Tpl.ExecuteTemplate(w, "platform_overview.html", nil) + data := AdminPageData{Page: "dashboard", Username: r.PathValue("username")} + err := h.Tpl.ExecuteTemplate(w, "platform_overview.html", data) if err != nil { log.Printf("Template execution error: %v", err) jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ @@ -24,7 +25,8 @@ func (h *Handler) PlatformOverviewView(w http.ResponseWriter, r *http.Request) { // MerchantManagementView serves the Merchant Management page func (h *Handler) MerchantManagementViews(w http.ResponseWriter, r *http.Request) { - err := h.Tpl.ExecuteTemplate(w, "merchant_management.html", nil) + data := AdminPageData{Page: "merchants", Username: r.PathValue("username")} + err := h.Tpl.ExecuteTemplate(w, "merchant_management.html", data) if err != nil { log.Printf("Template execution error: %v", err) jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ @@ -36,7 +38,8 @@ func (h *Handler) MerchantManagementViews(w http.ResponseWriter, r *http.Request // TerminalRegistryView serves the Hardware Registry page func (h *Handler) TerminalRegistryViews(w http.ResponseWriter, r *http.Request) { - err := h.Tpl.ExecuteTemplate(w, "hardware_registry.html", nil) + data := AdminPageData{Page: "terminals", Username: r.PathValue("username")} + err := h.Tpl.ExecuteTemplate(w, "hardware_registry.html", data) if err != nil { log.Printf("Template execution error: %v", err) jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ @@ -48,7 +51,8 @@ func (h *Handler) TerminalRegistryViews(w http.ResponseWriter, r *http.Request) // SystemSettingsView serves the System Settings page func (h *Handler) SystemSettingsView(w http.ResponseWriter, r *http.Request) { - err := h.Tpl.ExecuteTemplate(w, "system_settings.html", nil) + data := AdminPageData{Page: "settings", Username: r.PathValue("username")} + err := h.Tpl.ExecuteTemplate(w, "system_settings.html", data) if err != nil { log.Printf("Template execution error: %v", err) jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ diff --git a/backend/internal/auth/login.go b/backend/internal/auth/login.go index 39475c4..36f5b70 100644 --- a/backend/internal/auth/login.go +++ b/backend/internal/auth/login.go @@ -106,10 +106,10 @@ func (h *Handler) LoginAuthHandler(w http.ResponseWriter, r *http.Request) { } // Determine redirect based on role - redirectURL := "/dashboard?user=" + userName // Default for customer + redirectURL := "/" + userName // Default for customer switch role { case "super_admin": - redirectURL = "/admin/platform-overview" // Super admin dashboard + redirectURL = "/admin/" + userName // Super admin dashboard case "merchant_admin", "merchant_staff": redirectURL = "/merchant/dashboard" // Merchant dashboard } diff --git a/backend/internal/pkg/structs/struct.go b/backend/internal/pkg/structs/struct.go index 5ee8f8c..963a153 100644 --- a/backend/internal/pkg/structs/struct.go +++ b/backend/internal/pkg/structs/struct.go @@ -39,8 +39,9 @@ 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"` + DeviceName string `json:"device_name"` + LocationDetails string `json:"location_details"` + Status string `json:"status"` } // Transaction struct represents a user's transaction for the dashboard view diff --git a/backend/internal/user/dashboard.go b/backend/internal/user/dashboard.go index 3fb2abd..1d605f8 100644 --- a/backend/internal/user/dashboard.go +++ b/backend/internal/user/dashboard.go @@ -46,8 +46,8 @@ 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 user ID from query param (No cookies for now) - userID := r.URL.Query().Get("user") + // Get user ID from path param (No cookies for now) + userID := r.PathValue("username") if userID == "" { jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ Success: false, diff --git a/backend/tmp_app.exe b/backend/tmp_app.exe new file mode 100644 index 0000000..4244a53 Binary files /dev/null and b/backend/tmp_app.exe differ diff --git a/docs/unicardv1.sql b/docs/unicardv1.sql index 041e5b5..9f70d93 100644 --- a/docs/unicardv1.sql +++ b/docs/unicardv1.sql @@ -1,20 +1,8 @@ -- CREATE DATABASE IF NOT EXISTS unicardv1; --- USE unicardv1; +-- USE unicardv2; -- ========================================================================= --- 1. LOW-GROWTH / INDEPENDENT LOOKUP TABLES --- ========================================================================= - -CREATE TABLE regions ( - id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal region numeric auto-increment look-up index', - region_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom public identifier for the region tier data metrics (e.g., REG-01)', - region_name VARCHAR(100) NOT NULL COMMENT 'The official geographic regional designation name (e.g., Central Luzon, NCR)', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated timestamp logging when the target region entity was first added', - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Automatically monitors administrative adjustments or title updates over time' -) COMMENT='Geographic metadata region lookup entity driving system analytics and localized metric calculations'; - --- ========================================================================= --- 2. CORE IDENTITY & AUTHENTICATION TABLES +-- 1. CORE IDENTITY & AUTHENTICATION TABLES -- ========================================================================= CREATE TABLE users ( @@ -27,15 +15,13 @@ CREATE TABLE users ( password_hash VARCHAR(255) NOT NULL COMMENT 'Cryptographically secured password string handled via bcrypt in Go', role ENUM('super_admin', 'merchant_admin', 'merchant_staff', 'customer') NOT NULL COMMENT 'Defines application-wide role-based access control', status ENUM('active', 'suspended', 'inactive') DEFAULT 'active' COMMENT 'Account access state for platform security and compliance checks', - region_id INT NULL COMMENT 'Links customer location to a specific region lookup like Central Luzon', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated timestamp of account creation', - FOREIGN KEY (region_id) REFERENCES regions(id) ON DELETE SET NULL + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated timestamp of account creation' ) COMMENT='Core identity table tracking authentication and tenancy access control levels'; CREATE TABLE system_settings ( - setting_key VARCHAR(50) PRIMARY KEY COMMENT 'Unique string configuration key acting as the primary look-up token (e.g., global_topup_fee, min_card_balance)', - setting_value VARCHAR(255) NOT NULL COMMENT 'The active parameter threshold or value parsed directly by the Go backend business logic engines', - description TEXT NULL COMMENT 'Descriptive documentation notes detailing exactly what system rules or parameters this configuration key alters', + setting_key VARCHAR(50) PRIMARY KEY COMMENT 'Unique string configuration key acting as the primary look-up token', + setting_value VARCHAR(255) NOT NULL COMMENT 'The active parameter threshold or value parsed directly by the Go backend', + description TEXT NULL COMMENT 'Descriptive documentation notes detailing exactly what system rules or parameters this alters', updated_by VARCHAR(50) NOT NULL COMMENT 'The public users.user_id of the Super Admin who executed the latest configuration adjustment override', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated clock timestamp tracking when this specific configuration parameter was initialized', updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Automatically locks the exact clock timestamp whenever this system parameter value is updated', @@ -43,7 +29,7 @@ CREATE TABLE system_settings ( ) COMMENT='Global platform configuration matrix driving dynamic fees, operational bounds, and system constants'; -- ========================================================================= --- 3. MERCHANT TENANCY & HARDWARE REGISTRY TABLES +-- 2. MERCHANT TENANCY & HARDWARE REGISTRY TABLES -- ========================================================================= CREATE TABLE merchants ( @@ -54,7 +40,7 @@ CREATE TABLE merchants ( business_registration_number VARCHAR(100) NULL UNIQUE COMMENT 'Official government tracking number (e.g., DTI, SEC, or BIR TIN)', business_address TEXT NOT NULL COMMENT 'Physical location of the main store or corporate headquarters', - owner_user_id VARCHAR(50) NOT NULL COMMENT 'Links to the user_id in the users table who owns this business account', + user_id VARCHAR(50) NOT NULL COMMENT 'Links to the user_id in the users table who owns this business account', owner_name VARCHAR(100) NOT NULL COMMENT 'Full name of the principal owner or authorized business representative', business_email VARCHAR(100) NOT NULL UNIQUE COMMENT 'Official company contact email address for corporate updates and billing statements', business_phone VARCHAR(20) NOT NULL UNIQUE COMMENT 'Official telephone or mobile number for merchant support and emergency updates', @@ -70,7 +56,7 @@ CREATE TABLE merchants ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated date and time record of the initial registration request', updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Automatically updates whenever any merchant profile field is modified', - FOREIGN KEY (owner_user_id) REFERENCES users(user_id) ON DELETE RESTRICT, + FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE RESTRICT, FOREIGN KEY (approved_by) REFERENCES users(user_id) ON DELETE SET NULL ) COMMENT='Enterprise business registry tracking partner tenants, hardware mapping nodes, and financial settlement details'; @@ -78,18 +64,18 @@ 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 NULL COMMENT 'Links to the internal auto-increment id of the managing merchant entity', + merchant_id varchar(50) 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', + status ENUM('active', 'suspended', 'inactive') DEFAULT 'inactive' COMMENT 'Operational network connectivity state of the edge node hardware', last_heartbeat TIMESTAMP NULL COMMENT 'Tracks the precise timestamp of the last successful ping packet received from the ESP32 network stack', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated clock timestamp tracking initial edge device registration', updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Automatically monitors configuration adjustments or state transitions over time', - FOREIGN KEY (merchant_id) REFERENCES merchants(id) ON DELETE CASCADE + FOREIGN KEY (merchant_id) REFERENCES merchants(user_id) ON DELETE CASCADE ) COMMENT='Hardware node registry tracking deployed physical authentication nodes and network heartbeat states'; -- ========================================================================= --- 4. UTILITY & USER TRANSACTION LOGS TABLES (HIGH GROWING DATASETS) +-- 3. UTILITY & USER TRANSACTION LOGS TABLES (HIGH GROWING DATASETS) -- ========================================================================= CREATE TABLE cards ( @@ -103,6 +89,7 @@ CREATE TABLE cards ( status ENUM('active', 'inactive', 'blocked', 'lost') DEFAULT 'inactive' COMMENT 'Lifecycle block status constraint to instantly freeze stolen or missing tokens', expiry_date DATE NOT NULL COMMENT 'Expiration threshold date determining card block lifecycle validations', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Timestamp tracking when the physical token record was registered in inventory', + linked_at TIMESTAMP NULL DEFAULT NULL COMMENT 'Timestamp tracking the exact moment the card was registered to a specific user', updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Automatically updates when balances adjust or card status switches occur', FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE SET NULL ) COMMENT='Ecosystem transit wallet asset tracker maintaining balances, hardware mapping tokens, and fare tier flags'; @@ -111,8 +98,8 @@ CREATE TABLE transactions ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal financial primary index scaled to 64-bit headroom to comfortably support billions of platform entries', transaction_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom unique public reference string (e.g., TXN-2026-104294) printed on digital and paper receipts', card_number VARCHAR(20) NOT NULL COMMENT 'Links target token balance deduction via cards.card_number', - merchant_id INT NOT NULL COMMENT 'Identifies vendor company collecting the payment token via merchants.id', - terminal_id INT NOT NULL COMMENT 'Identifies physical ESP32 or terminal node hardware unit triggering the capture via terminals.id', + merchant_id varchar(50) NOT NULL COMMENT 'Identifies vendor company collecting the payment token via merchants.id', + terminal_id varchar(50) NOT NULL COMMENT 'Identifies physical ESP32 or terminal node hardware unit triggering the capture via terminals.id', transaction_type ENUM('payment', 'refund', 'reversal') DEFAULT 'payment' COMMENT 'Categorizes ledger records to process standard deductions or transaction void mappings cleanly', amount DECIMAL(10, 2) NOT NULL COMMENT 'Total Gross fiat amount captured from the card wallet balance tracking column', service_fee DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Platform revenue slice collected by UniCard ecosystem engine per tap processing action', @@ -120,8 +107,8 @@ CREATE TABLE transactions ( processed_by VARCHAR(50) NOT NULL COMMENT 'Public string identifier users.user_id capturing the identity of the physical staff member operating the payment client terminal', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Cryptographic server node timestamp securing exactly when transaction settlement clearing finalized', FOREIGN KEY (card_number) REFERENCES cards(card_number), - FOREIGN KEY (merchant_id) REFERENCES merchants(id), - FOREIGN KEY (terminal_id) REFERENCES terminals(id), + FOREIGN KEY (merchant_id) REFERENCES merchants(user_id), + FOREIGN KEY (terminal_id) REFERENCES terminals(terminal_id), FOREIGN KEY (processed_by) REFERENCES users(user_id) ) COMMENT='High-growth financial master ledger capturing all terminal token taps, transaction classifications, and system fees'; @@ -131,6 +118,8 @@ CREATE TABLE top_ups ( card_number VARCHAR(20) NOT NULL COMMENT 'Links target token balance injection via cards.card_number', amount DECIMAL(10, 2) NOT NULL COMMENT 'Gross load amount requested by the customer before convenience charges are applied', convenience_fee DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Ecosystem engine collection fee applied to over-the-air channels like GCash or Maya webhooks', + gateway_cost DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Actual fee incurred from external payment providers (GCash, Maya, Bank) per transaction', + net_gateway_fee DECIMAL(10, 2) GENERATED ALWAYS AS (convenience_fee - gateway_cost) STORED COMMENT 'Automatically calculated net revenue kept by the platform after 3rd party costs', total_charged DECIMAL(10, 2) GENERATED ALWAYS AS (amount + convenience_fee) STORED COMMENT 'Automatically calculated column representing the absolute total cash value collected from the external channel source', payment_method ENUM('cash', 'gcash', 'maya', 'over_the_counter') NOT NULL COMMENT 'Drives system tracking to audit cash-drawer liquid positions against programmatic API callbacks', handled_by VARCHAR(50) NULL COMMENT 'Public user_id string identifier referencing the administrative staff member who manually accepted physical bills if OTC cash-loaded', diff --git a/frontend/assets/admin/add_card.js b/frontend/assets/admin/add_card.js index 884c737..945cda7 100644 --- a/frontend/assets/admin/add_card.js +++ b/frontend/assets/admin/add_card.js @@ -22,7 +22,8 @@ document.addEventListener("DOMContentLoaded", function () { const cardUID = document.getElementById("cardUID").value; const initialAmount = document.getElementById("initialAmount").value; - fetch("/v1/admin/addcardauth", { + const adminUsername = window.location.pathname.split('/')[2]; + fetch(`/v1/admin/${adminUsername}/addcardauth`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ diff --git a/frontend/assets/admin/admin_dashboard.js b/frontend/assets/admin/admin_dashboard.js index 530231f..db28033 100644 --- a/frontend/assets/admin/admin_dashboard.js +++ b/frontend/assets/admin/admin_dashboard.js @@ -1,5 +1,6 @@ document.addEventListener("DOMContentLoaded", function () { - fetch('/v1/admin/dashboard-data') + const adminUsername = window.location.pathname.split('/')[2]; + fetch(`/v1/admin/${adminUsername}/dashboard-data`) .then(response => response.json()) .then(data => { if (data.success) { @@ -16,7 +17,26 @@ document.addEventListener("DOMContentLoaded", function () { if (data.data.merchants && data.data.merchants.length > 0) { data.data.merchants.forEach(m => { const tr = document.createElement('tr'); - tr.className = 'hover:bg-gray-50'; + tr.className = 'hover:bg-gray-50 cursor-pointer transition duration-150'; + + tr.onclick = () => { + document.getElementById('modalBusinessName').textContent = m.business_name; + document.getElementById('modalMerchantId').textContent = m.merchant_id; + document.getElementById('modalBusinessType').textContent = m.business_type.replace(/_/g, ' '); + document.getElementById('modalOwnerName').textContent = m.owner_name; + document.getElementById('modalContactEmail').textContent = m.business_email; + document.getElementById('modalContactPhone').textContent = m.business_phone; + + const statusEl = document.getElementById('modalStatus'); + statusEl.textContent = m.status.replace(/_/g, ' '); + statusEl.className = 'capitalize px-2 py-1 text-xs font-medium rounded-full'; + if (m.status === 'active') statusEl.classList.add('bg-green-100', 'text-green-800'); + else if (m.status === 'pending_approval') statusEl.classList.add('bg-yellow-100', 'text-yellow-800'); + else statusEl.classList.add('bg-red-100', 'text-red-800'); + + document.getElementById('modalCreatedAt').textContent = new Date(m.created_at).toLocaleDateString(); + document.getElementById('merchantDetailsModal').classList.remove('hidden'); + }; // Status badge styling let statusColor = 'bg-gray-100 text-gray-800'; @@ -29,21 +49,16 @@ document.addEventListener("DOMContentLoaded", function () { } tr.innerHTML = ` - -
-
- -
-
-
${m.business_name}
-
ID: ${m.merchant_id}
-
+ +
+
${m.business_name}
+
ID: ${m.merchant_id}
- ${m.business_type.replace(/_/g, ' ')} - -
${m.owner_name}
-
${m.business_email}
+ ${m.business_type.replace(/_/g, ' ')} + +
${m.owner_name}
+
${m.business_email}
${m.business_phone} diff --git a/frontend/assets/admin/admin_merchant.js b/frontend/assets/admin/admin_merchant.js index c04f5b5..4a7e13f 100644 --- a/frontend/assets/admin/admin_merchant.js +++ b/frontend/assets/admin/admin_merchant.js @@ -2,18 +2,60 @@ let currentPage = 1; // current page const itemsPerPage = 10; // items per page let currentSearchQuery = ''; let currentSortOrder = 'desc'; +let currentCategory = ''; +let currentStatus = ''; let totalItemsCount = 0; let currentMerchants = []; +let unassignedTerminals = []; + +window.renderAssignedTerminals = function(terminals) { + if (!terminals || terminals.length === 0) { + return 'No terminals'; + } + return terminals.map(t => { + return `
+
${t.device_name || t.terminal_id}
+
SN: ${t.terminal_sn}
+
`; + }).join(''); +}; + +function fetchUnassignedTerminals() { + const adminUsername = window.location.pathname.split('/')[2]; + fetch(`/v1/admin/${adminUsername}/terminals/unassigned`) + .then(res => res.json()) + .then(result => { + if (result.success && result.data) { + unassignedTerminals = result.data; + document.querySelectorAll('.terminal-sn-select').forEach(populateTerminalDropdown); + } + }) + .catch(error => console.error("Error fetching unassigned terminals", error)); +} + +function populateTerminalDropdown(selectElement) { + selectElement.innerHTML = ''; + unassignedTerminals.forEach(t => { + const opt = document.createElement('option'); + opt.value = t.terminal_sn; + opt.textContent = t.terminal_sn; + opt.dataset.deviceName = t.device_name; + selectElement.appendChild(opt); + }); +} function fetchMerchants() { const queryParams = new URLSearchParams({ page: currentPage, limit: itemsPerPage, search: currentSearchQuery, - sort: currentSortOrder + sort: currentSortOrder, + category: currentCategory, + status: currentStatus }); - fetch(`/v1/admin/merchants-data?${queryParams.toString()}`) + const adminUsername = window.location.pathname.split('/')[2]; + fetch(`/v1/admin/${adminUsername}/merchants-data?${queryParams.toString()}`) .then(response => response.json()) .then(result => { if (result.success && result.data) { @@ -75,23 +117,38 @@ function renderTable() { const highlightedBusinessName = highlightText(merchant.business_name, queryTerms); const highlightedMerchantId = highlightText(merchant.merchant_id, queryTerms); const highlightedOwnerName = highlightText(merchant.owner_name, queryTerms); + const highlightedEmail = highlightText(merchant.business_email, queryTerms); + + tr.className = 'hover:bg-gray-50 cursor-pointer transition duration-150'; + tr.onclick = (e) => { + if (e.target.closest('button')) return; // Ignore button clicks + document.getElementById('modalBusinessName').textContent = merchant.business_name; + document.getElementById('modalMerchantId').textContent = merchant.merchant_id; + document.getElementById('modalBusinessType').textContent = merchant.business_type.replace(/_/g, ' '); + document.getElementById('modalOwnerName').textContent = merchant.owner_name; + document.getElementById('modalContactEmail').textContent = merchant.business_email; + document.getElementById('modalContactPhone').textContent = merchant.business_phone; + + const statusEl = document.getElementById('modalStatus'); + statusEl.textContent = merchant.status.replace(/_/g, ' '); + statusEl.className = 'capitalize px-2 py-1 text-xs font-medium rounded-full'; + if (merchant.status.toLowerCase() === 'active') statusEl.classList.add('bg-green-100', 'text-green-800'); + else if (merchant.status.toLowerCase() === 'pending_approval') statusEl.classList.add('bg-yellow-100', 'text-yellow-800'); + else statusEl.classList.add('bg-red-100', 'text-red-800'); + + document.getElementById('modalCreatedAt').textContent = new Date(merchant.created_at).toLocaleDateString(); + document.getElementById('merchantDetailsModal').classList.remove('hidden'); + }; tr.innerHTML = ` - -
-
- -
-
-
${highlightedBusinessName}
-
ID: ${highlightedMerchantId}
-
-
+ +
${highlightedBusinessName}
+
ID: ${highlightedMerchantId}
- ${merchant.business_type.replace(/_/g, ' ')} - -
${highlightedOwnerName}
-
${merchant.business_email}
+ ${merchant.business_type.replace(/_/g, ' ')} + +
${highlightedOwnerName}
+
${highlightedEmail}
@@ -151,6 +208,36 @@ function renderPagination() { document.addEventListener('DOMContentLoaded', () => { fetchMerchants(); + fetchUnassignedTerminals(); + + // Delegate change event for terminal selection + const container = document.getElementById('merchantBlocksContainer'); + container.addEventListener('change', function(e) { + if (e.target.classList.contains('terminal-sn-select')) { + const selectedOption = e.target.options[e.target.selectedIndex]; + const block = e.target.closest('.merchant-block'); + const deviceNameInput = block.querySelector('.device-name-input'); + if (selectedOption && selectedOption.dataset.deviceName) { + deviceNameInput.value = selectedOption.dataset.deviceName; + } else { + deviceNameInput.value = ''; + } + } + }); + + // Auto-format fields on focus out to trim spaces and format as Title Case + container.addEventListener('focusout', function(e) { + if (e.target.tagName === 'INPUT') { + let val = e.target.value; + if (e.target.type === 'text' && !['businessPhone', 'commissionRate', 'registrationNum', 'deviceName'].includes(e.target.name)) { + e.target.value = val.trim().toLowerCase().replace(/\b\w/g, c => c.toUpperCase()); + } else if (e.target.type === 'email') { + e.target.value = val.trim().toLowerCase(); + } else { + e.target.value = val.trim(); + } + } + }); const searchInput = document.getElementById('searchInput'); let searchTimeout = null; @@ -173,13 +260,34 @@ document.addEventListener('DOMContentLoaded', () => { }); } + const filterCategory = document.getElementById('filterCategory'); + if (filterCategory) { + filterCategory.addEventListener('change', (e) => { + currentCategory = e.target.value; + applyFiltersAndSort(); + }); + } + + const filterStatus = document.getElementById('filterStatus'); + if (filterStatus) { + filterStatus.addEventListener('change', (e) => { + currentStatus = e.target.value; + applyFiltersAndSort(); + }); + } + const resetFiltersBtn = document.getElementById('resetFilters'); if (resetFiltersBtn) { resetFiltersBtn.addEventListener('click', () => { if (searchInput) searchInput.value = ''; if (sortOrder) sortOrder.value = 'desc'; + if (filterCategory) filterCategory.value = ''; + if (filterStatus) filterStatus.value = ''; + currentSearchQuery = ''; currentSortOrder = 'desc'; + currentCategory = ''; + currentStatus = ''; applyFiltersAndSort(); }); } @@ -236,7 +344,8 @@ document.getElementById('onboardForm').addEventListener('submit', function (e) { const alertBox = document.getElementById('formAlert'); alertBox.classList.add('hidden'); - fetch('/v1/admin/merchants/add', { + const adminUsername = window.location.pathname.split('/')[2]; + fetch(`/v1/admin/${adminUsername}/merchants/add`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(merchantsData) diff --git a/frontend/assets/admin/admin_terminal.js b/frontend/assets/admin/admin_terminal.js index 9fe70d7..5d3616d 100644 --- a/frontend/assets/admin/admin_terminal.js +++ b/frontend/assets/admin/admin_terminal.js @@ -20,9 +20,12 @@ document.addEventListener("DOMContentLoaded", () => { let currentPage = 1; const limit = 10; let currentSearch = ""; + let currentSortOrder = "desc"; + let currentStatus = ""; - function fetchTerminals(page = 1, search = "") { - const url = `/v1/admin/terminals-data?page=${page}&limit=${limit}&search=${encodeURIComponent(search)}`; + function fetchTerminals(page = 1, search = "", sort = currentSortOrder, status = currentStatus) { + const adminUsername = window.location.pathname.split('/')[2]; + const url = `/v1/admin/${adminUsername}/terminals-data?page=${page}&limit=${limit}&search=${encodeURIComponent(search)}&sort=${sort}&status=${status}`; fetch(url) .then(res => res.json()) @@ -30,6 +33,11 @@ document.addEventListener("DOMContentLoaded", () => { if (data.success) { renderTable(data.data.terminals || []); updatePagination(data.data.totalItems, data.data.page, data.data.limit); + + const activeCountEl = document.getElementById("activeTerminalCount"); + const inactiveCountEl = document.getElementById("inactiveTerminalCount"); + if (activeCountEl) activeCountEl.textContent = data.data.activeCount || 0; + if (inactiveCountEl) inactiveCountEl.textContent = data.data.inactiveCount || 0; } else { console.error("Failed to fetch terminals:", data.message); } @@ -48,9 +56,9 @@ document.addEventListener("DOMContentLoaded", () => { 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'; + : terminal.status === 'inactive' || terminal.status === 'Inactive' + ? 'bg-yellow-100 text-yellow-800' + : 'bg-red-100 text-red-800'; const statusText = terminal.status.charAt(0).toUpperCase() + terminal.status.slice(1); @@ -61,20 +69,30 @@ document.addEventListener("DOMContentLoaded", () => { const iconBg = statusText === 'Online' || statusText === 'Active' ? 'bg-blue-100 text-blue-600' : 'bg-gray-100 text-gray-600'; const row = document.createElement('tr'); + row.className = 'hover:bg-gray-50 cursor-pointer transition duration-150'; + row.onclick = (e) => { + if (e.target.closest('button')) return; // Ignore button clicks + document.getElementById('modalDeviceName').textContent = terminal.device_name; + document.getElementById('modalTerminalId').textContent = terminal.terminal_id; + document.getElementById('modalTerminalSn').textContent = terminal.terminal_sn; + document.getElementById('modalAssignedMerchant').textContent = terminal.assigned_merchant; + document.getElementById('modalLocationDetails').textContent = terminal.location_details || 'Not Set'; + + const statusEl = document.getElementById('modalTerminalStatus'); + statusEl.textContent = statusText; + statusEl.className = `capitalize px-2 py-1 text-xs font-medium rounded-full ${statusClass}`; + + document.getElementById('terminalDetailsModal').classList.remove('hidden'); + }; + row.innerHTML = ` -
-
- -
-
-
${terminal.terminal_id}
-
SN: ${terminal.terminal_sn}
-
-
+
${terminal.terminal_id}
+
SN: ${terminal.terminal_sn}
- ${terminal.assigned_merchant} - ${terminal.device_name} + ${terminal.assigned_merchant} + ${terminal.device_name} + ${terminal.location_details || 'Not Set'} ${statusText} @@ -114,12 +132,12 @@ document.addEventListener("DOMContentLoaded", () => { prevPageBtn.addEventListener("click", () => { if (currentPage > 1) { - fetchTerminals(currentPage - 1, currentSearch); + fetchTerminals(currentPage - 1, currentSearch, currentSortOrder, currentStatus); } }); nextPageBtn.addEventListener("click", () => { - fetchTerminals(currentPage + 1, currentSearch); + fetchTerminals(currentPage + 1, currentSearch, currentSortOrder, currentStatus); }); // Debounce search input @@ -128,10 +146,40 @@ document.addEventListener("DOMContentLoaded", () => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { currentSearch = e.target.value.trim(); - fetchTerminals(1, currentSearch); + fetchTerminals(1, currentSearch, currentSortOrder, currentStatus); }, 300); }); + const sortOrder = document.getElementById("sortOrder"); + if (sortOrder) { + sortOrder.addEventListener("change", (e) => { + currentSortOrder = e.target.value; + fetchTerminals(1, currentSearch, currentSortOrder, currentStatus); + }); + } + + const filterStatus = document.getElementById("filterStatus"); + if (filterStatus) { + filterStatus.addEventListener("change", (e) => { + currentStatus = e.target.value; + fetchTerminals(1, currentSearch, currentSortOrder, currentStatus); + }); + } + + const resetFiltersBtn = document.getElementById("resetFilters"); + if (resetFiltersBtn) { + resetFiltersBtn.addEventListener("click", () => { + if (searchInput) searchInput.value = ""; + if (sortOrder) sortOrder.value = "desc"; + if (filterStatus) filterStatus.value = ""; + + currentSearch = ""; + currentSortOrder = "desc"; + currentStatus = ""; + fetchTerminals(1, currentSearch, currentSortOrder, currentStatus); + }); + } + const addTerminalForm = document.getElementById("addTerminalForm"); if (addTerminalForm) { addTerminalForm.addEventListener("submit", (e) => { @@ -145,7 +193,8 @@ document.addEventListener("DOMContentLoaded", () => { const alertBox = document.getElementById("terminalFormAlert"); alertBox.classList.add("hidden"); - fetch('/v1/admin/terminals/add', { + const adminUsername = window.location.pathname.split('/')[2]; + fetch(`/v1/admin/${adminUsername}/terminals/add`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) @@ -160,7 +209,7 @@ document.addEventListener("DOMContentLoaded", () => { document.getElementById('addTerminalModal').classList.add('hidden'); addTerminalForm.reset(); alertBox.classList.add("hidden"); - fetchTerminals(1, currentSearch); + fetchTerminals(1, currentSearch, currentSortOrder, currentStatus); }, 1500); } else { alertBox.classList.add("bg-red-50", "text-red-600"); diff --git a/frontend/assets/admin/deactivate_card.js b/frontend/assets/admin/deactivate_card.js index c06086c..dc88adf 100644 --- a/frontend/assets/admin/deactivate_card.js +++ b/frontend/assets/admin/deactivate_card.js @@ -30,7 +30,8 @@ document.addEventListener("DOMContentLoaded", function () { cardType: cardTypeSelect.value }; - fetch("/v1/admin/deactivatecardauth", { + const adminUsername = window.location.pathname.split('/')[2]; + fetch(`/v1/admin/${adminUsername}/deactivatecardauth`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(bodyData) diff --git a/frontend/assets/js/dashboard.js b/frontend/assets/js/dashboard.js index 0f83626..4521b28 100644 --- a/frontend/assets/js/dashboard.js +++ b/frontend/assets/js/dashboard.js @@ -147,12 +147,19 @@ document.addEventListener("DOMContentLoaded", function () { // --- Fetch Dashboard Data --- function fetchDashboardData() { - const urlParams = new URLSearchParams(window.location.search); - const userId = urlParams.get('user'); - - let endpoint = "/v1/user/dashboard"; + const pathSegments = window.location.pathname.split('/'); + let userId = null; + if (pathSegments.length >= 2 && pathSegments[1] !== '') { + userId = pathSegments[1]; + } else { + // fallback if it's still somehow in query string + const urlParams = new URLSearchParams(window.location.search); + userId = urlParams.get('username'); + } + + let endpoint = "/v1/user/"; if (userId) { - endpoint += "?user=" + encodeURIComponent(userId); + endpoint += encodeURIComponent(userId); } fetch(endpoint) diff --git a/frontend/templates/admin/addCards.html b/frontend/templates/admin/addCards.html index 3c05a11..beaefe6 100644 --- a/frontend/templates/admin/addCards.html +++ b/frontend/templates/admin/addCards.html @@ -16,7 +16,7 @@
- {{template "admin_sidebar" "addcard"}} + {{template "admin_sidebar" .}}
@@ -127,7 +127,7 @@

Auto-Genera class="px-6 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition duration-150 shadow-sm"> Create Card - Cancel diff --git a/frontend/templates/admin/admin_dashboard.html b/frontend/templates/admin/admin_dashboard.html index c5f4f62..ed923e0 100644 --- a/frontend/templates/admin/admin_dashboard.html +++ b/frontend/templates/admin/admin_dashboard.html @@ -15,7 +15,7 @@
- {{template "admin_sidebar" "dashboard"}} + {{template "admin_sidebar" .}}
@@ -36,103 +36,44 @@

Dashboard

- +
-
+
-

Gross Revenue

-

₱0.00

+

Gross Revenue

+

₱0.00

-
- - - -
-
-
- - -
-
-

Net Revenue

-

₱0.00

-
-
- - - - +

Net Revenue

+

₱0.00

- +
-
+
-

Total Users

-

0

-
-
- - - - +

Total Users

+

0

-
-
- - -
-
-

Total Cards

-

0

-
-
- - - - +

Total Cards

+

0

- +
-
+
-

Active Merchants

-

0

-
-
- - - - +

Active Merchants

+

0

-
-
- - -
-
-

Active Terminals

-

0

-
-
- - - - +

Active Terminals

+

0

@@ -144,7 +85,7 @@

Dashboard

- +
@@ -175,6 +116,50 @@

Registered Merchants

+ + diff --git a/frontend/templates/admin/admin_merchant.html b/frontend/templates/admin/admin_merchant.html index 1bf0bc2..4cd4387 100644 --- a/frontend/templates/admin/admin_merchant.html +++ b/frontend/templates/admin/admin_merchant.html @@ -15,7 +15,7 @@
- {{template "admin_sidebar" "merchants"}} + {{template "admin_sidebar" .}}
@@ -58,20 +58,39 @@

Client Businesses

-
- +
+ + + + + + + + class="px-3 py-2 text-sm font-medium text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition duration-150 w-full md:w-auto">Reset
-
Merchant
+
Merchant #1
- +
- +
@@ -181,7 +200,7 @@

Owner / Contact Information

-
@@ -196,8 +215,16 @@

Settlement Details

- + + + + + + + +
@@ -216,13 +243,16 @@

Terminal Information

- +
- +
@@ -246,6 +276,50 @@

Terminal Information

+ + diff --git a/frontend/templates/admin/admin_sidebar.html b/frontend/templates/admin/admin_sidebar.html index 3459840..1f49b0a 100644 --- a/frontend/templates/admin/admin_sidebar.html +++ b/frontend/templates/admin/admin_sidebar.html @@ -17,9 +17,9 @@

Core System