diff --git a/.agents/rules/unicard.md b/.agents/rules/unicard.md new file mode 100644 index 0000000..513a1d3 --- /dev/null +++ b/.agents/rules/unicard.md @@ -0,0 +1,6 @@ +--- +trigger: always_on +glob: +description: +--- + diff --git a/app.exe b/app.exe index 23cd084..8e0d721 100644 Binary files a/app.exe and b/app.exe differ diff --git a/backend/cmd/app/main.go b/backend/cmd/app/main.go index f9679bf..5beb96a 100644 --- a/backend/cmd/app/main.go +++ b/backend/cmd/app/main.go @@ -39,9 +39,9 @@ func main() { dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", dbUser, dbPass, dbHost, dbPort, dbName) // Setup Templates - tpl, err = template.ParseGlob("./frontend/templates/*.html") + tpl, err = template.ParseGlob("./frontend/templates/*/*.html") if err != nil { - log.Fatal("Templates loaded but variable is nil. Check your folder path.") + log.Fatalf("Failed to load templates: %v. Check your folder path.", err) } // Setup Database @@ -68,25 +68,56 @@ func main() { fileServer := http.FileServer(http.Dir("./frontend/assets")) mux.Handle("/assets/", http.StripPrefix("/assets/", fileServer)) - // POST Request: JSON API endpoints + // general endpoints + mux.HandleFunc("GET /login", authHandler.LoginView) + mux.HandleFunc("GET /signup", authHandler.SignupView) mux.HandleFunc("POST /v1/loginauth", authHandler.LoginAuthHandler) // Login authentication endpoint mux.HandleFunc("POST /v1/signupauth", authHandler.SignupHandler) mux.HandleFunc("POST /v1/signup/check-details", authHandler.CheckDetailsHandler) mux.HandleFunc("POST /v1/signup/check-card", authHandler.CheckCardHandler) - mux.HandleFunc("GET /login", authHandler.LoginView) - mux.HandleFunc("GET /signup", authHandler.SignupView) mux.HandleFunc("GET /forgot-password", authHandler.ForgotPasswordView) 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.DashboardHandler) + 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) + }) + + // super admin endpoints + mux.HandleFunc("GET /admin/platform-overview", adminHanlder.PlatformOverviewView) + mux.HandleFunc("GET /admin/merchants", adminHanlder.MerchantManagementView) + mux.HandleFunc("GET /admin/terminals", adminHanlder.TerminalRegistryView) + mux.HandleFunc("GET /admin/settings", adminHanlder.SystemSettingsView) - // endpoints for admin + mux.HandleFunc("GET /admin/dashboard", adminHanlder.DashboardView) + mux.HandleFunc("GET /v1/admin/dashboard-data", adminHanlder.DashboardDataHandler) 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) + + // Wrap mux with custom handler for root redirect customHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { diff --git a/backend/internal/admin/addCard.go b/backend/internal/admin/addCard.go index e51fb32..7815380 100644 --- a/backend/internal/admin/addCard.go +++ b/backend/internal/admin/addCard.go @@ -2,18 +2,17 @@ package admin import ( "database/sql" + "encoding/json" "fmt" "math/rand" "net/http" "strconv" "strings" "time" - message "unicard-go/backend/internal/pkg" + jsonwrite "unicard-go/backend/internal/pkg/handler" ) -// This struct represents a card and its attributes. -// We can use it to easily pass card data around in our functions. -// It also helps to keep our code organized and makes it easier to manage card-related data. +// Card struct represents a card and its attributes. type Card struct { CardUID string CardNumber string @@ -23,33 +22,57 @@ type Card struct { CreatedAt string } -// This function renders the addCards.html template when the admin visits the /admin/addcard page. -// It doesn't do any processing yet, it just shows the form to the admin. -// We can also pass an empty AddCardsData struct to the template, which allows us to easily display error or success messages later on when we process the form submission. +// 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...") - // Render the addCards.html template - h.Tpl.ExecuteTemplate(w, "addCards.html", message.MessageData{}) + + // 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) } -// This function handles the form submission from the addCards.html page. -// It processes the form data, validates it, generates a card number, checks for duplicates, and inserts the new card into the database. -// Also have error handling at each step, and we pass error or success messages back to the template to inform the admin of the result. +// AddCardHandler handles card creation and returns JSON response. func (h *Handler) AddCardHandler(w http.ResponseWriter, r *http.Request) { - fmt.Println("addcardshandler running...") - - if err := r.ParseForm(); err != nil { - h.Tpl.ExecuteTemplate(w, "addCards.html", message.MessageData{Error: "Failed to parse form"}) + 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 } - // Process the form data here - cardUID := strings.TrimSpace(r.PostFormValue("cardUID")) - initialAmount := strings.TrimSpace(r.PostFormValue("initialAmount")) + var req struct { + CardUID string `json:"cardUID"` + InitialAmount string `json:"initialAmount"` + } + + // Try reading JSON body first + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + // Fallback to post form values + if err := r.ParseForm(); err == nil { + req.CardUID = r.PostFormValue("cardUID") + req.InitialAmount = r.PostFormValue("initialAmount") + } + } + + cardUID := strings.TrimSpace(req.CardUID) + initialAmount := strings.TrimSpace(req.InitialAmount) // Validate required fields if cardUID == "" || initialAmount == "" { - h.Tpl.ExecuteTemplate(w, "addCards.html", message.MessageData{Error: "Please fill in all required fields."}) + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: false, + Message: "Please fill in all required fields.", + }) return } @@ -60,36 +83,37 @@ func (h *Handler) AddCardHandler(w http.ResponseWriter, r *http.Request) { // Set default card type cardType := "Regular" - fmt.Printf("Set card type: %s\n", cardType) // Auto-calculate expiry date as 10 years from now expiryDate := time.Now().AddDate(10, 0, 0).Format("2006-01-02") - fmt.Printf("Set expiry date: %s\n", expiryDate) // Convert Initial Amount (String -> Float64) - fmt.Println("Parsing initial amount...") amount, err := strconv.ParseFloat(initialAmount, 64) if err != nil { fmt.Printf("Error parsing amount: %v\n", err) - h.Tpl.ExecuteTemplate(w, "addCards.html", message.MessageData{Error: "Invalid amount format. Must be a number."}) + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: false, + Message: "Invalid amount format. Must be a number.", + }) return } - fmt.Printf("Parsed amount: %.2f\n", amount) // Check for existing card UID - fmt.Printf("Calling cardUIDExist() for UID: %s\n", cardUID) cardUidExist, err := h.cardUIDExist(cardUID) if err != nil { - fmt.Println("Error checking card UID existence:", err) - h.Tpl.ExecuteTemplate(w, "addCards.html", message.MessageData{Error: "Error checking card UID."}) + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: false, + Message: "Error checking card UID.", + }) return } if cardUidExist { - fmt.Println("Card UID already exists") - h.Tpl.ExecuteTemplate(w, "addCards.html", message.MessageData{Error: "Card UID already exists."}) + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: false, + Message: "Card UID already exists.", + }) return } - fmt.Println("Card UID is unique") // Create a new Card struct card := Card{ @@ -101,28 +125,25 @@ func (h *Handler) AddCardHandler(w http.ResponseWriter, r *http.Request) { } // Check for existing card number - fmt.Printf("Calling cardNumberExist() for card number: %s\n", card.CardNumber) cardNumExists, err := h.cardNumberExist(card) if err != nil { - fmt.Println("Error checking card number existence:", err) - h.Tpl.ExecuteTemplate(w, "addCards.html", message.MessageData{Error: "Error checking card number."}) + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: false, + Message: "Error checking card number.", + }) return } if cardNumExists { - fmt.Println("Card number already exists") - h.Tpl.ExecuteTemplate(w, "addCards.html", message.MessageData{Error: "Card number already exists."}) + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: false, + Message: "Card number already exists.", + }) return } - fmt.Println("Card number is unique") - // Create current timestamp - // Output example: 2024-06-15 14:30:00 - fmt.Println("Generating timestamp...") createdAt := time.Now().Format("2006-01-02 15:04:05") - fmt.Printf("Timestamp: %s\n", createdAt) // Insert card into database - fmt.Println("Executing database insert...") query := "INSERT INTO cards (card_uid, card_number, card_type, initial_amount, expiry_date, created_at) VALUES (?, ?, ?, ?, ?, ?)" _, err = h.DB.Exec( query, @@ -131,61 +152,58 @@ func (h *Handler) AddCardHandler(w http.ResponseWriter, r *http.Request) { card.CardType, card.InitialAmount, card.ExpiryDate, - createdAt) + createdAt, + ) if err != nil { fmt.Println("Error inserting card into database:", err) - h.Tpl.ExecuteTemplate(w, "addCards.html", message.MessageData{Error: "Error while adding card."}) + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: false, + Message: "Error while adding card.", + }) return } // Successfully added the card - fmt.Printf("Card added successfully: %s\n", card.CardNumber) - h.Tpl.ExecuteTemplate(w, "addCards.html", message.MessageData{Success: "Card added successfully!"}) + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: true, + Message: "Card added successfully!", + }) } //--- HELPER FUNCTIONS --- -// Check card UID existence -// We can use this before adding a new card to avoid duplicates func (h *Handler) cardUIDExist(card string) (bool, error) { var uid string query := "SELECT card_uid FROM cards WHERE card_uid = ?" err := h.DB.QueryRow(query, card).Scan(&uid) if err == sql.ErrNoRows { - return false, nil // UID is unique + return false, nil } else if err != nil { - fmt.Println("Error checking card UID existence:", err) - return false, err // Exit on DB error + return false, err } - return true, nil // UID exists + return true, nil } -// Check if card number already exists. -// We can use this before adding a new card to avoid duplicates func (h *Handler) cardNumberExist(card Card) (bool, error) { var cardNum string query := "SELECT card_number FROM cards WHERE card_number = ?" err := h.DB.QueryRow(query, card.CardNumber).Scan(&cardNum) if err == sql.ErrNoRows { - return false, nil // Card number is unique + return false, nil } else if err != nil { - fmt.Println("Error checking card existence:", err) return false, err } - return true, nil // Card number exists + return true, nil } -// Generate a unique card number with format YYDDMM + 10 random digits func (h *Handler) generateCardNumber() string { rng := rand.New(rand.NewSource(time.Now().UnixNano())) - // Get current date components - now := time.Now() // Fixed prefix for all cards - year := now.Format("06") // YY format - month := now.Format("01") // MM format - day := now.Format("02") // DD format - datePrefix := year + day + month // YYDDMM format - - // Generate remaining 10 random digits to make 16 total - randomNum := rng.Intn(10000000000) + now := time.Now() + year := now.Format("06") + month := now.Format("01") + day := now.Format("02") + datePrefix := year + day + month + + randomNum := rng.Intn(1000000000) return fmt.Sprintf("%s%010d", datePrefix, randomNum) } diff --git a/backend/internal/admin/dashboard.go b/backend/internal/admin/dashboard.go new file mode 100644 index 0000000..73426ba --- /dev/null +++ b/backend/internal/admin/dashboard.go @@ -0,0 +1,140 @@ +package admin + +import ( + "database/sql" + "encoding/json" + "fmt" + "net/http" +) + +// 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"` +} + +// AdminStats contains statistics about cards +type AdminStats struct { + Total int `json:"total"` + Active int `json:"active"` + Inactive int `json:"inactive"` + Blocked int `json:"blocked"` + Lost int `json:"lost"` +} + +// DashboardView handles rendering the static admin dashboard view +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) + if err != nil { + fmt.Printf("Template execution error: %v\n", err) + } +} + +// DashboardDataHandler returns the stats and cards list as JSON +func (h *Handler) DashboardDataHandler(w http.ResponseWriter, r *http.Request) { + fmt.Println("DashboardDataHandler running...") + + //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) + h.DB.QueryRow("SELECT COUNT(*) FROM cards WHERE status = 'Active'").Scan(&stats.Active) + h.DB.QueryRow("SELECT COUNT(*) FROM cards WHERE status = 'Inactive'").Scan(&stats.Inactive) + h.DB.QueryRow("SELECT COUNT(*) FROM cards WHERE status = 'Blocked'").Scan(&stats.Blocked) + h.DB.QueryRow("SELECT COUNT(*) FROM cards WHERE status = 'Lost'").Scan(&stats.Lost) + + // 2. Fetch Cards + rows, err := h.DB.Query(` + SELECT id, card_uid, card_number, card_type, initial_amount, expiry_date, status, created_at, card_holder, user_id + FROM cards + ORDER BY created_at DESC + `) + if err != nil { + fmt.Printf("Error fetching cards: %v\n", err) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode(map[string]string{"error": "Failed to fetch cards"}) + return + } + defer rows.Close() + + var cards []AdminCard + for rows.Next() { + var c AdminCard + var cardHolderNull, userIDNull sql.NullString + var createdAtNull sql.NullString + err := rows.Scan( + &c.ID, + &c.CardUID, + &c.CardNumber, + &c.CardType, + &c.InitialAmount, + &c.ExpiryDate, + &c.Status, + &createdAtNull, + &cardHolderNull, + &userIDNull, + ) + if err != nil { + fmt.Printf("Error scanning card row: %v\n", err) + continue + } + + if createdAtNull.Valid { + c.CreatedAt = createdAtNull.String + } else { + c.CreatedAt = "" + } + + if cardHolderNull.Valid { + c.CardHolder = cardHolderNull.String + } else { + c.CardHolder = "Unlinked" + } + + if userIDNull.Valid { + c.UserID = userIDNull.String + } else { + c.UserID = "None" + } + + cards = append(cards, c) + } + + resp := struct { + Stats AdminStats `json:"stats"` + Cards []AdminCard `json:"cards"` + }{ + Stats: stats, + Cards: cards, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) +} diff --git a/backend/internal/admin/deactivateCard.go b/backend/internal/admin/deactivateCard.go index b0d0ea2..37f542b 100644 --- a/backend/internal/admin/deactivateCard.go +++ b/backend/internal/admin/deactivateCard.go @@ -1,45 +1,66 @@ package admin import ( + "encoding/json" "fmt" "net/http" "strings" - message "unicard-go/backend/internal/pkg" + jsonwrite "unicard-go/backend/internal/pkg/handler" ) -// This struct represents the details of a card that we want to deactivate. -// We can use it to easily pass card data around in our functions. -// It also helps to keep our code organized and makes it easier to manage card-related data when we process the deactivation form submission. -type CardDetails struct { - CardNumber string - CardType string -} - -// This function renders the deactivateCard.html template when the admin visits the /admin/deactivatecard page. -// It doesn't do any processing yet, it just shows the form to the admin. -// We can also pass an empty structMessage.MessageData struct to the template, which allows us to easily display error or success messages later on when we process the form submission. +// DeactivateView renders the deactivateCard.html template after verifying session. func (h *Handler) DeactivateView(w http.ResponseWriter, r *http.Request) { fmt.Println("DeactivateView running...") - // Render the deactivateCard.html template - h.Tpl.ExecuteTemplate(w, "deactivateCard.html", message.MessageData{}) + + // 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) } -// This function handles the form submission from the deactivateCard.html page. +// DeactivateCardHanlder handles deactivating a card and returns a JSON response. func (h *Handler) DeactivateCardHanlder(w http.ResponseWriter, r *http.Request) { - fmt.Println("Deactivate card handler running...") + fmt.Println("DeactivateCardHanlder running...") - if err := r.ParseForm(); err != nil { - h.Tpl.ExecuteTemplate(w, "deactivateCard.html", message.MessageData{Error: "Failed to parse form"}) + // 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 } - cardNumber := strings.TrimSpace(r.PostFormValue("cardNumber")) - cardHolder := strings.TrimSpace(r.PostFormValue("name")) - cardType := strings.TrimSpace(r.PostFormValue("cardType")) + var req struct { + CardNumber string `json:"cardNumber"` + Name string `json:"name"` + CardType string `json:"cardType"` + } + + // 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") + req.Name = r.PostFormValue("name") + req.CardType = r.PostFormValue("cardType") + } + } + + cardNumber := strings.TrimSpace(req.CardNumber) + cardHolder := strings.TrimSpace(req.Name) + cardType := strings.TrimSpace(req.CardType) if cardNumber == "" || cardHolder == "" || cardType == "" { - fmt.Println("Missing required fields:", cardNumber, cardHolder, cardType) - h.Tpl.ExecuteTemplate(w, "deactivateCard.html", message.MessageData{Error: "Please fill in all required fields."}) + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: false, + Message: "Please fill in all required fields.", + }) return } @@ -47,24 +68,31 @@ func (h *Handler) DeactivateCardHanlder(w http.ResponseWriter, r *http.Request) ok, err := h.deactivateCardIfActive(cardNumber, cardHolder, cardType) if err != nil { fmt.Println("Error while deactivating card:", err) - h.Tpl.ExecuteTemplate(w, "deactivateCard.html", message.MessageData{Error: "Failed to deactivate card."}) + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: false, + Message: "Failed to deactivate card.", + }) return } if !ok { fmt.Printf("Card not found or already inactive: %s with Card Type: %s\n", cardNumber, cardType) - h.Tpl.ExecuteTemplate(w, "deactivateCard.html", message.MessageData{Error: "Card not found or already inactive."}) + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: false, + Message: "Card not found, name/type mismatch, or card is already inactive.", + }) return } fmt.Println("Card deactivated successfully:", cardNumber, cardType) - h.Tpl.ExecuteTemplate(w, "deactivateCard.html", message.MessageData{Success: "Card deactivated successfully!"}) + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: true, + Message: "Card deactivated successfully!", + }) } // --- Helper functions --- -// This function checks if a card with the given number and type is active, -// and if so, it deactivates it by updating its status in the database. func (h *Handler) deactivateCardIfActive(cardNumber, cardHolder, cardType string) (bool, error) { result, err := h.DB.Exec(` UPDATE cards diff --git a/backend/internal/admin/deleteCard.go b/backend/internal/admin/deleteCard.go new file mode 100644 index 0000000..0a9e4d2 --- /dev/null +++ b/backend/internal/admin/deleteCard.go @@ -0,0 +1,90 @@ +package admin + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + jsonwrite "unicard-go/backend/internal/pkg/handler" +) + +// 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...") + + // 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{ + Success: false, + Message: "Method not allowed", + }) + return + } + + var req struct { + CardNumber string `json:"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") + } + } + + cardNumber := strings.TrimSpace(req.CardNumber) + + if cardNumber == "" { + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: false, + Message: "Card number is required for deletion.", + }) + return + } + + // Delete from the database + result, err := h.DB.Exec("DELETE FROM cards WHERE card_number = ?", cardNumber) + if err != nil { + 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.", + }) + return + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + fmt.Println("Error reading rows affected:", err) + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: false, + Message: "Failed to confirm card deletion.", + }) + return + } + + if rowsAffected == 0 { + fmt.Printf("Card %s not found for deletion\n", cardNumber) + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: false, + Message: "Card not found.", + }) + return + } + + fmt.Println("Card deleted successfully:", cardNumber) + jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.APIResponse{ + Success: true, + Message: "Card deleted successfully!", + }) +} diff --git a/backend/internal/admin/super_admin_pages.go b/backend/internal/admin/super_admin_pages.go new file mode 100644 index 0000000..b570c32 --- /dev/null +++ b/backend/internal/admin/super_admin_pages.go @@ -0,0 +1,42 @@ +package admin + +import ( + "log" + "net/http" +) + +// 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) + } +} + +// MerchantManagementView serves the Merchant Management page +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) + } +} + +// TerminalRegistryView serves the Hardware Registry page +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) + } +} + +// 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) + if err != nil { + log.Printf("Template execution error: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } +} diff --git a/backend/internal/auth/login.go b/backend/internal/auth/login.go index 66886cd..3a328d7 100644 --- a/backend/internal/auth/login.go +++ b/backend/internal/auth/login.go @@ -11,23 +11,16 @@ import ( "golang.org/x/crypto/bcrypt" ) +// Used Login Request from Frontend type LoginRequest struct { - ID string `json:"id,omitempty" db:"ID"` // Optional: can be used for direct ID login - UserID string `json:"userId,omitempty" db:"user_id"` // Optional: can be used for direct user ID login - Identifier string `json:"identifier" validate:"required" db:"full_name, email, phone"` // Allow login via email or username - Password string `json:"password" db:"password_hash" validate:"required"` // Expecting the password hash in the database -} - -// This struct is used to get the data from the database -type Login struct { - ID string `db:"ID"` - UserID string `db:"user_id"` - Username string `db:"username"` - Password string `db:"password_hash"` + ID string `json:"id,omitempty" db:"id"` // Optional: can be used for direct ID login + UserID string `json:"userId,omitempty" db:"user_id"` // Optional: can be used for direct user ID login + Identifier string `json:"identifier" validate:"required" db:"name, email, phone"` // Allow login via email or username + Password string `json:"password" db:"password_hash" validate:"required"` // Expecting the password hash in the database } // Validator instance for struct validation -// Initialize the validator for all handlers +// Initialize the validator for all handlers var Validate = validator.New() // View Handler (GET) @@ -81,22 +74,22 @@ func (h *Handler) LoginAuthHandler(w http.ResponseWriter, r *http.Request) { return } - // Query to check if credential matches var ( hash string // Store the password hash from the database ID string // Store the ID userID string // Store the user ID for successful login response + role string // Store the role ) - stmt := "SELECT ID, user_id, password_hash FROM users WHERE email = ? OR username = ? OR phone = ?" - err = h.DB.QueryRow(stmt, loginReq.Identifier, loginReq.Identifier, loginReq.Identifier).Scan(&ID, &userID, &hash) + stmt := "SELECT id, user_id, password_hash, role FROM users WHERE email = ? OR username = ? OR phone_number = ?" + err = h.DB.QueryRow(stmt, loginReq.Identifier, loginReq.Identifier, loginReq.Identifier).Scan(&ID, &userID, &hash, &role) // User not found if err != nil { log.Printf("User not found or DB error: %v", err) jsonwrite.WriteJSON(w, http.StatusUnauthorized, jsonwrite.APIResponse{ Success: false, - Message: "Incorrect phone number or password", + Message: "Incorrect username or password", }) return } @@ -112,12 +105,21 @@ func (h *Handler) LoginAuthHandler(w http.ResponseWriter, r *http.Request) { return } - // SUCCESS + // Determine redirect based on role + redirectURL := "/dashboard" // Default for customer + if role == "super_admin" { + redirectURL = "/admin/platform-overview" // Super admin dashboard + } else if role == "merchant_admin" || role == "merchant_staff" { + redirectURL = "/merchant/dashboard" // Merchant dashboard + } + + // SUCCESS User Login log.Printf("Login success for user: %s", loginReq.Identifier) jsonwrite.WriteJSON(w, http.StatusOK, jsonwrite.LoginResponse{ - Success: true, - Message: "Login successful", - ID: ID, - UserID: userID, + Success: true, + Message: "Login successful", + ID: ID, + UserID: userID, + RedirectURL: redirectURL, }) } diff --git a/backend/internal/pkg/handler/jsonWrite.go b/backend/internal/pkg/handler/jsonWrite.go index 17caeb1..3eb07ef 100644 --- a/backend/internal/pkg/handler/jsonWrite.go +++ b/backend/internal/pkg/handler/jsonWrite.go @@ -14,10 +14,11 @@ type APIResponse struct { // Login specific response — returns user data after login type LoginResponse struct { - Success bool `json:"success"` - Message string `json:"message"` - ID string `json:"id,omitempty"` // Optional: include user ID in response - UserID string `json:"userid,omitempty"` // Optional: include user ID in response + Success bool `json:"success"` + Message string `json:"message"` + ID string `json:"id,omitempty"` // Optional: include user ID in response + UserID string `json:"userid,omitempty"` // Optional: include user ID in response + RedirectURL string `json:"redirect_url,omitempty"` } // Auth Handler (POST) - Converted to JSON API diff --git a/backend/internal/user/dashboard.go b/backend/internal/user/dashboard.go index fb06ddb..7412abb 100644 --- a/backend/internal/user/dashboard.go +++ b/backend/internal/user/dashboard.go @@ -1,8 +1,12 @@ package user import ( + "database/sql" "fmt" "net/http" + "strings" + "time" + jsonwrite "unicard-go/backend/internal/pkg/handler" ) // Transaction struct represents a user's transaction for the dashboard view @@ -19,36 +23,170 @@ type DashboardUser struct { UserID string `json:"user_id,omitempty" db:"user_id"` Username string `json:"username" db:"username"` Name string `json:"name" db:"name"` - Transaction string `json:"transaction" db:"transaction"` + Email string `json:"email" db:"email"` + Phone string `json:"phone" db:"phone"` + Initials string `json:"initials"` Balance float64 `json:"balance" db:"balance"` - LoyaltyPoints int `json:"loyalty_points" db:"loyalty_points"` + LoyaltyPoints float64 `json:"loyalty_points" db:"loyalty_points"` AccountType string `db:"account_type" json:"account_type"` + CardNumber string `json:"card_number"` + CardExpiry string `json:"card_expiry"` + CardStatus string `json:"card_status"` RecentTransactions []Transaction `json:"recent_transactions"` // Add recent transactions to the dashboard response } func (h *Handler) DashboardView(w http.ResponseWriter, r *http.Request) { fmt.Println("Dashboard view is running...") + + // 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 + } + h.Tpl.ExecuteTemplate(w, "dashboard.html", nil) } func (h *Handler) DashboardHandler(w http.ResponseWriter, r *http.Request) { - fmt.Println("Dashboard handler is running...") + 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 + + // Fetch user and card details + var ( + id int + username string + fullName string + email string + phone string + userType string + balance float64 + loyaltyPoints float64 + cardNumber string + expiryDate string + cardStatus string + ) + 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 = ? + ` + err = h.DB.QueryRow(stmt, userID).Scan(&id, &username, &fullName, &email, &phone, &userType, &balance, &loyaltyPoints, &cardNumber, &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, + }) + jsonwrite.WriteJSON(w, http.StatusUnauthorized, jsonwrite.APIResponse{ + Success: false, + Message: "Unauthorized: User not found", + }) + return + } + + // Generate Initials + initials := "" + parts := strings.Fields(fullName) + if len(parts) > 0 { + initials += string([]rune(parts[0])[0]) + if len(parts) > 1 { + initials += string([]rune(parts[len(parts)-1])[0]) + } + } + if initials == "" { + initials = "U" + } + initials = strings.ToUpper(initials) + + // Format Expiry Date + expiryStr := "MM/YY" + if len(expiryDate) >= 10 { + tExpiry, errT := time.Parse("2006-01-02", expiryDate[:10]) + if errT == nil { + expiryStr = tExpiry.Format("01/06") + } + } + + // 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) + 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) + transactions = append(transactions, t) + } + } + } else { + fmt.Printf("Error fetching transactions: %v\n", err) + } - // For demonstration, we'll create a dummy user with some transactions dashboardUser := DashboardUser{ - ID: 1, - UserID: "user123", - Username: "johndoe", - Name: "John Doe", - Transaction: "Transaction", - Balance: 150.75, - LoyaltyPoints: 200, - AccountType: "Premium", - RecentTransactions: []Transaction{ - {Date: "2024-06-01", Description: "Grocery Store", Type: "Debit", Amount: 50.25}, - {Date: "2024-06-03", Description: "Salary", Type: "Credit", Amount: 2000.00}, - {Date: "2024-06-05", Description: "Online Shopping", Type: "Debit", Amount: 75.50}, - }, - } - h.Tpl.ExecuteTemplate(w, "dashboard.html", dashboardUser) + ID: id, + UserID: userID, + Username: username, + Name: fullName, + Email: email, + Phone: phone, + Initials: initials, + Balance: balance, + LoyaltyPoints: loyaltyPoints, + AccountType: userType, + CardNumber: cardNumber, + CardExpiry: expiryStr, + CardStatus: cardStatus, + RecentTransactions: transactions, + } + + jsonwrite.WriteJSON(w, http.StatusOK, dashboardUser) +} + +func formatDate(dbTime string) string { + t, err := time.Parse("2006-01-02 15:04:05", dbTime) + if err == nil { + return t.Format("Jan _2, 2006") + } + t2, err := time.Parse(time.RFC3339, dbTime) + if err == nil { + return t2.Format("Jan _2, 2006") + } + if len(dbTime) >= 10 { + return dbTime[:10] + } + return dbTime } diff --git a/backend/internal/user/pages.go b/backend/internal/user/pages.go new file mode 100644 index 0000000..a1efeb4 --- /dev/null +++ b/backend/internal/user/pages.go @@ -0,0 +1,105 @@ +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/docs/unicard.sql b/docs/unicard.sql new file mode 100644 index 0000000..98de3b5 --- /dev/null +++ b/docs/unicard.sql @@ -0,0 +1,407 @@ +CREATE DATABASE IF NOT EXISTS `unicard` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci */ /*!80016 DEFAULT ENCRYPTION='N' */; +USE `unicard`; +-- MySQL dump 10.13 Distrib 8.0.44, for Win64 (x86_64) +-- +-- Host: 127.0.0.1 Database: unicard +-- ------------------------------------------------------ +-- Server version 8.0.44 + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; + +-- +-- Table structure for table `admin_users` +-- + +DROP TABLE IF EXISTS `admin_users`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `admin_users` ( + `id` int NOT NULL AUTO_INCREMENT, + `username` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `full_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `password_hash` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Bcrypt password hash', + `role` enum('Super admin','Admin','Support','Viewer') COLLATE utf8mb4_unicode_ci DEFAULT 'Admin', + `status` enum('Active','Inactive','Suspended') COLLATE utf8mb4_unicode_ci DEFAULT 'Active', + `last_login` timestamp NULL DEFAULT NULL, + `login_attempts` int DEFAULT '0', + `locked_until` timestamp NULL DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `username` (`username`), + UNIQUE KEY `email` (`email`), + KEY `idx_username` (`username`), + KEY `idx_email` (`email`), + KEY `idx_role` (`role`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='System administrator accounts'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `admin_users` +-- + +LOCK TABLES `admin_users` WRITE; +/*!40000 ALTER TABLE `admin_users` DISABLE KEYS */; +INSERT INTO `admin_users` VALUES (1,'devzeeh','roxas.johnerrol@gmail.com','john errol','$2a$12$tjycPwp4svJajA6cAIywK.61wL/236Eht6E/1PgyyG1zQJM3KnWue','Admin','Active',NULL,0,NULL,'2026-05-20 02:01:11','2026-05-20 02:01:11'); +/*!40000 ALTER TABLE `admin_users` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `card_reports` +-- + +DROP TABLE IF EXISTS `card_reports`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `card_reports` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `card_number` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Printed 16-digit card number', + `reason` enum('Lost','Stolen','Fraud','User request','Admin action') COLLATE utf8mb4_unicode_ci NOT NULL, + `description` text COLLATE utf8mb4_unicode_ci, + `reported_by` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'User ID of the person who filed the report (user or admin)', + `status` enum('Reported','Resolved','Replaced') COLLATE utf8mb4_unicode_ci NOT NULL, + `replacement_card_number` varchar(16) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `blocked_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `resolved_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `replacement_card_number` (`replacement_card_number`), + KEY `idx_user_id` (`user_id`), + KEY `idx_card_number` (`card_number`), + KEY `idx_status` (`status`), + CONSTRAINT `card_reports_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE, + CONSTRAINT `card_reports_ibfk_2` FOREIGN KEY (`card_number`) REFERENCES `cards` (`card_number`), + CONSTRAINT `card_reports_ibfk_3` FOREIGN KEY (`replacement_card_number`) REFERENCES `cards` (`card_number`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Blocked and lost card records'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `card_reports` +-- + +LOCK TABLES `card_reports` WRITE; +/*!40000 ALTER TABLE `card_reports` DISABLE KEYS */; +/*!40000 ALTER TABLE `card_reports` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `cards` +-- + +DROP TABLE IF EXISTS `cards`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `cards` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `card_uid` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'RFID/NFC Tag ID (Internal)', + `card_holder` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `card_number` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Printed 16-digit card number', + `card_type` enum('Regular','PWD','Student','Senior') COLLATE utf8mb4_unicode_ci DEFAULT 'Regular', + `expiry_date` date DEFAULT NULL, + `cvv` varchar(3) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Security code (optional storage)', + `initial_amount` decimal(10,2) DEFAULT '0.00' COMMENT 'Amount to credit user upon registration (Set first by ADMIN)', + `status` enum('Active','Inactive','Blocked','Lost','Expired') COLLATE utf8mb4_unicode_ci DEFAULT 'Inactive', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'When card was added to inventory', + `is_primary` tinyint(1) DEFAULT '0' COMMENT 'Is this the primary card for the user?', + `linked_at` timestamp NULL DEFAULT NULL COMMENT 'Card was linked to user', + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `card_uid` (`card_uid`), + UNIQUE KEY `card_number` (`card_number`), + KEY `user_id` (`user_id`), + KEY `idx_card_uid` (`card_uid`), + KEY `idx_card_number` (`card_number`), + KEY `idx_status` (`status`), + KEY `idx_card_holder` (`card_holder`), + CONSTRAINT `cards_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE SET NULL +) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Master inventory of physical cards'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `cards` +-- + +LOCK TABLES `cards` WRITE; +/*!40000 ALTER TABLE `cards` DISABLE KEYS */; +INSERT INTO `cards` VALUES (1,'840570851385','A346F101','errol roxas','2621020251171655','Regular','2036-04-09',NULL,100.00,'Active','2026-02-14 08:50:13',0,NULL,'2026-04-09 09:33:05'),(3,'173886440021','93C13FED','john errol devss','2621028394671655','Regular','2036-02-21',NULL,100.00,'Active','2026-02-21 08:18:02',0,NULL,'2026-04-09 09:40:23'),(4,'615921401740','7310BCEC','john dev','2621020252341655','Regular','2036-02-21',NULL,100.00,'Active','2026-02-21 08:19:52',0,'2026-04-09 10:24:58','2026-04-09 10:24:58'),(5,'573343578549','53A327EC','maja salvador','2621028700527532','Regular','2036-02-21',NULL,100.00,'Active','2026-02-21 08:33:19',0,'2026-04-09 10:35:51','2026-04-09 10:35:51'),(6,'427079423054','F3BF44EC','julia baretto','2621062102879616','Regular','2036-04-09',NULL,100.00,'Active','2026-02-21 08:37:42',0,'2026-04-09 10:40:59','2026-04-09 10:40:59'),(7,'799817330001','038981EC','frances cruz','2621062102292676','Regular','2036-04-28',NULL,100.00,'Active','2026-02-21 08:41:57',0,'2026-04-28 06:05:53','2026-04-28 06:05:53'),(8,'849645895086','2067BE14','ash cue','2621028885926930','Regular','2036-04-29',NULL,100.00,'Active','2026-02-21 08:45:59',0,'2026-04-29 04:17:44','2026-04-29 04:17:44'),(9,'242070109521','50B32514','mhissy acosta','2621027620974570','Regular','2036-04-29',NULL,100.00,'Active','2026-02-21 08:47:26',0,'2026-04-29 04:31:54','2026-04-29 04:31:54'),(10,'100957470319','43006A19','lauren yen','2621020251771655','Regular','2036-05-01',NULL,100.00,'Active','2026-02-21 08:52:17',0,'2026-05-01 03:50:25','2026-05-01 03:50:25'),(11,NULL,'0005584041',NULL,'2601050762341385','Regular','2036-05-01',NULL,100.00,'Inactive','2026-05-01 04:08:10',0,NULL,'2026-05-01 04:08:10'); +/*!40000 ALTER TABLE `cards` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `fare_settings` +-- + +DROP TABLE IF EXISTS `fare_settings`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `fare_settings` ( + `id` int NOT NULL AUTO_INCREMENT, + `merchant_code` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `business_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `base_fare` decimal(10,2) DEFAULT '13.00', + `per_km_rate` decimal(10,2) DEFAULT '1.50', + `minimum_fare` decimal(10,2) DEFAULT '13.00', + `pwd_discount_rate` decimal(5,2) DEFAULT '20.00', + `student_discount_rate` decimal(5,2) DEFAULT '20.00', + `senior_discount_rate` decimal(5,2) DEFAULT '20.00', + `loyalty_rate` decimal(5,4) DEFAULT '0.0020' COMMENT '0.0020 = 0.2% cashback', + `route_number` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'For transport: route or plate number', + `effective_from` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `effective_to` timestamp NULL DEFAULT NULL, + `is_active` tinyint(1) DEFAULT '1', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `merchant_code` (`merchant_code`), + KEY `idx_route_number` (`route_number`), + KEY `idx_merchant_code` (`merchant_code`), + KEY `idx_is_active` (`is_active`), + CONSTRAINT `fare_settings_ibfk_1` FOREIGN KEY (`merchant_code`) REFERENCES `merchants` (`merchant_code`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Configurable fare and discount settings'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `fare_settings` +-- + +LOCK TABLES `fare_settings` WRITE; +/*!40000 ALTER TABLE `fare_settings` DISABLE KEYS */; +/*!40000 ALTER TABLE `fare_settings` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `loyalty_redemptions` +-- + +DROP TABLE IF EXISTS `loyalty_redemptions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `loyalty_redemptions` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `transaction_id` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `points_used` decimal(10,2) NOT NULL, + `value_php` decimal(10,2) NOT NULL COMMENT 'PHP value of redeemed points', + `status` enum('Pending','Completed','Cancelled') COLLATE utf8mb4_unicode_ci DEFAULT 'Completed', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `transaction_id` (`transaction_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_created_at` (`created_at`), + KEY `idx_transaction_id` (`transaction_id`), + CONSTRAINT `loyalty_redemptions_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE, + CONSTRAINT `loyalty_redemptions_ibfk_2` FOREIGN KEY (`transaction_id`) REFERENCES `transactions` (`user_id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Loyalty points redemption history'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `loyalty_redemptions` +-- + +LOCK TABLES `loyalty_redemptions` WRITE; +/*!40000 ALTER TABLE `loyalty_redemptions` DISABLE KEYS */; +/*!40000 ALTER TABLE `loyalty_redemptions` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `merchants` +-- + +DROP TABLE IF EXISTS `merchants`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `merchants` ( + `id` int NOT NULL AUTO_INCREMENT, + `merchant_code` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Public merchant identifier', + `business_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `business_type` enum('Transport','Retail','Both') COLLATE utf8mb4_unicode_ci NOT NULL, + `owner_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `phone` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL, + `address` text COLLATE utf8mb4_unicode_ci, + `route_number` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'For transport: route or plate number', + `store_location` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'For retail: store address', + `commission_rate` decimal(5,2) DEFAULT '0.00' COMMENT 'Commission percentage', + `settlement_account` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Bank account for settlements', + `status` enum('Active','Suspended','Inactive','Removed') COLLATE utf8mb4_unicode_ci DEFAULT 'Active', + `verified` tinyint(1) DEFAULT '0' COMMENT 'Verified status indicates if the merchant has completed KYC/verification', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `merchant_code` (`merchant_code`), + KEY `idx_merchant_code` (`merchant_code`), + KEY `idx_business_type` (`business_type`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Merchant accounts (drivers and store owners)'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `merchants` +-- + +LOCK TABLES `merchants` WRITE; +/*!40000 ALTER TABLE `merchants` DISABLE KEYS */; +/*!40000 ALTER TABLE `merchants` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `receipts` +-- + +DROP TABLE IF EXISTS `receipts`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `receipts` ( + `id` int NOT NULL AUTO_INCREMENT, + `transaction_id` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL, + `user_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `receipt_number` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `receipt_type` enum('Payment','Topup','Refund') COLLATE utf8mb4_unicode_ci NOT NULL, + `email_sent` tinyint(1) DEFAULT '0', + `email_sent_at` timestamp NULL DEFAULT NULL, + `email_opened` tinyint(1) DEFAULT '0', + `pdf_generated` tinyint(1) DEFAULT '0', + `pdf_data` longblob, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `receipt_number` (`receipt_number`), + KEY `idx_transaction_id` (`transaction_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_receipt_number` (`receipt_number`), + CONSTRAINT `receipts_ibfk_1` FOREIGN KEY (`transaction_id`) REFERENCES `transactions` (`user_id`) ON DELETE CASCADE, + CONSTRAINT `receipts_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='E-receipt generation and tracking'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `receipts` +-- + +LOCK TABLES `receipts` WRITE; +/*!40000 ALTER TABLE `receipts` DISABLE KEYS */; +/*!40000 ALTER TABLE `receipts` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `transactions` +-- + +DROP TABLE IF EXISTS `transactions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `transactions` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL, + `card_number` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL, + `transaction_type` enum('Topup','Payment','Refund','Adjustment') COLLATE utf8mb4_unicode_ci NOT NULL, + `category` enum('Transport','Retail','Other') COLLATE utf8mb4_unicode_ci NOT NULL, + `amount` decimal(10,2) NOT NULL COMMENT 'Final transaction amount', + `balance_before` decimal(10,2) NOT NULL, + `balance_after` decimal(10,2) NOT NULL, + `discount_amount` decimal(10,2) DEFAULT '0.00' COMMENT 'If amount is 8.00 and discount is 2.00, the original price was 10.00.', + `discount_type` enum('Regular','PWD','Student','Senior','Promo') COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Reason for discount (PWD, Student, Senior, Promo)', + `points_earned` decimal(10,2) DEFAULT '0.00', + `description` text COLLATE utf8mb4_unicode_ci COMMENT 'Optional notes or item details (e.g., "Monthly Pass")', + `location` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Route number or store name(Human-readable location (e.g., "Main St. Coffee" or "Bus 42")', + `merchant_code` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'Link to the merchant who received payment (Nullable for system adjustments)', + `terminal_id` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'ID of the physical device/reader used (for debugging/tracking)', + `payment_method` enum('Stripe','E-Wallet','Bank','Cash','Points') COLLATE utf8mb4_unicode_ci NOT NULL, + `reference_id` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'External/Internal REF ID (e.g. Stripe Charge ID, GCash Ref No.)', + `transaction_id` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `status` enum('Pending','Completed','Failed','Refunded') COLLATE utf8mb4_unicode_ci DEFAULT 'Completed', + `failure_reason` text COLLATE utf8mb4_unicode_ci COMMENT 'Error message if status is "Failed" (e.g., "Insufficient funds")', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `completed_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_card_number` (`card_number`), + KEY `idx_transaction_type` (`transaction_type`), + KEY `idx_status` (`status`), + KEY `idx_created_at` (`created_at`), + KEY `idx_merchant_code` (`merchant_code`), + KEY `idx_reference_id` (`reference_id`), + KEY `idx_transaction_id` (`transaction_id`), + CONSTRAINT `transactions_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE, + CONSTRAINT `transactions_ibfk_2` FOREIGN KEY (`merchant_code`) REFERENCES `merchants` (`merchant_code`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='All transaction records (payments, top-ups, refunds)'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `transactions` +-- + +LOCK TABLES `transactions` WRITE; +/*!40000 ALTER TABLE `transactions` DISABLE KEYS */; +/*!40000 ALTER TABLE `transactions` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `users` +-- + +DROP TABLE IF EXISTS `users`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `users` ( + `id` int NOT NULL AUTO_INCREMENT, + `user_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Public user ID (e.g., account number)', + `username` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Login username', + `full_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `phone` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `password_hash` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Bcrypt password hash', + `card_id` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'RFID card unique identifier (internal)', + `card_number` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '16-digit printed card number', + `user_type` enum('Regular','PWD','Student','Senior') COLLATE utf8mb4_unicode_ci DEFAULT 'Regular', + `balance` decimal(10,2) DEFAULT '0.00' COMMENT 'Current card balance in PHP', + `loyalty_points` decimal(10,2) DEFAULT '0.00' COMMENT 'Accumulated loyalty points', + `status` enum('Active','Blocked','Inactive') COLLATE utf8mb4_unicode_ci DEFAULT 'Active', + `id_verified` tinyint(1) DEFAULT '0' COMMENT 'Has user uploaded valid ID', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `last_login` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `user_id` (`user_id`), + UNIQUE KEY `username` (`username`), + UNIQUE KEY `email` (`email`), + UNIQUE KEY `card_id` (`card_id`), + UNIQUE KEY `card_number` (`card_number`), + KEY `idx_user_id` (`user_id`), + KEY `idx_username` (`username`), + KEY `idx_email` (`email`), + KEY `idx_card_number` (`card_number`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB AUTO_INCREMENT=52 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='User accounts and card information'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `users` +-- + +LOCK TABLES `users` WRITE; +/*!40000 ALTER TABLE `users` DISABLE KEYS */; +INSERT INTO `users` VALUES (1,'840570851385','user26fxvolv05729','errol roxas','one.devteam.25@gmail.com','09952329743','$2a$10$qpIlzDQLDucvSdNWZy/R3OMdhonkG2bo75WyHZjz8QnOB4NasmbMa','CARD-021426fgxRf9Q','459543887671','Regular',100.00,0.00,'Active',0,'2026-02-13 20:57:29','2026-02-21 04:33:59',NULL),(43,'173886440021','user26ehrzr3e2121','john errol devss','devzeeh@gmail.com','09123456789','$2a$10$1cDLHp3cfBoPtXYzGjkMquqv/whHWE/qlHLFNKgp6kQfZ/IsG8yJi','CARD-040926nzAdIIc','2621028394671655','Regular',100.00,0.00,'Active',0,'2026-04-08 20:21:21','2026-04-17 07:14:57',NULL),(44,'615921401740','user26ilbbt392458','john dev','jjohnroxas06@gmail.com','09123456788','$2a$10$ETcFIEEED8jmkwaK3OicpueNulxYPCEJToeKZjZWTQsA8lSNWHkR.','CARD-040926mcnG6F7','2621020252341655','Regular',100.00,0.00,'Active',0,'2026-04-08 22:24:58','2026-05-20 01:49:20',NULL),(45,'573343578549','user262evjp0v3551','maja salvador','majasalvador@gmail.com','09123123412','$2a$12$u3ySLle7MOBG/2FeOjS96eyCxITcjD10LvYPDOP9pkuvxLe6JHVx2','CARD-040926EvJOB49','2621028700527532','Regular',100.00,0.00,'Active',0,'2026-04-08 22:35:51','2026-04-14 05:08:25',NULL),(47,'427079423054','user26t5tp7ys4059','julia baretto','juliabaretto@gmail.com','0987654321','$2a$10$SnUcFjvugos89TQFqJqDm.6bKCtcrNOdAwvxdLMnvaMU8nrBiMDf.','CARD-040926VQUXMFZ','2621062102879616','Regular',100.00,0.00,'Active',0,'2026-04-08 22:40:59','2026-04-09 10:40:59',NULL),(48,'799817330001','user266iynhsu0553','frances cruz','francescruz@gmail.com','09987654321','$2a$12$xC4wv64kjhMF8Rt6dswkFOleSaU5olPTameBMh5uDX1rGLV6nHEDi','CARD-042826Z2nMhwE','2621062102292676','Regular',100.00,0.00,'Active',0,'2026-04-27 18:05:53','2026-04-28 06:29:20',NULL),(49,'849645895086','user26xx95elc1744','ash cue','jjohnroxas06+dev@gmail.com','09987252723','$2a$10$Hbu8bQ1iCBuIE8U5LCanOeDlqY8WHhE7bNzN6HIHvtSncBMi5fLoC','CARD-042926N1gZYZs','2621028885926930','Regular',100.00,0.00,'Active',0,'2026-04-29 04:17:44','2026-04-29 04:17:44',NULL),(50,'242070109521','user26i89wuxv3154','mhissy acosta','missacosta@gmail.com','09987526323','$2a$10$BimoLj3vyJqFcpquZ5LcauViuR0ZJwwFa0zEG/176yVNbBSTGy7/G','CARD-042926zX5v3kZ','2621027620974570','Regular',100.00,0.00,'Active',0,'2026-04-29 04:31:54','2026-04-29 04:31:54',NULL),(51,'100957470319','user26fw59znn5025','lauren yen','laurenyen@gmail.com','09765434526','$2a$10$/AOys2.oPFXDPOjNQDcBM.vG1WNAF0k3Ej1YvYerqzbIUy5upa8Pa','CARD-050126FFAanLE','2621020251771655','Regular',100.00,0.00,'Active',0,'2026-05-01 03:50:25','2026-05-01 03:50:25',NULL); +/*!40000 ALTER TABLE `users` ENABLE KEYS */; +UNLOCK TABLES; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +-- Dump completed on 2026-05-20 10:05:59 diff --git a/docs/unicardv1.sql b/docs/unicardv1.sql new file mode 100644 index 0000000..7942046 --- /dev/null +++ b/docs/unicardv1.sql @@ -0,0 +1,142 @@ +-- CREATE DATABASE IF NOT EXISTS unicardv1; +-- USE unicardv1; + +-- ========================================================================= +-- 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 +-- ========================================================================= + +CREATE TABLE users ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal row index optimized for database indexing and fast joins', + user_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom public ID (e.g., UNI-YYMM-minsecxxxx) used in APIs and frontend', + username VARCHAR(50) NOT NULL UNIQUE COMMENT 'Unique handle for admin/staff to log in quickly without an email', + name VARCHAR(100) NOT NULL COMMENT 'Full name of the individual user or client contact person', + email VARCHAR(100) UNIQUE NOT NULL COMMENT 'Primary email address used for consumer logins and notifications', + phone_number VARCHAR(20) NULL UNIQUE COMMENT 'Mobile number (e.g., +639...) for OTPs and SMS transaction alerts', + 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 +) 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', + 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', + FOREIGN KEY (updated_by) REFERENCES users(user_id) +) COMMENT='Global platform configuration matrix driving dynamic fees, operational bounds, and system constants'; + +-- ========================================================================= +-- 3. MERCHANT TENANCY & HARDWARE REGISTRY TABLES +-- ========================================================================= + +CREATE TABLE merchants ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal merchant row index used for fast database indexing', + merchant_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom public identifier for the business entity (e.g., MCH-2026-001)', + business_name VARCHAR(150) NOT NULL COMMENT 'Registered trade or company name of the client merchant', + business_type ENUM('retail', 'transportation', 'food_and_beverage', 'services', 'other') NOT NULL COMMENT 'Industry category for transaction filtering and analytics', + 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', + 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', + + commission_rate DECIMAL(5, 2) DEFAULT 2.00 COMMENT 'Percentage cut taken by UniCard per processed card transaction (e.g., 2.50 = 2.5%)', + settlement_account_name VARCHAR(100) NOT NULL COMMENT 'The name on the merchant bank account or mobile wallet for payouts', + settlement_account_number VARCHAR(50) NOT NULL COMMENT 'The actual bank account number or mobile number (GCash/Maya) for payouts', + settlement_bank_name VARCHAR(100) NOT NULL COMMENT 'The target bank or e-wallet company name (e.g., BDO, BPI, GCash, Maya)', + + status ENUM('pending_approval', 'active', 'suspended') DEFAULT 'pending_approval' COMMENT 'Operational state of the merchant ecosystem tenancy', + approved_by VARCHAR(50) NULL COMMENT 'The user_id of the Super Admin who verified and activated this company profile', + approved_at TIMESTAMP NULL COMMENT 'The specific date and timestamp when the business was activated', + 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 (approved_by) REFERENCES users(user_id) ON DELETE SET NULL +) COMMENT='Enterprise business registry tracking partner tenants, hardware mapping nodes, and financial settlement details'; + +CREATE TABLE terminals ( + id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal hardware registry auto-increment row index', + terminal_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom public hardware identifier (e.g., TRM-2026-0001) used in API payloads', + terminal_sn VARCHAR(50) UNIQUE NOT NULL COMMENT 'Physical factory-assigned unique serial number or MAC address of the ESP32 board', + merchant_id INT NOT NULL COMMENT 'Links to the internal auto-increment id of the managing merchant entity', + 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', + 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 +) COMMENT='Hardware node registry tracking deployed physical authentication nodes and network heartbeat states'; + +-- ========================================================================= +-- 4. UTILITY & USER TRANSACTION LOGS TABLES (HIGH GROWING DATASETS) +-- ========================================================================= + +CREATE TABLE cards ( + card_number VARCHAR(20) PRIMARY KEY COMMENT 'The visible consumer-facing identifier printed on the physical plastic token', + card_uid VARCHAR(50) NOT NULL UNIQUE COMMENT 'The physical hardware chip unique UID read directly from the MIFARE/RFID sectors', + user_id VARCHAR(50) NULL COMMENT 'Links cardholder account identity via the public users.user_id string identifier', + card_type ENUM('regular', 'student', 'pwd', 'senior') DEFAULT 'regular' COMMENT 'Drives dynamic discount calculation algorithms across transportation fares', + discount_verified BOOLEAN DEFAULT FALSE COMMENT 'Flags whether regulatory documents were verified for fare discount tier eligibility', + balance DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Current secure stored monetary value assigned to the physical card unit', + loyalty_points DECIMAL(10, 2) DEFAULT 0.00 COMMENT 'Accrued transaction reward points redeemable at verified retail merchant stations', + 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', + 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'; + +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', + 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', + net_merchant_payout DECIMAL(10, 2) GENERATED ALWAYS AS (amount - service_fee) STORED COMMENT 'Automatically calculated column tracking exactly how much money goes to the merchant after our platform cut', + 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 (processed_by) REFERENCES users(user_id) +) COMMENT='High-growth financial master ledger capturing all terminal token taps, transaction classifications, and system fees'; + +CREATE TABLE top_ups ( + id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 'Internal tracking row index scaled to 64-bit headroom to safely accommodate massive historical logging growth', + topup_id VARCHAR(50) NOT NULL UNIQUE COMMENT 'Custom unique public transaction code (e.g., LD-2026-987153) used for user receipts and payment gateway queries', + 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', + 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', + status ENUM('pending', 'completed', 'failed') DEFAULT 'completed' COMMENT 'State pipeline tracker handling payment gateway processing exceptions or drops', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT 'Auto-generated clock timestamp mapping exactly when wallet balance credits were finalized', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Tracks chronological life cycle changes, such as a top-up shifting from pending to completed', + FOREIGN KEY (card_number) REFERENCES cards(card_number), + FOREIGN KEY (handled_by) REFERENCES users(user_id) +) COMMENT='High-growth balance loader ledger maintaining immutable compliance auditing for all incoming ecosystem liquidity channels'; \ No newline at end of file diff --git a/frontend/assets/js/dashboard.js b/frontend/assets/js/dashboard.js index e75b2a3..e6310e7 100644 --- a/frontend/assets/js/dashboard.js +++ b/frontend/assets/js/dashboard.js @@ -138,11 +138,145 @@ document.addEventListener("DOMContentLoaded", function () { // Confirm logout and redirect confirmLogoutButton.addEventListener('click', () => { console.log('Logging out...'); - window.location.href = "login.html"; + window.location.href = "/login"; }); } else { console.error("Logout modal elements not found. Make sure all IDs are correct."); } + // --- Fetch Dashboard Data --- + function fetchDashboardData() { + fetch("/v1/user/dashboard") + .then(response => { + if (response.status === 401) { + window.location.href = "/login"; + return null; + } + return response.json(); + }) + .then(data => { + if (!data) return; + + const fullNameEl = document.getElementById("user-full-name"); + const initialsEl = document.getElementById("user-initials"); + const balanceEl = document.getElementById("user-balance"); + const loyaltyPointsEl = document.getElementById("user-loyalty-points"); + const accountTypeEl = document.getElementById("user-account-type"); + const transactionsBody = document.getElementById("transactions-table-body"); + + const cardNoEl = document.getElementById("user-card-number"); + const cardHolderEl = document.getElementById("user-card-holder"); + const cardExpiryEl = document.getElementById("user-card-expiry"); + const cardStatusEl = document.getElementById("card-status-badge"); + + // Profile Page Specific Elements + const profileViewName = document.getElementById("profile-view-name"); + const profileViewEmail = document.getElementById("profile-view-email"); + const profileViewPhone = document.getElementById("profile-view-phone"); + const profileViewUsername = document.getElementById("profile-view-username"); + const profileEditName = document.getElementById("full_name"); + const profileEditEmail = document.getElementById("email"); + const profileEditPhone = document.getElementById("phone"); + + // Card Page Specific Elements + const cardDetailNumber = document.getElementById("card-detail-number"); + const cardDetailExpiry = document.getElementById("card-detail-expiry"); + + if (fullNameEl) fullNameEl.innerText = data.name || ""; + if (initialsEl) initialsEl.innerText = data.initials || "U"; + if (balanceEl) balanceEl.innerText = Number(data.balance).toFixed(2); + if (loyaltyPointsEl) loyaltyPointsEl.innerText = Number(data.loyalty_points).toFixed(2); + if (accountTypeEl) accountTypeEl.innerText = data.account_type || "Regular"; + + if (profileViewName) profileViewName.innerText = data.name || ""; + if (profileViewEmail) profileViewEmail.innerText = data.email || ""; + if (profileViewPhone) profileViewPhone.innerText = data.phone || ""; + if (profileViewUsername) profileViewUsername.innerText = data.username || ""; + + if (profileEditName) profileEditName.value = data.name || ""; + if (profileEditEmail) profileEditEmail.value = data.email || ""; + if (profileEditPhone) profileEditPhone.value = data.phone || ""; + + if (cardNoEl || cardDetailNumber) { + const rawNum = data.card_number || "••••••••••••••••"; + const formattedNum = rawNum.replace(/(\d{4})/g, '$1 ').trim(); + if (cardNoEl) cardNoEl.innerText = formattedNum || "•••• •••• •••• ••••"; + if (cardDetailNumber) cardDetailNumber.innerText = formattedNum || "•••• •••• •••• ••••"; + } + if (cardHolderEl) { + cardHolderEl.innerText = data.name || "CARDHOLDER NAME"; + } + if (cardExpiryEl || cardDetailExpiry) { + if (cardExpiryEl) cardExpiryEl.innerText = data.card_expiry || "MM/YY"; + if (cardDetailExpiry) cardDetailExpiry.innerText = data.card_expiry || "MM/YY"; + } + if (cardStatusEl) { + const status = data.card_status || "No Card"; + cardStatusEl.textContent = status; + cardStatusEl.className = "px-2 py-0.5 text-[9px] font-bold uppercase rounded-full shadow-sm"; + if (status === "Active") { + cardStatusEl.classList.add("bg-green-500", "text-white"); + } else if (status === "Blocked" || status === "Lost" || status === "Expired") { + cardStatusEl.classList.add("bg-red-500", "text-white"); + } else { + cardStatusEl.classList.add("bg-yellow-500", "text-white"); + } + } + + if (transactionsBody) { + transactionsBody.innerHTML = ""; + if (data.recent_transactions && data.recent_transactions.length > 0) { + data.recent_transactions.forEach(tx => { + const tr = document.createElement("tr"); + const isPayment = tx.type === "Payment"; + const colorClass = isPayment ? "text-red-600" : "text-green-600"; + const sign = isPayment ? "-" : "+"; + const amount = Number(tx.amount).toFixed(2); + + tr.innerHTML = ` + + \${tx.date} + + + \${tx.description} + + + \${tx.type} + + + \${sign}₱\${amount} + + `; + transactionsBody.appendChild(tr); + }); + } else { + transactionsBody.innerHTML = ` + + + No recent transactions found. + + + `; + } + } + }) + .catch(error => { + console.error("Error loading dashboard data:", error); + const transactionsBody = document.getElementById("transactions-table-body"); + if (transactionsBody) { + transactionsBody.innerHTML = ` + + + Failed to load dashboard data. + + + `; + } + }); + } + + // Call fetch on load + fetchDashboardData(); + }); \ No newline at end of file diff --git a/frontend/assets/js/forgot-password.js b/frontend/assets/js/forgot-password.js index 2b5866e..2960ae0 100644 --- a/frontend/assets/js/forgot-password.js +++ b/frontend/assets/js/forgot-password.js @@ -233,8 +233,7 @@ document.addEventListener("DOMContentLoaded", function () { const emailRegex = /^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$/; if (!emailVal || !emailRegex.test(emailVal)) { - // validateEmailDynamically will handle the UI - if (emailInput) emailInput.dispatchEvent(new Event('input')); + showFieldError(emailInput, emailError, "Invalid email format"); return; } diff --git a/frontend/assets/js/login.js b/frontend/assets/js/login.js index 88da77f..b3cad07 100644 --- a/frontend/assets/js/login.js +++ b/frontend/assets/js/login.js @@ -20,7 +20,7 @@ document.addEventListener("DOMContentLoaded", function () { .then(r => r.json()) .then(data => { if (data.success) { - window.location.href = "/dashboard"; + window.location.href = data.redirect_url || "/dashboard"; } else { if (errorMessage) { errorMessage.classList.remove("hidden"); diff --git a/frontend/assets/js/profile.js b/frontend/assets/js/profile.js index 64b0c98..4273f55 100644 --- a/frontend/assets/js/profile.js +++ b/frontend/assets/js/profile.js @@ -3,36 +3,49 @@ document.addEventListener("DOMContentLoaded", function () { // --- Profile Edit Elements --- const editProfileBtn = document.getElementById('edit-profile-btn'); + const cancelEditBtn = document.getElementById('cancel-edit-btn'); + const saveProfileBtn = document.getElementById('save-profile-btn'); + const profileActions = document.getElementById('profile-edit-actions'); const profileView = document.getElementById('profile-details-view'); const profileEditForm = document.getElementById('profile-details-edit'); - if (editProfileBtn && profileView && profileEditForm) { + if (editProfileBtn && cancelEditBtn && profileActions && profileView && profileEditForm && saveProfileBtn) { editProfileBtn.addEventListener('click', () => { - const isEditing = editProfileBtn.textContent.trim() === 'Save'; - - if (isEditing) { - // --- THIS IS A FRONTEND-ONLY DEMO --- - // In a real app, you would send a fetch() request to your backend - // to save the new details from the form. - - // For now, we'll just update the text and switch back - const newName = document.getElementById('full_name').value; - const newEmail = document.getElementById('email').value; - const newPhone = document.getElementById('phone').value; - - profileView.querySelector('dd:nth-of-type(1)').textContent = newName; - profileView.querySelector('dd:nth-of-type(2)').textContent = newEmail; - profileView.querySelector('dd:nth-of-type(3)').textContent = newPhone; - - profileView.classList.remove('hidden'); - profileEditForm.classList.add('hidden'); - editProfileBtn.innerHTML = ' Edit'; - } else { - // Switch to edit mode - profileView.classList.add('hidden'); - profileEditForm.classList.remove('hidden'); - editProfileBtn.innerHTML = ' Save'; - } + profileView.classList.add('hidden'); + profileEditForm.classList.remove('hidden'); + editProfileBtn.classList.add('hidden'); + profileActions.classList.remove('hidden'); + }); + + cancelEditBtn.addEventListener('click', (e) => { + e.preventDefault(); + profileEditForm.classList.add('hidden'); + profileView.classList.remove('hidden'); + profileActions.classList.add('hidden'); + editProfileBtn.classList.remove('hidden'); + }); + + saveProfileBtn.addEventListener('click', (e) => { + e.preventDefault(); + + // In a real app, send fetch() request here + const newName = document.getElementById('full_name').value; + const newEmail = document.getElementById('email').value; + const newPhone = document.getElementById('phone').value; + + // Since dashboard.js manages the span values via ID, we just update the text content of the spans + const nameSpan = document.getElementById('profile-view-name'); + const emailSpan = document.getElementById('profile-view-email'); + const phoneSpan = document.getElementById('profile-view-phone'); + + if (nameSpan) nameSpan.innerText = newName; + if (emailSpan) emailSpan.innerText = newEmail; + if (phoneSpan) phoneSpan.innerText = newPhone; + + profileEditForm.classList.add('hidden'); + profileView.classList.remove('hidden'); + profileActions.classList.add('hidden'); + editProfileBtn.classList.remove('hidden'); }); } diff --git a/frontend/assets/js/topup.js b/frontend/assets/js/topup.js new file mode 100644 index 0000000..a3c67f6 --- /dev/null +++ b/frontend/assets/js/topup.js @@ -0,0 +1,28 @@ +document.addEventListener("DOMContentLoaded", function () { + const quickBtns = document.querySelectorAll('.quick-amount-btn'); + const amountInput = document.getElementById('amount'); + + if (quickBtns && amountInput) { + quickBtns.forEach(btn => { + btn.addEventListener('click', () => { + amountInput.value = btn.innerText; + }); + }); + } + + const form = document.getElementById('topup-form'); + const submitBtn = document.getElementById('topup-submit-btn'); + + if (form && submitBtn) { + form.addEventListener('input', () => { + const amount = parseFloat(amountInput.value); + const method = document.querySelector('input[name="payment_method"]:checked'); + + if (amount > 0 && method) { + submitBtn.disabled = false; + } else { + submitBtn.disabled = true; + } + }); + } +}); diff --git a/frontend/assets/js/transaction.js b/frontend/assets/js/transaction.js new file mode 100644 index 0000000..051918d --- /dev/null +++ b/frontend/assets/js/transaction.js @@ -0,0 +1,80 @@ +document.addEventListener("DOMContentLoaded", function () { + const transactionsBody = document.getElementById("transactions-table-body"); + + function loadTransactions() { + fetch("/v1/user/transactions") + .then(response => { + if (response.status === 401) { + window.location.href = "/login"; + return null; + } + return response.json(); + }) + .then(data => { + if (!data || !data.success) { + showError(); + return; + } + + transactionsBody.innerHTML = ""; + const txs = data.transactions; + + if (txs && txs.length > 0) { + txs.forEach(tx => { + const tr = document.createElement("tr"); + const isPayment = tx.type === "Payment"; + const colorClass = isPayment ? "text-red-600" : "text-green-600"; + const sign = isPayment ? "-" : "+"; + const amount = Number(tx.amount).toFixed(2); + + // We do not have a running balance per transaction from the DB currently, + // so we will just show a dash or N/A in the balance column for now. + tr.innerHTML = ` + + ${tx.date} + + + ${tx.description} + + + ${tx.type} + + + ${sign}₱${amount} + + + - + + `; + transactionsBody.appendChild(tr); + }); + } else { + transactionsBody.innerHTML = ` + + + No transactions found. + + + `; + } + }) + .catch(error => { + console.error("Error loading transactions:", error); + showError(); + }); + } + + function showError() { + if (transactionsBody) { + transactionsBody.innerHTML = ` + + + Failed to load transactions. + + + `; + } + } + + loadTransactions(); +}); diff --git a/frontend/templates/addCards.html b/frontend/templates/addCards.html deleted file mode 100644 index 45079fa..0000000 --- a/frontend/templates/addCards.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - Add Card - - - -

