From 609c4314fe2dd0d551e0b8961831e236f8d30165 Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Mon, 25 May 2026 17:46:52 +0800 Subject: [PATCH 1/5] feat: add card management functionality with add and delete handlers --- backend/cmd/app/main.go | 1 + .../admin/{addCard.go => add_card.go} | 7 ++- .../admin/{deleteCard.go => delete_card.go} | 44 +++++++++++-------- backend/internal/pkg/structs/card_struct.go | 2 +- 4 files changed, 30 insertions(+), 24 deletions(-) rename backend/internal/admin/{addCard.go => add_card.go} (95%) rename backend/internal/admin/{deleteCard.go => delete_card.go} (66%) diff --git a/backend/cmd/app/main.go b/backend/cmd/app/main.go index 17f86a3..0f61d6d 100644 --- a/backend/cmd/app/main.go +++ b/backend/cmd/app/main.go @@ -102,6 +102,7 @@ func main() { 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) // Wrap mux with custom handler for root redirect diff --git a/backend/internal/admin/addCard.go b/backend/internal/admin/add_card.go similarity index 95% rename from backend/internal/admin/addCard.go rename to backend/internal/admin/add_card.go index b7c6229..5828808 100644 --- a/backend/internal/admin/addCard.go +++ b/backend/internal/admin/add_card.go @@ -17,8 +17,6 @@ import ( "github.com/go-playground/validator/v10" ) -// CardRequest struct mapped directly to your frontend JSON payload - // 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...") @@ -29,6 +27,7 @@ func (h *Handler) AddCardsView(w http.ResponseWriter, r *http.Request) { func (h *Handler) AddCardHandler(w http.ResponseWriter, r *http.Request) { fmt.Println("AddCardHandler running...") + // Define request struct var req structs.CardData // Decode JSON @@ -50,8 +49,8 @@ func (h *Handler) AddCardHandler(w http.ResponseWriter, r *http.Request) { var validationErrs validator.ValidationErrors if errors.As(err, &validationErrs) { errorMap := map[string]string{ - "CardUID": "Card UID is required.", - "InitialAmount": "Initial amount is required and cannot be negative.", + "CardUID": "Card UID is required.", + "Balance": "Initial amount is required and cannot be negative.", } if msg, ok := errorMap[validationErrs[0].Field()]; ok { errorMessage = msg diff --git a/backend/internal/admin/deleteCard.go b/backend/internal/admin/delete_card.go similarity index 66% rename from backend/internal/admin/deleteCard.go rename to backend/internal/admin/delete_card.go index a20d886..b3e7018 100644 --- a/backend/internal/admin/deleteCard.go +++ b/backend/internal/admin/delete_card.go @@ -2,48 +2,54 @@ package admin import ( "encoding/json" + "errors" "fmt" "net/http" "strings" jsonwrite "unicard-go/backend/internal/pkg/handler" + "unicard-go/backend/internal/pkg/structs" + + "github.com/go-playground/validator/v10" ) func (h *Handler) DeleteCardView(w http.ResponseWriter, r *http.Request) { fmt.Println("DeleteCardView running...") - h.Tpl.ExecuteTemplate(w, "deleteCard.html", nil) + h.Tpl.ExecuteTemplate(w, "delete_card.html", nil) } // DeleteCardHandler handles deleting a card by card_number and returns JSON. func (h *Handler) DeleteCardHandler(w http.ResponseWriter, r *http.Request) { fmt.Println("DeleteCardHandler running...") - if r.Method != http.MethodPost { - jsonwrite.WriteJSON(w, http.StatusMethodNotAllowed, jsonwrite.APIResponse{ + var req structs.CardData + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ Success: false, - Message: "Method not allowed", + Message: "Invalid request format", }) return } - var req struct { - CardNumber string `json:"cardNumber"` - } + cardNumber := strings.TrimSpace(req.CardNumber) - // Try reading JSON body - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - // Fallback to post form values - if err := r.ParseForm(); err == nil { - req.CardNumber = r.PostFormValue("cardNumber") + validate := validator.New() + if err := validate.Struct(req); err != nil { + errorMessage := "Invalid input provided." + var validationErrs validator.ValidationErrors + if errors.As(err, &validationErrs) { + errorMap := map[string]string{ + "CardNumber": "Card number is required.", + } + if msg, ok := errorMap[validationErrs[0].Field()]; ok { + errorMessage = msg + } } - } - cardNumber := strings.TrimSpace(req.CardNumber) - - if cardNumber == "" { - jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ Success: false, - Message: "Card number is required for deletion.", + Message: errorMessage, }) return } @@ -54,7 +60,7 @@ func (h *Handler) DeleteCardHandler(w http.ResponseWriter, r *http.Request) { fmt.Println("Error deleting card from DB:", err) jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ Success: false, - Message: "Failed to delete card due to database error.", + Message: "Failed to delete card.", }) return } diff --git a/backend/internal/pkg/structs/card_struct.go b/backend/internal/pkg/structs/card_struct.go index 26c0a9b..a077aaa 100644 --- a/backend/internal/pkg/structs/card_struct.go +++ b/backend/internal/pkg/structs/card_struct.go @@ -1,7 +1,7 @@ package structs type CardData struct { - CardUID string `json:"card_uid" db:"card_uid" validate:"required"` + CardUID string `json:"card_uid" db:"card_uid" validate:"required"` CardNumber string `json:"cardNumber" db:"card_number" validate:"required"` CardHolder string `json:"cardHolder" db:"user_id" validate:"required"` CardType string `json:"cardType" db:"card_type" validate:"required"` From 53d4fcad94dfe9bc93d1d66f57d56cd09bf79dfd Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Wed, 27 May 2026 12:05:22 +0800 Subject: [PATCH 2/5] feat: restore CardData struct and add AdminDashboardData struct for dashboard representation --- backend/internal/pkg/structs/card_struct.go | 9 --------- backend/internal/pkg/structs/struct.go | 20 ++++++++++++++++++++ 2 files changed, 20 insertions(+), 9 deletions(-) delete mode 100644 backend/internal/pkg/structs/card_struct.go create mode 100644 backend/internal/pkg/structs/struct.go diff --git a/backend/internal/pkg/structs/card_struct.go b/backend/internal/pkg/structs/card_struct.go deleted file mode 100644 index a077aaa..0000000 --- a/backend/internal/pkg/structs/card_struct.go +++ /dev/null @@ -1,9 +0,0 @@ -package structs - -type CardData struct { - CardUID string `json:"card_uid" db:"card_uid" validate:"required"` - CardNumber string `json:"cardNumber" db:"card_number" validate:"required"` - CardHolder string `json:"cardHolder" db:"user_id" validate:"required"` - CardType string `json:"cardType" db:"card_type" validate:"required"` - Balance float64 `json:"initial_amount" db:"balance" validate:"required,min=0"` -} diff --git a/backend/internal/pkg/structs/struct.go b/backend/internal/pkg/structs/struct.go new file mode 100644 index 0000000..520daed --- /dev/null +++ b/backend/internal/pkg/structs/struct.go @@ -0,0 +1,20 @@ +package structs + +// AdminDashboardData struct represents the data to be displayed on the admin dashboard +type AdminDashboardData struct { + GrossRevenue float64 `json:"grossRevenue"` + NetRevenue float64 `json:"netRevenue"` + TotalUsers int `json:"totalUsers"` + TotalCards int `json:"totalCards"` + ActiveMerchants int `json:"activeMerchants"` + ActiveTerminals int `json:"activeTerminals"` +} + +// CardData struct represents the data required to create a new card +type CardData struct { + CardUID string `json:"card_uid" db:"card_uid" validate:"required"` + CardNumber string `json:"cardNumber" db:"card_number" validate:"required"` + CardHolder string `json:"cardHolder" db:"user_id" validate:"required"` + CardType string `json:"cardType" db:"card_type" validate:"required"` + Balance float64 `json:"initial_amount" db:"balance" validate:"required,min=0"` +} \ No newline at end of file From f075d810ca6aaedf770b0f8f6dfbb21ae3040792 Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Wed, 27 May 2026 13:53:22 +0800 Subject: [PATCH 3/5] feat: refactor structs and add admin dashboard functionality with modal controls --- backend/internal/admin/add_card.go | 4 ++-- backend/internal/admin/deactivate_card.go | 4 ++-- backend/internal/admin/delete_card.go | 2 +- backend/internal/pkg/handler/jsonWrite.go | 1 + backend/internal/pkg/structs/struct.go | 2 +- frontend/assets/js/{admin => }/admin-dasboard.js | 0 6 files changed, 7 insertions(+), 6 deletions(-) rename frontend/assets/js/{admin => }/admin-dasboard.js (100%) diff --git a/backend/internal/admin/add_card.go b/backend/internal/admin/add_card.go index 5828808..f151fe2 100644 --- a/backend/internal/admin/add_card.go +++ b/backend/internal/admin/add_card.go @@ -12,7 +12,7 @@ import ( "strings" "time" jsonwrite "unicard-go/backend/internal/pkg/handler" - "unicard-go/backend/internal/pkg/structs" + "unicard-go/backend/internal/pkg/structure" "github.com/go-playground/validator/v10" ) @@ -28,7 +28,7 @@ func (h *Handler) AddCardHandler(w http.ResponseWriter, r *http.Request) { fmt.Println("AddCardHandler running...") // Define request struct - var req structs.CardData + var req structure.CardData // Decode JSON if err := json.NewDecoder(r.Body).Decode(&req); err != nil { diff --git a/backend/internal/admin/deactivate_card.go b/backend/internal/admin/deactivate_card.go index c3e7422..ad6427a 100644 --- a/backend/internal/admin/deactivate_card.go +++ b/backend/internal/admin/deactivate_card.go @@ -8,13 +8,13 @@ import ( "net/http" "strings" jsonwrite "unicard-go/backend/internal/pkg/handler" - "unicard-go/backend/internal/pkg/structs" + "unicard-go/backend/internal/pkg/structure" "github.com/go-playground/validator/v10" ) // Type alias for CardData to resolve the redeclaration error -type CardData = structs.CardData +type CardData = structure.CardData func (h *Handler) DeactivateView(w http.ResponseWriter, r *http.Request) { fmt.Println("DeactivateView running...") diff --git a/backend/internal/admin/delete_card.go b/backend/internal/admin/delete_card.go index b3e7018..ab83b94 100644 --- a/backend/internal/admin/delete_card.go +++ b/backend/internal/admin/delete_card.go @@ -22,7 +22,7 @@ func (h *Handler) DeleteCardView(w http.ResponseWriter, r *http.Request) { func (h *Handler) DeleteCardHandler(w http.ResponseWriter, r *http.Request) { fmt.Println("DeleteCardHandler running...") - var req structs.CardData + var req structure.CardData if err := json.NewDecoder(r.Body).Decode(&req); err != nil { jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ diff --git a/backend/internal/pkg/handler/jsonWrite.go b/backend/internal/pkg/handler/jsonWrite.go index 3eb07ef..1498534 100644 --- a/backend/internal/pkg/handler/jsonWrite.go +++ b/backend/internal/pkg/handler/jsonWrite.go @@ -10,6 +10,7 @@ type APIResponse struct { Success bool `json:"success"` Message string `json:"message"` Field string `json:"field,omitempty"` + Data any `json:"data,omitempty"` // Optional field for additional data } // Login specific response — returns user data after login diff --git a/backend/internal/pkg/structs/struct.go b/backend/internal/pkg/structs/struct.go index 520daed..550b720 100644 --- a/backend/internal/pkg/structs/struct.go +++ b/backend/internal/pkg/structs/struct.go @@ -1,4 +1,4 @@ -package structs +package structure // AdminDashboardData struct represents the data to be displayed on the admin dashboard type AdminDashboardData struct { diff --git a/frontend/assets/js/admin/admin-dasboard.js b/frontend/assets/js/admin-dasboard.js similarity index 100% rename from frontend/assets/js/admin/admin-dasboard.js rename to frontend/assets/js/admin-dasboard.js From 926606c8733206f3136174660b6d78df75fca349 Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Wed, 27 May 2026 14:08:51 +0800 Subject: [PATCH 4/5] refactor: change the struct to structure --- backend/internal/admin/add_card.go | 2 +- backend/internal/admin/deactivate_card.go | 2 +- backend/internal/admin/delete_card.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/internal/admin/add_card.go b/backend/internal/admin/add_card.go index f151fe2..5205634 100644 --- a/backend/internal/admin/add_card.go +++ b/backend/internal/admin/add_card.go @@ -12,7 +12,7 @@ import ( "strings" "time" jsonwrite "unicard-go/backend/internal/pkg/handler" - "unicard-go/backend/internal/pkg/structure" + structure "unicard-go/backend/internal/pkg/structs" "github.com/go-playground/validator/v10" ) diff --git a/backend/internal/admin/deactivate_card.go b/backend/internal/admin/deactivate_card.go index ad6427a..70a309b 100644 --- a/backend/internal/admin/deactivate_card.go +++ b/backend/internal/admin/deactivate_card.go @@ -8,7 +8,7 @@ import ( "net/http" "strings" jsonwrite "unicard-go/backend/internal/pkg/handler" - "unicard-go/backend/internal/pkg/structure" + structure "unicard-go/backend/internal/pkg/structs" "github.com/go-playground/validator/v10" ) diff --git a/backend/internal/admin/delete_card.go b/backend/internal/admin/delete_card.go index ab83b94..b866e9a 100644 --- a/backend/internal/admin/delete_card.go +++ b/backend/internal/admin/delete_card.go @@ -7,7 +7,7 @@ import ( "net/http" "strings" jsonwrite "unicard-go/backend/internal/pkg/handler" - "unicard-go/backend/internal/pkg/structs" + structure "unicard-go/backend/internal/pkg/structs" "github.com/go-playground/validator/v10" ) From 1f78f9fb59419892003b11287472ae2187064e49 Mon Sep 17 00:00:00 2001 From: devzeeh <148837352+devzeeh@users.noreply.github.com> Date: Wed, 27 May 2026 17:42:28 +0800 Subject: [PATCH 5/5] feat: implement admin dashboard UI and backend infrastructure for system management --- backend/cmd/app/main.go | 3 +- backend/internal/admin/admin_dashboard.go | 156 +++++++ backend/internal/pkg/structs/struct.go | 10 - frontend/templates/admin/admin_dashboard.html | 421 +++++------------- .../templates/admin/platform_overview.html | 211 --------- 5 files changed, 269 insertions(+), 532 deletions(-) create mode 100644 backend/internal/admin/admin_dashboard.go delete mode 100644 frontend/templates/admin/platform_overview.html diff --git a/backend/cmd/app/main.go b/backend/cmd/app/main.go index 0f61d6d..d63e1ae 100644 --- a/backend/cmd/app/main.go +++ b/backend/cmd/app/main.go @@ -90,7 +90,8 @@ func main() { //mux.HandleFunc("GET /logout",) // super admin endpoints - mux.HandleFunc("GET /admin/platform-overview", adminHanlder.PlatformOverviewView) + mux.HandleFunc("GET /admin/dashboard", adminHanlder.AdminDashboardView) + mux.HandleFunc("POST /v1/admin/dashboard-data", adminHanlder.AdminDashboardDataHandler) mux.HandleFunc("GET /admin/merchants", adminHanlder.MerchantManagementView) mux.HandleFunc("GET /admin/terminals", adminHanlder.TerminalRegistryView) mux.HandleFunc("GET /admin/settings", adminHanlder.SystemSettingsView) diff --git a/backend/internal/admin/admin_dashboard.go b/backend/internal/admin/admin_dashboard.go new file mode 100644 index 0000000..12a7afd --- /dev/null +++ b/backend/internal/admin/admin_dashboard.go @@ -0,0 +1,156 @@ +package admin + +import ( + "encoding/json" + "log" + "net/http" + jsonwrite "unicard-go/backend/internal/pkg/handler" +) + +// AdminDashboardData struct represents the data to be displayed on the admin dashboard +type AdminDashboardData struct { + GrossRevenue float64 `json:"grossRevenue"` + NetRevenue float64 `json:"netRevenue"` + TotalUsers int `json:"totalUsers"` + TotalCards int `json:"totalCards"` + ActiveMerchants int `json:"activeMerchants"` + ActiveTerminals int `json:"activeTerminals"` +} + +// 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) +} + +// AdminDashboardDataHandler handles the request for admin dashboard data and returns JSON response. +// AdminDashboardDataHandler handles the request for admin dashboard data and returns JSON response. +func (h *Handler) AdminDashboardDataHandler(w http.ResponseWriter, r *http.Request) { + log.Println("AdminDashboardDataHandler running...") + + var req AdminDashboardData + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + log.Println("Error decoding JSON:", err) + jsonwrite.WriteJSON(w, http.StatusBadRequest, jsonwrite.APIResponse{ + Success: false, + Message: "Invalid request format", + }) + return + } + + // Compute UniCard's Absolute Gross Revenue + // (Transaction Service Fees) + query := `SELECT COALESCE(SUM(service_fee), 0.00) FROM transactions WHERE transaction_type = 'payment'` + row := h.DB.QueryRow(query) + + var grossRevenue float64 + if err := row.Scan(&grossRevenue); err != nil { + log.Println("Error scanning gross revenue:", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Internal server error", + }) + return + } + log.Println("Gross revenue row:", grossRevenue) + + // Compute UniCard's Net Revenue + // (Service fees from successful payments MINUS service fees lost to refunds/reversals) + query = `SELECT COALESCE(SUM( + CASE + WHEN transaction_type = 'payment' THEN service_fee + WHEN transaction_type IN ('refund', 'reversal') THEN -service_fee + ELSE 0 + END + ), 0.00) FROM transactions` + + row = h.DB.QueryRow(query) + + var netRevenue float64 + if err := row.Scan(&netRevenue); err != nil { + log.Println("Error scanning net revenue:", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Internal server error while calculating Net Revenue", + }) + return + } + log.Println("Net revenue row:", netRevenue) + + // Display the number of users + // Counting only 'active' customers (excluding suspended or inactive accounts) + row = h.DB.QueryRow("SELECT COUNT(*) FROM users WHERE role = 'customer'") // status = 'active' + + var totalUsers int + if err := row.Scan(&totalUsers); err != nil { + log.Println("Error scanning total users:", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Internal server error", + }) + return + } + log.Println("Total users row:", totalUsers) + + // Display the number of cards + // Counting only 'active' cards to show actual circulatory supply + row = h.DB.QueryRow("SELECT COUNT(*) FROM cards") // WHERE status = 'active' + + var totalCards int + if err := row.Scan(&totalCards); err != nil { + log.Println("Error scanning total cards:", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Internal server error", + }) + return + } + log.Println("Total cards row:", totalCards) + + // Display the number of merchants + // Counting only 'active' merchants (excluding 'pending_approval' or 'suspended') + row = h.DB.QueryRow("SELECT COUNT(*) FROM merchants WHERE status = 'active'") + + var totalMerchants int + if err := row.Scan(&totalMerchants); err != nil { + log.Println("Error scanning total merchants:", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Internal server error", + }) + return + } + log.Println("Total merchants row:", totalMerchants) + + // Display the number of terminals + // Counting 'active' ESP32 nodes (excluding 'offline' or 'suspended') + row = h.DB.QueryRow("SELECT COUNT(*) FROM terminals WHERE status = 'active'") + + var totalTerminals int + if err := row.Scan(&totalTerminals); err != nil { + log.Println("Error scanning total terminals:", err) + jsonwrite.WriteJSON(w, http.StatusInternalServerError, jsonwrite.APIResponse{ + Success: false, + Message: "Internal server error", + }) + return + } + log.Println("Total terminals row:", totalTerminals) + + // Return the data as JSON + response := AdminDashboardData{ + GrossRevenue: grossRevenue, + NetRevenue: netRevenue, + TotalUsers: totalUsers, + TotalCards: totalCards, + ActiveMerchants: totalMerchants, + ActiveTerminals: totalTerminals, + } + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: true, + Message: "Admin dashboard data retrieved successfully", + Data: response, + }) + log.Println("AdminDashboardDataHandler finished") +} diff --git a/backend/internal/pkg/structs/struct.go b/backend/internal/pkg/structs/struct.go index 550b720..879f17d 100644 --- a/backend/internal/pkg/structs/struct.go +++ b/backend/internal/pkg/structs/struct.go @@ -1,15 +1,5 @@ package structure -// AdminDashboardData struct represents the data to be displayed on the admin dashboard -type AdminDashboardData struct { - GrossRevenue float64 `json:"grossRevenue"` - NetRevenue float64 `json:"netRevenue"` - TotalUsers int `json:"totalUsers"` - TotalCards int `json:"totalCards"` - ActiveMerchants int `json:"activeMerchants"` - ActiveTerminals int `json:"activeTerminals"` -} - // CardData struct represents the data required to create a new card type CardData struct { CardUID string `json:"card_uid" db:"card_uid" validate:"required"` diff --git a/frontend/templates/admin/admin_dashboard.html b/frontend/templates/admin/admin_dashboard.html index 4c77912..40e7804 100644 --- a/frontend/templates/admin/admin_dashboard.html +++ b/frontend/templates/admin/admin_dashboard.html @@ -1,10 +1,9 @@ -
-Core System
- Legacy Operations
+Legacy Operations
Monitor, create, and manage cards in the system inventory.
+Real-time metrics for UniCard system-wide performance and revenue.
Total Cards
-0
+Gross Revenue
+₱0.00
Active
-0
+Net Revenue
+₱0.00
Inactive
-0
+Total Users
+0
Blocked
-0
+Total Cards
+0
Lost
-0
+Active Merchants
+0
View and manage all cards registered in the system - database.
-| - Card Number | -- Cardholder | -- Type | -- Expiry | -- Preloaded Balance | -- Status | -- Actions | -
|---|---|---|---|---|---|---|
| - Loading inventory... - | -||||||
- Showing 0 to 0 of 0 entries -
+Active Terminals
+0