From 5c1f02614c96f2f9a96166e1cc4123e931dcdc62 Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Thu, 21 May 2026 18:59:12 +0800 Subject: [PATCH 1/8] feat: implement super admin merchant management system with database onboarding and UI template --- backend/cmd/app/main.go | 1 + backend/internal/admin/super_admin_pages.go | 182 +++++++++++++++++- .../templates/admin/merchant_management.html | 117 ++++++++++- 3 files changed, 295 insertions(+), 5 deletions(-) diff --git a/backend/cmd/app/main.go b/backend/cmd/app/main.go index 5beb96a..0e7f107 100644 --- a/backend/cmd/app/main.go +++ b/backend/cmd/app/main.go @@ -108,6 +108,7 @@ func main() { mux.HandleFunc("GET /admin/merchants", adminHanlder.MerchantManagementView) mux.HandleFunc("GET /admin/terminals", adminHanlder.TerminalRegistryView) mux.HandleFunc("GET /admin/settings", adminHanlder.SystemSettingsView) + mux.HandleFunc("POST /v1/admin/merchants/add", adminHanlder.AddMerchantHandler) mux.HandleFunc("GET /admin/dashboard", adminHanlder.DashboardView) mux.HandleFunc("GET /v1/admin/dashboard-data", adminHanlder.DashboardDataHandler) diff --git a/backend/internal/admin/super_admin_pages.go b/backend/internal/admin/super_admin_pages.go index b570c32..b74a0c6 100644 --- a/backend/internal/admin/super_admin_pages.go +++ b/backend/internal/admin/super_admin_pages.go @@ -1,16 +1,46 @@ 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 func (h *Handler) PlatformOverviewView(w http.ResponseWriter, r *http.Request) { err := h.Tpl.ExecuteTemplate(w, "platform_overview.html", nil) if err != nil { log.Printf("Template execution error: %v", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Internal Server Error", + }) } } @@ -19,7 +49,10 @@ func (h *Handler) MerchantManagementView(w http.ResponseWriter, r *http.Request) err := h.Tpl.ExecuteTemplate(w, "merchant_management.html", nil) if err != nil { log.Printf("Template execution error: %v", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Internal Server Error", + }) } } @@ -28,7 +61,10 @@ func (h *Handler) TerminalRegistryView(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) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Internal Server Error", + }) } } @@ -37,6 +73,144 @@ func (h *Handler) SystemSettingsView(w http.ResponseWriter, r *http.Request) { err := h.Tpl.ExecuteTemplate(w, "system_settings.html", nil) if err != nil { log.Printf("Template execution error: %v", err) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Internal Server Error", + }) + } +} + +// AddMerchantHandler creates a new merchant and its corresponding owner user +func (h *Handler) AddMerchantHandler(w http.ResponseWriter, r *http.Request) { + var req AddMerchantRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Invalid JSON payload", + }) + return + } + + err := Validate.Struct(req) + if err != nil { + log.Printf("Validation error: %v", err) + + var validationErrs validator.ValidationErrors + if errors.As(err, &validationErrs) { + firstErr := validationErrs[0] + + fieldMessages := map[string]string{ + "BusinessName": "Business name is required", + "BusinessType": "Business type is required", + "RegistrationNum": "Registration number is required", + "BusinessAddress": "Business address is required", + "OwnerName": "Owner name is required", + "BusinessEmail": "A valid business email is required", + "BusinessPhone": "Business phone number is required", + "CommissionRate": "Commission rate is required", + "SettlementName": "Settlement name is required", + "SettlementAccount": "Settlement account number is required", + "SettlementBank": "Settlement bank name is required", + } + + msg := "Validation failed on field: " + firstErr.Field() + if customMsg, ok := fieldMessages[firstErr.Field()]; ok { + msg = customMsg + } + + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: msg, + }) + return + } + + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Validation error", + }) + return + } + + // Generate IDs (Format: YYMMminsecxxxxx where xxxxx is 5 random numbers) + timestamp := time.Now().Format("06010405") // YYMMDDHH + + nUser, _ := rand.Int(rand.Reader, big.NewInt(100000)) + userID := fmt.Sprintf("UNI-%s%04d", timestamp, nUser.Int64()) + + nMerchant, _ := rand.Int(rand.Reader, big.NewInt(100000)) + merchantID := fmt.Sprintf("MCH-%s%04d", timestamp, nMerchant.Int64()) + + // Create user for the merchant owner + hashedPassword, err := bcrypt.GenerateFromPassword([]byte("TempPass123!"), bcrypt.DefaultCost) + if err != nil { + log.Printf("Error hashing password: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Failed to secure user credentials", + }) + return + } + + tx, err := h.DB.Begin() + if err != nil { + log.Printf("Error starting tx: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Database error", + }) + return + } + + // Insert User + userStmt := `INSERT INTO users (user_id, username, name, email, phone_number, password_hash, role, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + // Using business email as username for simplicity, you could use a dedicated username field + username := req.BusinessEmail + _, err = tx.Exec(userStmt, userID, username, req.OwnerName, req.BusinessEmail, req.BusinessPhone, string(hashedPassword), "merchant_admin", "active") + if err != nil { + tx.Rollback() + log.Printf("Error creating user: %v", err) + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Failed to create user account (email or phone might already exist)", + }) + return } + + // Insert Merchant + merchStmt := `INSERT INTO merchants ( + merchant_id, business_name, business_type, business_registration_number, business_address, + owner_user_id, owner_name, business_email, business_phone, commission_rate, + settlement_account_name, settlement_account_number, settlement_bank_name, status + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + + _, err = tx.Exec(merchStmt, + merchantID, req.BusinessName, req.BusinessType, req.RegistrationNum, req.BusinessAddress, + userID, req.OwnerName, req.BusinessEmail, req.BusinessPhone, req.CommissionRate, + req.SettlementName, req.SettlementAccount, req.SettlementBank, "active", + ) + + if err != nil { + tx.Rollback() + log.Printf("Error creating merchant: %v", err) + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Failed to create merchant profile (registration num or email might exist)", + }) + return + } + + if err := tx.Commit(); err != nil { + log.Printf("Error committing tx: %v", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Failed to finalize creation", + }) + return + } + + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: true, + Message: "Merchant onboarded successfully", + }) } diff --git a/frontend/templates/admin/merchant_management.html b/frontend/templates/admin/merchant_management.html index 0a0946b..017a61f 100644 --- a/frontend/templates/admin/merchant_management.html +++ b/frontend/templates/admin/merchant_management.html @@ -118,7 +118,7 @@