Add Card

- {{if .Error}} -
{{.Error}}
- {{end}} - {{if .Success}} -
{{.Success}}
- {{end}} -
-
- - -
-
- - -
-
-

The following fields will be - auto-generated:

-
- -

Auto-generated (16-digit)

-
-
- -

Default: Regular

-
-
- -

Auto-calculated (10 years from today)

-
-
- - - -
- - - \ No newline at end of file diff --git a/frontend/templates/admin/addCards.html b/frontend/templates/admin/addCards.html new file mode 100644 index 0000000..bd246c7 --- /dev/null +++ b/frontend/templates/admin/addCards.html @@ -0,0 +1,312 @@ + + + + + + + UniCard Admin - Add Card + + + + + + + + + +
+ + + + +
+ +
+

Admin Control Panel

+
+ Welcome, Administrator +
+ AD +
+
+
+ + +
+
+ +
+

Add New Card

+

Register a new RFID/NFC Tag into the UniCard inventory.

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

The internal RFID/NFC Tag ID scanner code.

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

Preloaded funds that will be credited to the card owner upon registration.

+
+ + +
+

Auto-Generated Parameters

+
+
+ Card Number + 16-Digit Unique +
+
+ Card Type + Regular +
+
+ Expiry Date + 10 Years From Today +
+
+
+ + +
+ + + Cancel + +
+
+
+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/templates/admin/admin_dashboard.html b/frontend/templates/admin/admin_dashboard.html new file mode 100644 index 0000000..eec2b64 --- /dev/null +++ b/frontend/templates/admin/admin_dashboard.html @@ -0,0 +1,739 @@ + + + + + + + UniCard Admin - Dashboard + + + + + + + + + + +
+ + + + +
+ +
+

Admin Control Panel

+
+ Welcome, Administrator +
+ AD +
+
+
+ + +
+
+ +
+

Card Inventory Dashboard

+

Monitor, create, and manage cards in the system inventory.

+
+ + +
+ +
+
+
+

Total Cards

+

0

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

Active

+

0

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

Inactive

+

0

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

Blocked

+

0

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

Lost

+

0

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

Physical Cards Inventory

+

View and manage all cards registered in the system database.

+
+ +
+ + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + +
Card NumberCardholderTypeExpiryPreloaded BalanceStatusActions
+ Loading inventory... +
+
+ + +
+
+ + +
+ +
+
+
+
+
+
+ + + + + + + + + + + + + + + diff --git a/frontend/templates/admin/deactivateCard.html b/frontend/templates/admin/deactivateCard.html new file mode 100644 index 0000000..f951040 --- /dev/null +++ b/frontend/templates/admin/deactivateCard.html @@ -0,0 +1,343 @@ + + + + + + + UniCard Admin - Deactivate Card + + + + + + + + + +
+ + + + +
+ +
+

Admin Control Panel

+
+ Welcome, Administrator +
+ AD +
+
+
+ + +
+
+ +
+

Deactivate Card

+

Block or suspend an active card in the system.

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

The 16-digit number printed on the physical card.

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