Merchant Management

Client Businesses

View and manage all registered merchants operating on the platform.

- @@ -197,5 +197,120 @@

Client Businesses

+ + + + + From 3e8a3f640eca22a67e31264d0b73e70d0619641e Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Sun, 24 May 2026 15:25:43 +0800 Subject: [PATCH 2/8] feat: implement admin card management handlers and user dashboard functionality --- backend/cmd/app/main.go | 28 +- backend/internal/admin/addCard.go | 17 +- backend/internal/admin/dashboard.go | 55 +-- backend/internal/admin/deactivateCard.go | 17 +- backend/internal/admin/deleteCard.go | 9 - backend/internal/user/dashboard.go | 70 ++- backend/internal/user/pages.go | 105 ---- frontend/assets/js/admin/admin-dasboard.js | 319 ++++++++++++ frontend/templates/admin/admin_dashboard.html | 466 ++++-------------- 9 files changed, 473 insertions(+), 613 deletions(-) delete mode 100644 backend/internal/user/pages.go create mode 100644 frontend/assets/js/admin/admin-dasboard.js diff --git a/backend/cmd/app/main.go b/backend/cmd/app/main.go index 0e7f107..7290ac2 100644 --- a/backend/cmd/app/main.go +++ b/backend/cmd/app/main.go @@ -81,27 +81,13 @@ func main() { 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 /transaction", userHandler.TransactionView) - mux.HandleFunc("GET /topup", userHandler.TopupView) - mux.HandleFunc("GET /profile", userHandler.ProfileView) - mux.HandleFunc("GET /settings", userHandler.SettingsView) - mux.HandleFunc("GET /card", userHandler.CardView) - mux.HandleFunc("GET /v1/user/transactions", userHandler.TransactionsJSONHandler) - mux.HandleFunc("GET /logout", func(w http.ResponseWriter, r *http.Request) { - http.SetCookie(w, &http.Cookie{ - Name: "session_user_id", - Value: "", - Path: "/", - MaxAge: -1, - }) - http.SetCookie(w, &http.Cookie{ - Name: "session_admin_username", - Value: "", - Path: "/", - MaxAge: -1, - }) - http.Redirect(w, r, "/login", http.StatusSeeOther) - }) + //mux.HandleFunc("GET /transaction", userHandler.TransactionView) + //mux.HandleFunc("GET /topup", userHandler.TopupView) + //mux.HandleFunc("GET /profile", userHandler.ProfileView) + //mux.HandleFunc("GET /settings", userHandler.SettingsView) + //mux.HandleFunc("GET /card", userHandler.CardView) + //mux.HandleFunc("GET /v1/user/transactions", userHandler.TransactionsJSONHandler) + //mux.HandleFunc("GET /logout",) // super admin endpoints mux.HandleFunc("GET /admin/platform-overview", adminHanlder.PlatformOverviewView) diff --git a/backend/internal/admin/addCard.go b/backend/internal/admin/addCard.go index 7815380..7f4d5c9 100644 --- a/backend/internal/admin/addCard.go +++ b/backend/internal/admin/addCard.go @@ -26,12 +26,7 @@ type Card struct { func (h *Handler) AddCardsView(w http.ResponseWriter, r *http.Request) { fmt.Println("AddCardsView running...") - // Validate admin session - cookie, err := r.Cookie("session_admin_username") - if err != nil || cookie.Value == "" { - http.Redirect(w, r, "/login", http.StatusSeeOther) - return - } + h.Tpl.ExecuteTemplate(w, "addCards.html", nil) } @@ -40,16 +35,6 @@ func (h *Handler) AddCardsView(w http.ResponseWriter, r *http.Request) { func (h *Handler) AddCardHandler(w http.ResponseWriter, r *http.Request) { fmt.Println("AddCardHandler running...") - // Verify session - cookie, err := r.Cookie("session_admin_username") - if err != nil || cookie.Value == "" { - jsonwrite.WriteJSON(w, http.StatusUnauthorized, jsonwrite.APIResponse{ - Success: false, - Message: "Unauthorized", - }) - return - } - var req struct { CardUID string `json:"cardUID"` InitialAmount string `json:"initialAmount"` diff --git a/backend/internal/admin/dashboard.go b/backend/internal/admin/dashboard.go index 73426ba..47626a7 100644 --- a/backend/internal/admin/dashboard.go +++ b/backend/internal/admin/dashboard.go @@ -5,20 +5,21 @@ import ( "encoding/json" "fmt" "net/http" + jsonwrite "unicard-go/backend/internal/pkg/handler" ) // AdminCard represents a card entry in the admin database type AdminCard struct { - ID int `json:"id"` - CardUID string `json:"card_uid"` - CardNumber string `json:"card_number"` - CardType string `json:"card_type"` - InitialAmount float64 `json:"initial_amount"` - ExpiryDate string `json:"expiry_date"` - Status string `json:"status"` - CreatedAt string `json:"created_at"` - CardHolder string `json:"card_holder"` - UserID string `json:"user_id"` + ID int `json:"id" db:"id"` + CardUID string `json:"card_uid" db:"card_uid"` + CardNumber string `json:"card_number" db:"card_number"` + CardType string `json:"card_type" db:"card_type"` + Balance float64 `json:"initial_amount" db:"balance"` + ExpiryDate string `json:"expiry_date" db:"expiry_date"` + Status string `json:"status" db:"status"` + CreatedAt string `json:"created_at" db:"created_at"` + CardHolder string `json:"card_holder" db:"card_holder"` + UserID string `json:"user_id" db:"user_id"` } // AdminStats contains statistics about cards @@ -34,14 +35,7 @@ type AdminStats struct { func (h *Handler) DashboardView(w http.ResponseWriter, r *http.Request) { fmt.Println("DashboardView running...") - // Validate admin session - cookie, err := r.Cookie("session_admin_username") - if err != nil || cookie.Value == "" { - http.Redirect(w, r, "/login", http.StatusSeeOther) - return - } - - err = h.Tpl.ExecuteTemplate(w, "admin_dashboard.html", nil) + err := h.Tpl.ExecuteTemplate(w, "admin_dashboard.html", nil) if err != nil { fmt.Printf("Template execution error: %v\n", err) } @@ -53,14 +47,6 @@ func (h *Handler) DashboardDataHandler(w http.ResponseWriter, r *http.Request) { //time.Sleep(5 * time.Second) - cookie, err := r.Cookie("session_admin_username") - if err != nil || cookie.Value == "" { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusUnauthorized) - json.NewEncoder(w).Encode(map[string]string{"error": "Unauthorized"}) - return - } - // 1. Fetch Stats var stats AdminStats h.DB.QueryRow("SELECT COUNT(*) FROM cards").Scan(&stats.Total) @@ -94,7 +80,7 @@ func (h *Handler) DashboardDataHandler(w http.ResponseWriter, r *http.Request) { &c.CardUID, &c.CardNumber, &c.CardType, - &c.InitialAmount, + &c.Balance, &c.ExpiryDate, &c.Status, &createdAtNull, @@ -106,22 +92,16 @@ func (h *Handler) DashboardDataHandler(w http.ResponseWriter, r *http.Request) { continue } - if createdAtNull.Valid { - c.CreatedAt = createdAtNull.String - } else { - c.CreatedAt = "" - } + c.CreatedAt = createdAtNull.String + c.CardHolder = "Unlinked" if cardHolderNull.Valid { c.CardHolder = cardHolderNull.String - } else { - c.CardHolder = "Unlinked" } + c.UserID = "None" if userIDNull.Valid { c.UserID = userIDNull.String - } else { - c.UserID = "None" } cards = append(cards, c) @@ -135,6 +115,5 @@ func (h *Handler) DashboardDataHandler(w http.ResponseWriter, r *http.Request) { Cards: cards, } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(resp) + jsonwrite.WriteJSON(w, http.StatusOK, resp) } diff --git a/backend/internal/admin/deactivateCard.go b/backend/internal/admin/deactivateCard.go index 37f542b..50c2731 100644 --- a/backend/internal/admin/deactivateCard.go +++ b/backend/internal/admin/deactivateCard.go @@ -12,12 +12,7 @@ import ( func (h *Handler) DeactivateView(w http.ResponseWriter, r *http.Request) { fmt.Println("DeactivateView running...") - // Validate admin session - cookie, err := r.Cookie("session_admin_username") - if err != nil || cookie.Value == "" { - http.Redirect(w, r, "/login", http.StatusSeeOther) - return - } + h.Tpl.ExecuteTemplate(w, "deactivateCard.html", nil) } @@ -26,16 +21,6 @@ func (h *Handler) DeactivateView(w http.ResponseWriter, r *http.Request) { func (h *Handler) DeactivateCardHanlder(w http.ResponseWriter, r *http.Request) { fmt.Println("DeactivateCardHanlder running...") - // Verify session - cookie, err := r.Cookie("session_admin_username") - if err != nil || cookie.Value == "" { - jsonwrite.WriteJSON(w, http.StatusUnauthorized, jsonwrite.APIResponse{ - Success: false, - Message: "Unauthorized", - }) - return - } - var req struct { CardNumber string `json:"cardNumber"` Name string `json:"name"` diff --git a/backend/internal/admin/deleteCard.go b/backend/internal/admin/deleteCard.go index 0a9e4d2..45ab39a 100644 --- a/backend/internal/admin/deleteCard.go +++ b/backend/internal/admin/deleteCard.go @@ -12,15 +12,6 @@ import ( func (h *Handler) DeleteCardHandler(w http.ResponseWriter, r *http.Request) { fmt.Println("DeleteCardHandler running...") - // Verify session - cookie, err := r.Cookie("session_admin_username") - if err != nil || cookie.Value == "" { - jsonwrite.WriteJSON(w, http.StatusUnauthorized, jsonwrite.APIResponse{ - Success: false, - Message: "Unauthorized", - }) - return - } if r.Method != http.MethodPost { jsonwrite.WriteJSON(w, http.StatusMethodNotAllowed, jsonwrite.APIResponse{ diff --git a/backend/internal/user/dashboard.go b/backend/internal/user/dashboard.go index 7412abb..46a21ea 100644 --- a/backend/internal/user/dashboard.go +++ b/backend/internal/user/dashboard.go @@ -38,13 +38,7 @@ type DashboardUser struct { func (h *Handler) DashboardView(w http.ResponseWriter, r *http.Request) { fmt.Println("Dashboard view is running...") - // Check if session cookie is present - cookie, err := r.Cookie("session_user_id") - if err != nil || cookie.Value == "" { - fmt.Println("No user session found in view, redirecting to login") - http.Redirect(w, r, "/login", http.StatusSeeOther) - return - } + // Check if session cookie is present (Removed) h.Tpl.ExecuteTemplate(w, "dashboard.html", nil) } @@ -52,17 +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 session cookie - cookie, err := r.Cookie("session_user_id") - if err != nil || cookie.Value == "" { - fmt.Println("No user session found, returning unauthorized JSON") - jsonwrite.WriteJSON(w, http.StatusUnauthorized, jsonwrite.APIResponse{ - Success: false, - Message: "Unauthorized", - }) - return - } - userID := cookie.Value + // Get session cookie (Removed) + userID := "UNI-060104051234" // Dummy user ID for testing // Fetch user and card details var ( @@ -80,35 +65,30 @@ func (h *Handler) DashboardHandler(w http.ResponseWriter, r *http.Request) { ) stmt := ` SELECT - u.id, - u.username, - u.full_name, - u.email, - u.phone, - u.user_type, - u.balance, - u.loyalty_points, - u.card_number, - u.status - FROM users u - LEFT JOIN cards c - ON u.card_number = c.card_number - WHERE u.user_id = ? + u.id, + u.username, + u.name, + u.email, + COALESCE(u.phone_number, ''), + u.role, + COALESCE(c.balance, 0), + COALESCE(c.loyalty_points, 0), + COALESCE(c.card_number, ''), + COALESCE(c.expiry_date, ''), + COALESCE(c.status, '') + FROM users u + LEFT JOIN cards c + ON u.user_id = c.user_id + WHERE u.user_id = ? ` - err = h.DB.QueryRow(stmt, userID).Scan(&id, &username, &fullName, &email, &phone, &userType, &balance, &loyaltyPoints, &cardNumber, &cardStatus) + err := h.DB.QueryRow(stmt, userID).Scan(&id, &username, &fullName, &email, &phone, &userType, &balance, &loyaltyPoints, &cardNumber, &expiryDate, &cardStatus) if err != nil { if err == sql.ErrNoRows { fmt.Printf("User %s not found in DB\n", userID) } else { fmt.Printf("Error fetching user %s from DB: %v\n", userID, err) } - // Clear invalid session cookie and return unauthorized response - http.SetCookie(w, &http.Cookie{ - Name: "session_user_id", - Value: "", - Path: "/", - MaxAge: -1, - }) + // Clear invalid session cookie (Removed) jsonwrite.WriteJSON(w, http.StatusUnauthorized, jsonwrite.APIResponse{ Success: false, Message: "Unauthorized: User not found", @@ -140,7 +120,15 @@ func (h *Handler) DashboardHandler(w http.ResponseWriter, r *http.Request) { } // Fetch recent transactions - rows, err := h.DB.Query("SELECT created_at, description, transaction_type, amount FROM transactions WHERE user_id = ? ORDER BY created_at DESC LIMIT 5", userID) + txnQuery := ` + SELECT t.created_at, m.business_name, t.transaction_type, t.amount + FROM transactions t + JOIN cards c ON t.card_number = c.card_number + JOIN merchants m ON t.merchant_id = m.id + WHERE c.user_id = ? + ORDER BY t.created_at DESC LIMIT 5 + ` + rows, err := h.DB.Query(txnQuery, userID) var transactions []Transaction if err == nil { defer rows.Close() diff --git a/backend/internal/user/pages.go b/backend/internal/user/pages.go deleted file mode 100644 index a1efeb4..0000000 --- a/backend/internal/user/pages.go +++ /dev/null @@ -1,105 +0,0 @@ -package user - -import ( - "fmt" - "net/http" - - jsonwrite "unicard-go/backend/internal/pkg/handler" -) - -func (h *Handler) TransactionView(w http.ResponseWriter, r *http.Request) { - fmt.Println("Transaction view is running...") - cookie, err := r.Cookie("session_user_id") - if err != nil || cookie.Value == "" { - http.Redirect(w, r, "/login", http.StatusSeeOther) - return - } - h.Tpl.ExecuteTemplate(w, "transaction.html", nil) -} - -func (h *Handler) TopupView(w http.ResponseWriter, r *http.Request) { - fmt.Println("Topup view is running...") - cookie, err := r.Cookie("session_user_id") - if err != nil || cookie.Value == "" { - http.Redirect(w, r, "/login", http.StatusSeeOther) - return - } - h.Tpl.ExecuteTemplate(w, "topup.html", nil) -} - -func (h *Handler) ProfileView(w http.ResponseWriter, r *http.Request) { - fmt.Println("Profile view is running...") - cookie, err := r.Cookie("session_user_id") - if err != nil || cookie.Value == "" { - http.Redirect(w, r, "/login", http.StatusSeeOther) - return - } - h.Tpl.ExecuteTemplate(w, "profile.html", nil) -} - -func (h *Handler) SettingsView(w http.ResponseWriter, r *http.Request) { - fmt.Println("Settings view is running...") - cookie, err := r.Cookie("session_user_id") - if err != nil || cookie.Value == "" { - http.Redirect(w, r, "/login", http.StatusSeeOther) - return - } - h.Tpl.ExecuteTemplate(w, "settings.html", nil) -} - -func (h *Handler) CardView(w http.ResponseWriter, r *http.Request) { - fmt.Println("Card view is running...") - cookie, err := r.Cookie("session_user_id") - if err != nil || cookie.Value == "" { - http.Redirect(w, r, "/login", http.StatusSeeOther) - return - } - h.Tpl.ExecuteTemplate(w, "card.html", nil) -} - -func (h *Handler) TransactionsJSONHandler(w http.ResponseWriter, r *http.Request) { - fmt.Println("Transactions JSON handler is running...") - - // Get session cookie - cookie, err := r.Cookie("session_user_id") - if err != nil || cookie.Value == "" { - jsonwrite.WriteJSON(w, http.StatusUnauthorized, jsonwrite.APIResponse{ - Success: false, - Message: "Unauthorized", - }) - return - } - userID := cookie.Value - - // Fetch all transactions - rows, err := h.DB.Query("SELECT created_at, description, transaction_type, amount FROM transactions WHERE user_id = ? ORDER BY created_at DESC", userID) - var transactions []Transaction - if err == nil { - defer rows.Close() - for rows.Next() { - var t Transaction - var createdAt string - if err := rows.Scan(&createdAt, &t.Description, &t.Type, &t.Amount); err == nil { - t.Date = formatDate(createdAt) // Using formatDate from dashboard.go - transactions = append(transactions, t) - } - } - } else { - fmt.Printf("Error fetching transactions: %v\n", err) - jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ - Success: false, - Message: "Failed to load transactions", - }) - return - } - - // If no transactions, ensure we send an empty array, not null - if transactions == nil { - transactions = []Transaction{} - } - - jsonwrite.WriteJSON(w, http.StatusOK, map[string]interface{}{ - "success": true, - "transactions": transactions, - }) -} diff --git a/frontend/assets/js/admin/admin-dasboard.js b/frontend/assets/js/admin/admin-dasboard.js new file mode 100644 index 0000000..131a71f --- /dev/null +++ b/frontend/assets/js/admin/admin-dasboard.js @@ -0,0 +1,319 @@ +// Modal Control Functions +function openDeactivateModal(num, holder, type) { + document.getElementById('deactivate-card-number').value = num; + document.getElementById('deactivate-card-holder').value = holder; + document.getElementById('deactivate-card-type').value = type; + + document.getElementById('deactivate-modal-num').innerText = num; + document.getElementById('deactivate-modal-holder').innerText = holder; + document.getElementById('deactivate-modal-type').innerText = type; + + document.getElementById('deactivate-modal').classList.remove('hidden'); +} + +function closeDeactivateModal() { + document.getElementById('deactivate-modal').classList.add('hidden'); +} + +function openDeleteModal(num) { + document.getElementById('delete-card-number').value = num; + document.getElementById('delete-modal-num').innerText = num; + document.getElementById('delete-modal').classList.remove('hidden'); +} + +function closeDeleteModal() { + document.getElementById('delete-modal').classList.add('hidden'); +} + +function openLogoutModal() { + document.getElementById('logout-modal').classList.remove('hidden'); +} + +function closeLogoutModal() { + document.getElementById('logout-modal').classList.add('hidden'); +} + +// Pagination and Search Configuration +const rowsPerPage = 4; +let currentPage = 1; +let allCards = []; +let filteredCards = []; + +function renderCards() { + const totalRows = filteredCards.length; + const totalPages = Math.ceil(totalRows / rowsPerPage); + + if (currentPage > totalPages) { + currentPage = Math.max(1, totalPages); + } + + const startIdx = (currentPage - 1) * rowsPerPage; + const endIdx = Math.min(startIdx + rowsPerPage, totalRows); + + const tbody = document.getElementById('table-body'); + tbody.innerHTML = ''; + + const pageCards = filteredCards.slice(startIdx, endIdx); + if (pageCards.length === 0) { + tbody.innerHTML = ` + + + No cards registered or found. + + + `; + } else { + pageCards.forEach(c => { + const tr = document.createElement('tr'); + tr.className = 'hover:bg-gray-50 transition duration-150'; + + let holderHtml = ''; + if (c.card_holder === 'Unlinked') { + holderHtml = 'Unlinked'; + } else { + holderHtml = c.card_holder; + } + + let statusClass = ''; + if (c.status === 'Active') { + statusClass = 'bg-green-100 text-green-800'; + } else if (c.status === 'Inactive') { + statusClass = 'bg-gray-100 text-gray-800'; + } else if (c.status === 'Blocked') { + statusClass = 'bg-red-100 text-red-800'; + } else if (c.status === 'Lost') { + statusClass = 'bg-yellow-100 text-yellow-800'; + } else { + statusClass = 'bg-red-100 text-red-800'; + } + + const canDeactivate = c.status === 'Active'; + const deactivateBtn = canDeactivate + ? `` + : `Deactivate`; + + tr.innerHTML = ` + ${c.card_number} + ${holderHtml} + ${c.card_type} + ${c.expiry_date} + ₱${Number(c.initial_amount).toFixed(2)} + + + ${c.status} + + + + ${deactivateBtn} + + + `; + tbody.appendChild(tr); + }); + } + + // Update footer metrics text + document.getElementById('start-index').innerText = totalRows === 0 ? 0 : startIdx + 1; + document.getElementById('end-index').innerText = endIdx; + document.getElementById('total-count').innerText = totalRows; + + // Enable/disable page control buttons + const prevBtn = document.getElementById('prev-btn'); + const nextBtn = document.getElementById('next-btn'); + const prevBtnMobile = document.getElementById('prev-btn-mobile'); + const nextBtnMobile = document.getElementById('next-btn-mobile'); + + if (prevBtn) prevBtn.disabled = currentPage === 1; + if (nextBtn) nextBtn.disabled = currentPage === totalPages || totalPages === 0; + if (prevBtnMobile) prevBtnMobile.disabled = currentPage === 1; + if (nextBtnMobile) nextBtnMobile.disabled = currentPage === totalPages || totalPages === 0; +} + +// Search Filtering Logic +function filterCards() { + const query = document.getElementById('search-input').value.toLowerCase().trim(); + + if (query === '') { + filteredCards = [...allCards]; + } else { + filteredCards = allCards.filter(c => { + const searchString = `${c.card_number} ${c.card_holder} ${c.card_type} ${c.status}`.toLowerCase(); + return searchString.includes(query); + }); + } + + renderCards(); +} + +function fetchDashboardData(showAlert = null) { + fetch("/v1/admin/dashboard-data") + .then(res => { + if (res.status === 401) { + window.location.href = "/login"; + return null; + } + return res.json(); + }) + .then(data => { + if (!data) return; + + // Update stats + document.getElementById('stats-total').innerText = data.stats.total || 0; + document.getElementById('stats-active').innerText = data.stats.active || 0; + document.getElementById('stats-inactive').innerText = data.stats.inactive || 0; + document.getElementById('stats-blocked').innerText = data.stats.blocked || 0; + document.getElementById('stats-lost').innerText = data.stats.lost || 0; + + // Update cards list + allCards = data.cards || []; + + // Show success/error if passed + if (showAlert) { + const errorAlert = document.getElementById("error-alert"); + const successAlert = document.getElementById("success-alert"); + const errorText = document.getElementById("error-text"); + const successText = document.getElementById("success-text"); + + if (errorAlert) errorAlert.classList.add("hidden"); + if (successAlert) successAlert.classList.add("hidden"); + + if (showAlert.type === 'success') { + if (successAlert && successText) { + successText.innerText = showAlert.message; + successAlert.classList.remove("hidden"); + } + } else if (showAlert.type === 'error') { + if (errorAlert && errorText) { + errorText.innerText = showAlert.message; + errorAlert.classList.remove("hidden"); + } + } + } + + // Apply filters & render + filterCards(); + }) + .catch(err => { + console.error("Error loading dashboard data:", err); + }); +} + +// Set up search event listener +document.getElementById('search-input').addEventListener('input', () => { + currentPage = 1; + filterCards(); +}); + +// Set up pagination control event listeners +document.getElementById('prev-btn').addEventListener('click', () => { + if (currentPage > 1) { + currentPage--; + renderCards(); + } +}); + +document.getElementById('next-btn').addEventListener('click', () => { + const totalPages = Math.ceil(filteredCards.length / rowsPerPage); + if (currentPage < totalPages) { + currentPage++; + renderCards(); + } +}); + +document.getElementById('prev-btn-mobile').addEventListener('click', () => { + if (currentPage > 1) { + currentPage--; + renderCards(); + } +}); + +document.getElementById('next-btn-mobile').addEventListener('click', () => { + const totalPages = Math.ceil(filteredCards.length / rowsPerPage); + if (currentPage < totalPages) { + currentPage++; + renderCards(); + } +}); + +// Setup confirm event listeners +document.getElementById('confirm-deactivate-btn').addEventListener('click', () => { + const cardNumber = document.getElementById('deactivate-card-number').value; + const name = document.getElementById('deactivate-card-holder').value; + const cardType = document.getElementById('deactivate-card-type').value; + + fetch('/v1/admin/deactivatecardauth', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cardNumber, name, cardType }) + }) + .then(res => res.json()) + .then(data => { + closeDeactivateModal(); + if (data.success) { + fetchDashboardData({ type: 'success', message: data.message }); + } else { + fetchDashboardData({ type: 'error', message: data.message }); + } + }) + .catch(err => { + console.error(err); + closeDeactivateModal(); + fetchDashboardData({ type: 'error', message: 'Failed to deactivate card due to a network error.' }); + }); +}); + +document.getElementById('confirm-delete-btn').addEventListener('click', () => { + const cardNumber = document.getElementById('delete-card-number').value; + + fetch('/v1/admin/deletecardauth', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cardNumber }) + }) + .then(res => res.json()) + .then(data => { + closeDeleteModal(); + if (data.success) { + fetchDashboardData({ type: 'success', message: data.message }); + } else { + fetchDashboardData({ type: 'error', message: data.message }); + } + }) + .catch(err => { + console.error(err); + closeDeleteModal(); + fetchDashboardData({ type: 'error', message: 'Failed to delete card due to a network error.' }); + }); +}); + +function fetchAdminProfile() { + fetch("/v1/admin/me") + .then(res => { + if (res.status === 401) { + window.location.href = "/login"; + return null; + } + return res.json(); + }) + .then(data => { + if (!data || !data.username) return; + const username = data.username; + const welcomeEl = document.getElementById('admin-welcome-name'); + if (welcomeEl) { + welcomeEl.innerText = `Welcome, ${username}`; + } + const avatarEl = document.getElementById('admin-avatar'); + if (avatarEl) { + avatarEl.innerText = username.substring(0, 2).toUpperCase(); + } + }) + .catch(err => console.error("Error fetching admin profile:", err)); +} + +// Initialize display by fetching JSON +document.addEventListener("DOMContentLoaded", () => { + fetchAdminProfile(); + fetchDashboardData(); +}); \ No newline at end of file diff --git a/frontend/templates/admin/admin_dashboard.html b/frontend/templates/admin/admin_dashboard.html index eec2b64..4c77912 100644 --- a/frontend/templates/admin/admin_dashboard.html +++ b/frontend/templates/admin/admin_dashboard.html @@ -45,50 +45,59 @@

Core System

-

Legacy Operations

+

+ Legacy Operations