The full name of the card owner associated with this card.

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

Critical Action Warning

+

+ Deactivating a card will immediately transition its status to Blocked. + This action is irreversible. The card owner will not be able to use this card for transport or retail transactions. +

+
+
+ + +
+ + + Cancel + +
+
+
+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/frontend/templates/admin/hardware_registry.html b/frontend/templates/admin/hardware_registry.html new file mode 100644 index 0000000..84dbf3e --- /dev/null +++ b/frontend/templates/admin/hardware_registry.html @@ -0,0 +1,201 @@ + + + + + + Super Admin - Terminal Registry + + + + + + + + +
+ + + + +
+
+

Terminal Registry

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

Hardware Inventory

+

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

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

Merchant Management

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

Client Businesses

+

View and manage all registered merchants operating on the platform.

+
+ +
+ +
+
+
+ + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Merchant InfoCategoryVolume (MTD)StatusActions
+
+
+ +
+
+
Alfamart Phils.
+
ID: MER-882194
+
+
+
Retail / Convenience₱ 4.2M + Active + + + +
+
+
+ +
+
+
Local Brew Cafe
+
ID: MER-993847
+
+
+
Food & Beverage₱ 0.00 + Pending + + + +
+
+
+
+
+
+
+ + diff --git a/frontend/templates/admin/platform_overview.html b/frontend/templates/admin/platform_overview.html new file mode 100644 index 0000000..6d55abd --- /dev/null +++ b/frontend/templates/admin/platform_overview.html @@ -0,0 +1,211 @@ + + + + + + Super Admin - Platform Overview + + + + + + + + +
+ + + + +
+ +
+

Platform Overview

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

Global Command Center

+

Real-time metrics for UniCard system-wide performance and revenue.

+
+ + +
+ +
+
+
+

Gross Transaction Vol.

+

₱42.8M

+

12.5%

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

Total Active Users

+

128.4K

+

3.2%

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

Net Gateway Fees

+

₱1.24M

+

8.1%

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

Recent System Events

+
    +
  • +
    +
    +

    Main Gateway API Updated

    +

    v2.4.1 deployed successfully without downtime.

    +
    +
  • +
  • +
    +
    +

    New Merchant Onboarded

    +

    Alfamart Phils. activated 12 new terminals.

    +
    +
  • +
+
+
+

Active Regions

+
+ 4 +
+
+
Metro Manila
+
Cebu
+
Others
+
+
+
+
+
+
+
+ + diff --git a/frontend/templates/admin/system_settings.html b/frontend/templates/admin/system_settings.html new file mode 100644 index 0000000..8dac720 --- /dev/null +++ b/frontend/templates/admin/system_settings.html @@ -0,0 +1,162 @@ + + + + + + Super Admin - System Settings + + + + + + + + + +
+ + + + +
+
+

System Settings

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

Core Application Rules

+

Modify global application limits, fees, and gateway APIs.

+
+ +
+

Global Fees & Limits

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

Transaction APIs

+
+
+ + +
+
+ + +
+
+
+ +
+ + +
+
+
+
+
+ + diff --git a/frontend/templates/card.html b/frontend/templates/customer/card.html similarity index 90% rename from frontend/templates/card.html rename to frontend/templates/customer/card.html index b52988b..7ae7e90 100644 --- a/frontend/templates/card.html +++ b/frontend/templates/customer/card.html @@ -65,13 +65,11 @@
- - - +
@@ -144,6 +142,23 @@ class="ml-3 group-hover:text-blue-500 transition duration-300 cursor-pointer">Profile + +
  • + + + Top Up + +
  • +
    + +
  • + + + Settings + +
  • @@ -199,8 +214,8 @@

    Card Preview

    -

    - **** **** **9831 +

    + **** **** **** ****

    @@ -208,12 +223,12 @@

    Card Preview

    Cardholder Name

    -

    JUAN DELA CRUZ

    +

    CARDHOLDER

    Expires End

    -

    12/30

    +

    MM/YY

    @@ -226,8 +241,10 @@

    Card Details

    -
    Card ID
    -
    **** **** 9831
    +
    Card Number
    +
    +
    +
    Status
    @@ -241,11 +258,15 @@

    Card Details

    Account Type
    -
    PWD (20% Discount)
    +
    +
    +
    Expiry Date
    -
    December 31, 2030
    +
    +
    +
    @@ -392,8 +413,7 @@

    Request Replacement

    - - + \ No newline at end of file diff --git a/frontend/templates/dashboard.html b/frontend/templates/customer/dashboard.html similarity index 50% rename from frontend/templates/dashboard.html rename to frontend/templates/customer/dashboard.html index 2334095..b994874 100644 --- a/frontend/templates/dashboard.html +++ b/frontend/templates/customer/dashboard.html @@ -91,6 +91,23 @@ class="ml-3 group-hove r:text-blue-500 transition duration-300 cursor-pointer">Profile + +
  • + + + Top Up + +
  • +
    + +
  • + + + Settings + +
  • @@ -114,13 +131,13 @@
    - +
    @@ -153,145 +170,82 @@

    Dashboard

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

    Current Balance

    -

    ₱139.60

    -
    + +
    + + +
    +
    + +
    +
    +

    Available Balance

    +

    0.00

    - -
    -
    -
    - - - - -
    -
    -

    Loyalty Points (Cashback)

    -

    ₱12.85

    -
    + +
    +
    + +
    +
    +

    Loyalty Points
    (Cashback)

    +

    0.00

    - -
    -
    -
    - - - -
    -
    -

    Account Type

    -

    PWD

    -
    + +
    +
    + +
    +
    +

    Account Type

    +

    Loading...

    -
    +
    - -
    -
    -

    Recent Transactions

    - - See all - - -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - Date - - Description - - Type - - Amount -
    - Nov 1, 2025 - - Retail - Main St. Coffee - - Payment - - -₱100.00 -
    - Nov 1, 2025 - - Transport - Route 42 - - Payment - - -₱10.40 -
    - Oct 31, 2025 - - Top-up via GCash - - Top-up - - +₱250.00 -
    + +
    +
    +
    +

    Recent Transactions

    + + See all + + +
    +
    + + + + + + + + + + + + + + +
    + Date + + Description + + Type + + Amount +
    + Loading transactions... +
    +
    -
    +
    @@ -300,7 +254,33 @@

    Recent Transactions

    - Logout + + + + + diff --git a/frontend/templates/forgot-password.html b/frontend/templates/customer/forgot-password.html similarity index 69% rename from frontend/templates/forgot-password.html rename to frontend/templates/customer/forgot-password.html index 777ff5a..526afad 100644 --- a/frontend/templates/forgot-password.html +++ b/frontend/templates/customer/forgot-password.html @@ -128,7 +128,6 @@

    Forgot Passwor @@ -155,7 +163,16 @@

    Forgot Passwor + class="w-full px-4 py-3 pl-10 pr-10 border border-gray-300 rounded-lg shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-150 ease-in-out" /> + @@ -217,6 +234,49 @@

    Forgot Passwor + diff --git a/frontend/templates/login.html b/frontend/templates/customer/login.html similarity index 69% rename from frontend/templates/login.html rename to frontend/templates/customer/login.html index f67b3c1..d79230e 100644 --- a/frontend/templates/login.html +++ b/frontend/templates/customer/login.html @@ -72,7 +72,16 @@

    UniCard

    + class="w-full px-4 py-3 pl-10 pr-10 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-150 ease-in-out" /> + @@ -116,6 +125,28 @@

    UniCard

    + \ No newline at end of file diff --git a/frontend/templates/profile.html b/frontend/templates/customer/profile.html similarity index 88% rename from frontend/templates/profile.html rename to frontend/templates/customer/profile.html index ad56539..b9d3050 100644 --- a/frontend/templates/profile.html +++ b/frontend/templates/customer/profile.html @@ -62,13 +62,12 @@ -
    - +
    @@ -374,8 +398,8 @@

    Delete Account

    - - + + \ No newline at end of file diff --git a/frontend/templates/settings.html b/frontend/templates/customer/settings.html similarity index 98% rename from frontend/templates/settings.html rename to frontend/templates/customer/settings.html index 9b312df..58fe9a1 100644 --- a/frontend/templates/settings.html +++ b/frontend/templates/customer/settings.html @@ -47,11 +47,11 @@
    - +
    - - + \ No newline at end of file diff --git a/frontend/templates/signup.html b/frontend/templates/customer/signup.html similarity index 75% rename from frontend/templates/signup.html rename to frontend/templates/customer/signup.html index f3f4ac6..c53fb6d 100644 --- a/frontend/templates/signup.html +++ b/frontend/templates/customer/signup.html @@ -146,7 +146,16 @@

    Create your account

    + class="w-full px-4 py-3 pl-10 pr-10 border border-gray-300 rounded-lg shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-150 ease-in-out" /> +
    @@ -161,7 +170,16 @@

    Create your account

    + class="w-full px-4 py-3 pl-10 pr-10 border border-gray-300 rounded-lg shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-150 ease-in-out" /> + @@ -242,6 +260,49 @@

    Account Created!

    + \ No newline at end of file diff --git a/frontend/templates/topup.html b/frontend/templates/customer/topup.html similarity index 97% rename from frontend/templates/topup.html rename to frontend/templates/customer/topup.html index 26f876a..09c8601 100644 --- a/frontend/templates/topup.html +++ b/frontend/templates/customer/topup.html @@ -47,11 +47,11 @@
    - +
    @@ -351,8 +355,8 @@

    Confirm Logout

    - - + + \ No newline at end of file diff --git a/frontend/templates/transaction.html b/frontend/templates/customer/transaction.html similarity index 85% rename from frontend/templates/transaction.html rename to frontend/templates/customer/transaction.html index c121f89..f4c494d 100644 --- a/frontend/templates/transaction.html +++ b/frontend/templates/customer/transaction.html @@ -90,6 +90,23 @@ class="ml-3 group-hover:text-blue-500 transition duration-300 cursor-pointer">Profile + +
  • + + + Top Up + +
  • +
    + +
  • + + + Settings + +
  • @@ -112,14 +129,14 @@
    - +
    @@ -212,42 +229,29 @@

    Transaction History

    - - + + - Nov 1, 2025, 4:15 - PM - Retail - - Main St. Coffee - Payment - -₱100.00 - - ₱39.60 - +
    +
    +
    +
    +
    - Nov 1, 2025, 3:35 - PM - - Transport - Route 42 - Payment - -₱10.40 - - ₱139.60 - +
    +
    +
    +
    +
    - Oct 31, 2025, 9:00 - AM - Top-up - via GCash - Top-up - - +₱250.00 - ₱150.00 - +
    +
    +
    +
    +
    - @@ -323,7 +327,8 @@

    Confirm Logout

    - + + \ No newline at end of file diff --git a/frontend/templates/deactivateCard.html b/frontend/templates/deactivateCard.html deleted file mode 100644 index 7e69025..0000000 --- a/frontend/templates/deactivateCard.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - Deactivate Card - - - -

    Deactivate Card

    - {{if .Error}} -
    {{.Error}}
    - {{end}} - {{if .Success}} -
    {{.Success}}
    - {{end}} -
    -
    - - -
    -
    - - -
    -
    - - -
    - - - -
    - - - \ No newline at end of file