Compare commits
3 Commits
c0143df8c0
...
c3a7480c65
| Author | SHA1 | Date | |
|---|---|---|---|
| c3a7480c65 | |||
| b466c351da | |||
| f8dab97a54 |
68
handlers/admin/dashboard.go
Normal file
68
handlers/admin/dashboard.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
helpers "synlotto-website/helpers"
|
||||
"synlotto-website/models"
|
||||
)
|
||||
|
||||
func AdminDashboardHandler(db *sql.DB) http.HandlerFunc {
|
||||
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
// userID, ok := helpers.GetCurrentUserID(r)
|
||||
// if !ok {
|
||||
// http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
// return
|
||||
// }
|
||||
|
||||
// TODO: check is_admin from users table here
|
||||
|
||||
context := helpers.TemplateContext(w, r)
|
||||
|
||||
// Total ticket stats
|
||||
var total, winners int
|
||||
var prizeSum float64
|
||||
db.QueryRow(`SELECT COUNT(*), SUM(CASE WHEN is_winner THEN 1 ELSE 0 END), SUM(prize_amount) FROM my_tickets`).Scan(&total, &winners, &prizeSum)
|
||||
context["Stats"] = map[string]interface{}{
|
||||
"TotalTickets": total,
|
||||
"TotalWinners": winners,
|
||||
"TotalPrizeAmount": prizeSum,
|
||||
}
|
||||
|
||||
// Match run log
|
||||
rows, err := db.Query(`
|
||||
SELECT run_at, triggered_by, tickets_matched, winners_found, COALESCE(notes, '')
|
||||
FROM log_ticket_matching
|
||||
ORDER BY run_at DESC LIMIT 10
|
||||
`)
|
||||
if err != nil {
|
||||
log.Println("⚠️ Failed to load logs:", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var logs []models.MatchLog
|
||||
for rows.Next() {
|
||||
var logEntry models.MatchLog
|
||||
err := rows.Scan(&logEntry.RunAt, &logEntry.TriggeredBy, &logEntry.TicketsMatched, &logEntry.WinnersFound, &logEntry.Notes)
|
||||
if err != nil {
|
||||
log.Println("⚠️ Failed to scan log row:", err)
|
||||
continue
|
||||
}
|
||||
logs = append(logs, logEntry)
|
||||
}
|
||||
context["MatchLogs"] = logs
|
||||
|
||||
tmpl := template.Must(template.New("").Funcs(helpers.TemplateFuncs()).ParseFiles(
|
||||
"templates/layout.html",
|
||||
"templates/admin/dashboard.html",
|
||||
))
|
||||
|
||||
err = tmpl.ExecuteTemplate(w, "layout", context)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to render dashboard", http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
72
handlers/admin/draws.go
Normal file
72
handlers/admin/draws.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
helpers "synlotto-website/helpers"
|
||||
)
|
||||
|
||||
func NewDrawHandler(db *sql.DB) http.HandlerFunc {
|
||||
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
context := helpers.TemplateContext(w, r)
|
||||
|
||||
if r.Method == http.MethodPost {
|
||||
game := r.FormValue("game_type")
|
||||
date := r.FormValue("draw_date")
|
||||
machine := r.FormValue("machine")
|
||||
ballset := r.FormValue("ball_set")
|
||||
|
||||
_, err := db.Exec(`INSERT INTO results_thunderball (game_type, draw_date, machine, ball_set) VALUES (?, ?, ?, ?)`,
|
||||
game, date, machine, ballset)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to add draw", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/admin/dashboard", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl := template.Must(template.New("new_draw").Funcs(helpers.TemplateFuncs()).ParseFiles(
|
||||
"templates/layout.html",
|
||||
"templates/admin/draws/new_draw.html",
|
||||
))
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
})
|
||||
}
|
||||
|
||||
func ModifyDrawHandler(db *sql.DB) http.HandlerFunc {
|
||||
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
id := r.FormValue("id")
|
||||
_, err := db.Exec(`UPDATE results_thunderball SET game_type=?, draw_date=?, ball_set=?, machine=? WHERE id=?`,
|
||||
r.FormValue("game_type"), r.FormValue("draw_date"), r.FormValue("ball_set"), r.FormValue("machine"), id)
|
||||
if err != nil {
|
||||
http.Error(w, "Update failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/dashboard", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
// For GET: load draw by ID (pseudo-code)
|
||||
// id := r.URL.Query().Get("id")
|
||||
// query DB, pass into context.Draw
|
||||
})
|
||||
}
|
||||
|
||||
func DeleteDrawHandler(db *sql.DB) http.HandlerFunc {
|
||||
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
id := r.FormValue("id")
|
||||
_, err := db.Exec(`DELETE FROM results_thunderball WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
http.Error(w, "Delete failed", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/dashboard", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -43,6 +43,14 @@ func AdminTriggersHandler(db *sql.DB) http.HandlerFunc {
|
||||
}
|
||||
flashMsg = "✅ Missing prizes updated."
|
||||
|
||||
case "refresh_prizes":
|
||||
err := services.RefreshTicketPrizes(db)
|
||||
if err != nil {
|
||||
http.Error(w, "Refresh failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
flashMsg = "✅ Ticket prizes refreshed."
|
||||
|
||||
case "run_all":
|
||||
stats, err := services.RunTicketMatching(db, "manual")
|
||||
if err != nil {
|
||||
|
||||
70
handlers/admin/prizes.go
Normal file
70
handlers/admin/prizes.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"synlotto-website/helpers"
|
||||
)
|
||||
|
||||
func AddPrizesHandler(db *sql.DB) http.HandlerFunc {
|
||||
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
tmpl := template.Must(template.New("").Funcs(helpers.TemplateFuncs()).ParseFiles(
|
||||
"templates/layout.html",
|
||||
"templates/admin/draws/prizes/add_prizes.html",
|
||||
))
|
||||
tmpl.ExecuteTemplate(w, "layout", helpers.TemplateContext(w, r))
|
||||
return
|
||||
}
|
||||
|
||||
drawDate := r.FormValue("draw_date")
|
||||
values := make([]interface{}, 0)
|
||||
for i := 1; i <= 9; i++ {
|
||||
val, _ := strconv.Atoi(r.FormValue(fmt.Sprintf("prize%d_per_winner", i)))
|
||||
values = append(values, val)
|
||||
}
|
||||
|
||||
stmt := `INSERT INTO prizes_thunderball (
|
||||
draw_date, prize1_per_winner, prize2_per_winner, prize3_per_winner,
|
||||
prize4_per_winner, prize5_per_winner, prize6_per_winner,
|
||||
prize7_per_winner, prize8_per_winner, prize9_per_winner
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
|
||||
_, err := db.Exec(stmt, append([]interface{}{drawDate}, values...)...)
|
||||
if err != nil {
|
||||
http.Error(w, "Insert failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/admin/draws", http.StatusSeeOther)
|
||||
})
|
||||
}
|
||||
|
||||
func ModifyPrizesHandler(db *sql.DB) http.HandlerFunc {
|
||||
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
tmpl := template.Must(template.New("").Funcs(helpers.TemplateFuncs()).ParseFiles(
|
||||
"templates/layout.html",
|
||||
"templates/admin/draws/prizes/modify_prizes.html",
|
||||
))
|
||||
tmpl.ExecuteTemplate(w, "layout", helpers.TemplateContext(w, r))
|
||||
return
|
||||
}
|
||||
|
||||
drawDate := r.FormValue("draw_date")
|
||||
for i := 1; i <= 9; i++ {
|
||||
key := fmt.Sprintf("prize%d_per_winner", i)
|
||||
val, _ := strconv.Atoi(r.FormValue(key))
|
||||
_, err := db.Exec("UPDATE prizes_thunderball SET "+key+" = ? WHERE draw_date = ?", val, drawDate)
|
||||
if err != nil {
|
||||
http.Error(w, "Update failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/admin/draws", http.StatusSeeOther)
|
||||
})
|
||||
}
|
||||
@@ -107,3 +107,41 @@ func Submit(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func ListDrawsHandler(db *sql.DB) http.HandlerFunc {
|
||||
return helpers.AuthMiddleware(func(w http.ResponseWriter, r *http.Request) {
|
||||
context := helpers.TemplateContext(w, r)
|
||||
draws := []models.DrawSummary{}
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT r.id, r.game_type, r.draw_date, r.ball_set, r.machine,
|
||||
(SELECT COUNT(1) FROM prizes_thunderball p WHERE p.draw_date = r.draw_date) as prize_exists
|
||||
FROM results_thunderball r
|
||||
ORDER BY r.draw_date DESC
|
||||
`)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to query draws", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var d models.DrawSummary
|
||||
var prizeFlag int
|
||||
if err := rows.Scan(&d.Id, &d.GameType, &d.DrawDate, &d.BallSet, &d.Machine, &prizeFlag); err != nil {
|
||||
log.Println("⚠️ Draw scan failed:", err)
|
||||
continue
|
||||
}
|
||||
d.PrizeSet = prizeFlag > 0
|
||||
draws = append(draws, d)
|
||||
}
|
||||
|
||||
context["Draws"] = draws
|
||||
|
||||
tmpl := template.Must(template.New("draw_list").Funcs(helpers.TemplateFuncs()).ParseFiles(
|
||||
"templates/layout.html",
|
||||
"templates/admin/draws/list.html",
|
||||
))
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@ func GetMyTickets(db *sql.DB) http.HandlerFunc {
|
||||
ball1, ball2, ball3, ball4, ball5, ball6,
|
||||
bonus1, bonus2,
|
||||
purchase_method, purchase_date, image_path, duplicate,
|
||||
matched_main, matched_bonus, prize_tier, is_winner
|
||||
matched_main, matched_bonus, prize_tier, is_winner, prize_label, prize_amount
|
||||
FROM my_tickets
|
||||
WHERE userid = ?
|
||||
ORDER BY draw_date DESC, created_at DESC
|
||||
@@ -300,13 +300,15 @@ func GetMyTickets(db *sql.DB) http.HandlerFunc {
|
||||
var matchedMain, matchedBonus sql.NullInt64
|
||||
var prizeTier sql.NullString
|
||||
var isWinner sql.NullBool
|
||||
var prizeLabel sql.NullString
|
||||
var prizeAmount sql.NullFloat64
|
||||
|
||||
err := rows.Scan(
|
||||
&t.Id, &t.GameType, &t.DrawDate,
|
||||
&b1, &b2, &b3, &b4, &b5, &b6,
|
||||
&bo1, &bo2,
|
||||
&t.PurchaseMethod, &t.PurchaseDate, &t.ImagePath, &t.Duplicate,
|
||||
&matchedMain, &matchedBonus, &prizeTier, &isWinner,
|
||||
&matchedMain, &matchedBonus, &prizeTier, &isWinner, &prizeLabel, &prizeAmount,
|
||||
)
|
||||
if err != nil {
|
||||
log.Println("⚠️ Failed to scan ticket row:", err)
|
||||
@@ -335,7 +337,12 @@ func GetMyTickets(db *sql.DB) http.HandlerFunc {
|
||||
if isWinner.Valid {
|
||||
t.IsWinner = isWinner.Bool
|
||||
}
|
||||
|
||||
if prizeLabel.Valid {
|
||||
t.PrizeLabel = prizeLabel.String
|
||||
}
|
||||
if prizeAmount.Valid {
|
||||
t.PrizeAmount = prizeAmount.Float64
|
||||
}
|
||||
// Build balls slices (for template use)
|
||||
t.Balls = helpers.BuildBallsSlice(t)
|
||||
t.BonusBalls = helpers.BuildBonusSlice(t)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package helpers
|
||||
|
||||
import "synlotto-website/models"
|
||||
import (
|
||||
"database/sql"
|
||||
"synlotto-website/models"
|
||||
)
|
||||
|
||||
func BuildBallsSlice(t models.Ticket) []int {
|
||||
balls := []int{t.Ball1, t.Ball2, t.Ball3, t.Ball4, t.Ball5}
|
||||
@@ -22,3 +25,26 @@ func BuildBonusSlice(t models.Ticket) []int {
|
||||
|
||||
return bonuses
|
||||
}
|
||||
|
||||
// BuildBallsFromNulls builds main balls from sql.NullInt64 values
|
||||
func BuildBallsFromNulls(vals ...sql.NullInt64) []int {
|
||||
var result []int
|
||||
for _, v := range vals {
|
||||
if v.Valid {
|
||||
result = append(result, int(v.Int64))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// BuildBonusFromNulls builds bonus balls from two sql.NullInt64 values
|
||||
func BuildBonusFromNulls(b1, b2 sql.NullInt64) []int {
|
||||
var result []int
|
||||
if b1.Valid {
|
||||
result = append(result, int(b1.Int64))
|
||||
}
|
||||
if b2.Valid {
|
||||
result = append(result, int(b2.Int64))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
40
main.go
40
main.go
@@ -1,10 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"log"
|
||||
"net/http"
|
||||
"synlotto-website/handlers"
|
||||
services "synlotto-website/handlers/admin"
|
||||
admin "synlotto-website/handlers/admin"
|
||||
"synlotto-website/helpers"
|
||||
"synlotto-website/middleware"
|
||||
"synlotto-website/models"
|
||||
@@ -24,27 +25,40 @@ func main() {
|
||||
)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
setupAdminRoutes(mux, db)
|
||||
setupAccountRoutes(mux, db)
|
||||
setupResultRoutes(mux, db)
|
||||
|
||||
// Styling
|
||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
||||
|
||||
mux.HandleFunc("/", handlers.Home(db))
|
||||
mux.HandleFunc("/new", handlers.NewDraw) // ToDo: needs to be wrapped in admin auth
|
||||
mux.HandleFunc("/submit", handlers.Submit)
|
||||
|
||||
// Result pages
|
||||
mux.HandleFunc("/results/thunderball", handlers.ResultsThunderball(db))
|
||||
log.Println("🌐 Running on http://localhost:8080")
|
||||
http.ListenAndServe(":8080", helpers.RateLimit(csrfMiddleware(mux)))
|
||||
}
|
||||
|
||||
// Account Pages
|
||||
func setupAdminRoutes(mux *http.ServeMux, db *sql.DB) {
|
||||
mux.HandleFunc("/admin/dashboard", admin.AdminDashboardHandler(db))
|
||||
mux.HandleFunc("/admin/triggers", admin.AdminTriggersHandler(db))
|
||||
|
||||
// Draw management
|
||||
mux.HandleFunc("/admin/draws/new", admin.NewDrawHandler(db))
|
||||
mux.HandleFunc("/admin/draws/modify", admin.ModifyDrawHandler(db))
|
||||
mux.HandleFunc("/admin/draws/delete", admin.DeleteDrawHandler(db))
|
||||
|
||||
// Prize management
|
||||
mux.HandleFunc("/admin/draws/prizes/add", admin.AddPrizesHandler(db))
|
||||
mux.HandleFunc("/admin/draws/prizes/modify", admin.ModifyPrizesHandler(db))
|
||||
}
|
||||
|
||||
func setupAccountRoutes(mux *http.ServeMux, db *sql.DB) {
|
||||
mux.HandleFunc("/login", middleware.Auth(false)(handlers.Login))
|
||||
mux.HandleFunc("/logout", handlers.Logout)
|
||||
mux.HandleFunc("/signup", middleware.Auth(false)(handlers.Signup))
|
||||
mux.HandleFunc("/account/tickets/add_ticket", handlers.AddTicket(db))
|
||||
mux.HandleFunc("/account/tickets/my_tickets", handlers.GetMyTickets(db))
|
||||
|
||||
// Admin Pages
|
||||
mux.HandleFunc("/admin/triggers", services.AdminTriggersHandler(db))
|
||||
|
||||
log.Println("🌐 Running on http://localhost:8080")
|
||||
http.ListenAndServe(":8080", helpers.RateLimit(csrfMiddleware(mux)))
|
||||
}
|
||||
|
||||
func setupResultRoutes(mux *http.ServeMux, db *sql.DB) {
|
||||
mux.HandleFunc("/results/thunderball", handlers.ResultsThunderball(db))
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ func MatchTicketToDraw(ticket models.MatchTicket, draw models.DrawResult, rules
|
||||
mainMatches := helpers.CountMatches(ticket.Balls, draw.Balls)
|
||||
bonusMatches := helpers.CountMatches(ticket.BonusBalls, draw.BonusBalls)
|
||||
|
||||
prizeTier := getPrizeTier(ticket.GameType, mainMatches, bonusMatches, rules)
|
||||
prizeTier := GetPrizeTier(ticket.GameType, mainMatches, bonusMatches, rules)
|
||||
isWinner := prizeTier != ""
|
||||
|
||||
result := models.MatchResult{
|
||||
@@ -43,7 +43,7 @@ func MatchTicketToDraw(ticket models.MatchTicket, draw models.DrawResult, rules
|
||||
return result
|
||||
}
|
||||
|
||||
func getPrizeTier(game string, main, bonus int, rules []models.PrizeRule) string {
|
||||
func GetPrizeTier(game string, main, bonus int, rules []models.PrizeRule) string {
|
||||
for _, rule := range rules {
|
||||
if rule.Game == game && rule.MainMatches == main && rule.BonusMatches == bonus {
|
||||
return rule.Tier
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
package models
|
||||
|
||||
type DrawSummary struct {
|
||||
Id int
|
||||
GameType string
|
||||
DrawDate string
|
||||
BallSet string
|
||||
Machine string
|
||||
PrizeSet bool
|
||||
}
|
||||
|
||||
type ThunderballResult struct {
|
||||
Id int
|
||||
DrawDate string
|
||||
|
||||
@@ -38,3 +38,12 @@ type MatchRunStats struct {
|
||||
TicketsMatched int
|
||||
WinnersFound int
|
||||
}
|
||||
|
||||
type MatchLog struct {
|
||||
ID int
|
||||
TriggeredBy string
|
||||
RunAt string
|
||||
TicketsMatched int
|
||||
WinnersFound int
|
||||
Notes string
|
||||
}
|
||||
|
||||
@@ -22,23 +22,23 @@ var ThunderballPrizeRules = []models.PrizeRule{
|
||||
|
||||
func GetThunderballPrizeIndex(main, bonus int) (int, bool) {
|
||||
switch {
|
||||
case main == 5 && bonus == 1:
|
||||
case main == 0 && bonus == 1:
|
||||
return 9, true
|
||||
case main == 5 && bonus == 0:
|
||||
case main == 1 && bonus == 1:
|
||||
return 8, true
|
||||
case main == 4 && bonus == 1:
|
||||
case main == 2 && bonus == 1:
|
||||
return 7, true
|
||||
case main == 4 && bonus == 0:
|
||||
case main == 3 && bonus == 0:
|
||||
return 6, true
|
||||
case main == 3 && bonus == 1:
|
||||
return 5, true
|
||||
case main == 3 && bonus == 0:
|
||||
case main == 4 && bonus == 0:
|
||||
return 4, true
|
||||
case main == 2 && bonus == 1:
|
||||
case main == 4 && bonus == 1:
|
||||
return 3, true
|
||||
case main == 1 && bonus == 1:
|
||||
case main == 5 && bonus == 0:
|
||||
return 2, true
|
||||
case main == 0 && bonus == 1:
|
||||
case main == 5 && bonus == 1:
|
||||
return 1, true
|
||||
default:
|
||||
return 0, false
|
||||
|
||||
@@ -6,9 +6,10 @@ import (
|
||||
"log"
|
||||
"synlotto-website/handlers"
|
||||
"synlotto-website/helpers"
|
||||
"synlotto-website/matcher"
|
||||
"synlotto-website/models"
|
||||
"synlotto-website/rules"
|
||||
draws "synlotto-website/services/draws"
|
||||
thunderballrules "synlotto-website/rules"
|
||||
services "synlotto-website/services/draws"
|
||||
)
|
||||
|
||||
func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, error) {
|
||||
@@ -62,8 +63,8 @@ func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, er
|
||||
BonusBalls: helpers.BuildBonusSlice(t),
|
||||
}
|
||||
|
||||
draw := draws.GetDrawResultForTicket(db, t.GameType, t.DrawDate)
|
||||
result := handlers.MatchTicketToDraw(matchTicket, draw, rules.ThunderballPrizeRules)
|
||||
draw := services.GetDrawResultForTicket(db, t.GameType, t.DrawDate)
|
||||
result := handlers.MatchTicketToDraw(matchTicket, draw, thunderballrules.ThunderballPrizeRules)
|
||||
|
||||
if result.MatchedDrawID == 0 {
|
||||
continue
|
||||
@@ -130,7 +131,7 @@ func UpdateMissingPrizes(db *sql.DB) error {
|
||||
continue
|
||||
}
|
||||
|
||||
idx, ok := rules.GetThunderballPrizeIndex(t.Main, t.Bonus)
|
||||
idx, ok := thunderballrules.GetThunderballPrizeIndex(t.Main, t.Bonus)
|
||||
if !ok {
|
||||
log.Printf("❌ No index for %d main, %d bonus", t.Main, t.Bonus)
|
||||
continue
|
||||
@@ -163,3 +164,96 @@ func UpdateMissingPrizes(db *sql.DB) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func RefreshTicketPrizes(db *sql.DB) error {
|
||||
type TicketRow struct {
|
||||
ID int
|
||||
GameType string
|
||||
DrawDate string
|
||||
B1, B2, B3, B4, B5, B6 sql.NullInt64
|
||||
Bonus1, Bonus2 sql.NullInt64
|
||||
}
|
||||
|
||||
var tickets []TicketRow
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT id, game_type, draw_date,
|
||||
ball1, ball2, ball3, ball4, ball5, ball6,
|
||||
bonus1, bonus2
|
||||
FROM my_tickets
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var t TicketRow
|
||||
if err := rows.Scan(&t.ID, &t.GameType, &t.DrawDate,
|
||||
&t.B1, &t.B2, &t.B3, &t.B4, &t.B5, &t.B6, &t.Bonus1, &t.Bonus2); err != nil {
|
||||
log.Println("⚠️ Failed to scan ticket:", err)
|
||||
continue
|
||||
}
|
||||
tickets = append(tickets, t)
|
||||
}
|
||||
rows.Close() // ✅ Release read lock before updating
|
||||
|
||||
for _, row := range tickets {
|
||||
matchTicket := models.MatchTicket{
|
||||
GameType: row.GameType,
|
||||
DrawDate: row.DrawDate,
|
||||
Balls: helpers.BuildBallsFromNulls(row.B1, row.B2, row.B3, row.B4, row.B5, row.B6),
|
||||
BonusBalls: helpers.BuildBonusFromNulls(row.Bonus1, row.Bonus2),
|
||||
}
|
||||
|
||||
draw := services.GetDrawResultForTicket(db, row.GameType, row.DrawDate)
|
||||
if draw.DrawID == 0 {
|
||||
log.Printf("❌ No draw result for %s (%s)", row.DrawDate, row.GameType)
|
||||
continue
|
||||
}
|
||||
|
||||
mainMatches := helpers.CountMatches(matchTicket.Balls, draw.Balls)
|
||||
bonusMatches := helpers.CountMatches(matchTicket.BonusBalls, draw.BonusBalls)
|
||||
prizeTier := matcher.GetPrizeTier(row.GameType, mainMatches, bonusMatches, thunderballrules.ThunderballPrizeRules)
|
||||
isWinner := prizeTier != ""
|
||||
|
||||
var label string
|
||||
var amount float64
|
||||
if row.GameType == "Thunderball" {
|
||||
idx, ok := thunderballrules.GetThunderballPrizeIndex(mainMatches, bonusMatches)
|
||||
if ok {
|
||||
query := fmt.Sprintf(`SELECT prize%d_per_winner FROM prizes_thunderball WHERE draw_date = ?`, idx)
|
||||
var val int
|
||||
err := db.QueryRow(query, row.DrawDate).Scan(&val)
|
||||
if err == nil {
|
||||
amount = float64(val)
|
||||
if val > 0 {
|
||||
label = fmt.Sprintf("£%.2f", amount)
|
||||
} else {
|
||||
label = "Free Ticket"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("🧪 Ticket %d → Matches: %d+%d, Tier: %s, Winner: %v, Label: %s, Amount: %.2f",
|
||||
row.ID, mainMatches, bonusMatches, prizeTier, isWinner, label, amount)
|
||||
|
||||
res, err := db.Exec(`
|
||||
UPDATE my_tickets
|
||||
SET matched_main = ?, matched_bonus = ?, prize_tier = ?, is_winner = ?, prize_amount = ?, prize_label = ?
|
||||
WHERE id = ?
|
||||
`, mainMatches, bonusMatches, prizeTier, isWinner, amount, label, row.ID)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("❌ Failed to update ticket %d: %v", row.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
rowsAffected, _ := res.RowsAffected()
|
||||
log.Printf("✅ Ticket %d updated — rows affected: %d | Tier: %s | Label: %s | Matches: %d+%d",
|
||||
row.ID, rowsAffected, prizeTier, label, mainMatches, bonusMatches)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
body { font-family: Arial, sans-serif; margin: 40px; }
|
||||
table { border-collapse: collapse; width: 100%; margin-top: 20px; }
|
||||
th, td { padding: 8px 12px; border: 1px solid #ddd; text-align: center; }
|
||||
th { background-color: #f5f5f5; }
|
||||
.form-section { margin-bottom: 20px; }
|
||||
.topbar { margin-bottom: 20px; }
|
||||
.flash { padding: 10px; color: green; background: #e9ffe9; border: 1px solid #c3e6c3; margin-bottom: 15px; }
|
||||
|
||||
|
||||
|
||||
/* Ball Stuff */
|
||||
.ball {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -73,18 +73,16 @@
|
||||
</td>
|
||||
<td>
|
||||
{{ if $ticket.IsWinner }}
|
||||
{{ if $ticket.PrizeLabel }}
|
||||
{{ if eq $ticket.PrizeLabel "Free Ticket" }}
|
||||
🎟️ {{ $ticket.PrizeLabel }}
|
||||
{{ else }}
|
||||
💷 {{ $ticket.PrizeLabel }}
|
||||
{{ end }}
|
||||
{{ if eq $ticket.PrizeLabel "" }}
|
||||
<span class="text-yellow-500 italic">pending</span>
|
||||
{{ else if eq $ticket.PrizeLabel "Free Ticket" }}
|
||||
🎟️ {{ $ticket.PrizeLabel }}
|
||||
{{ else }}
|
||||
<span class="text-gray-400 italic">pending</span>
|
||||
💷 {{ $ticket.PrizeLabel }}
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
–
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
|
||||
49
templates/admin/dashboard.html
Normal file
49
templates/admin/dashboard.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{{ define "content" }}
|
||||
<h2>📊 Admin Dashboard</h2>
|
||||
<p class="text-sm text-gray-600">Welcome back, admin.</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 my-6">
|
||||
<div class="bg-white rounded-xl p-4 shadow">
|
||||
<h3 class="text-sm text-gray-500">Total Tickets</h3>
|
||||
<p class="text-2xl font-bold">{{ .Stats.TotalTickets }}</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-4 shadow">
|
||||
<h3 class="text-sm text-gray-500">Total Winners</h3>
|
||||
<p class="text-2xl font-bold text-green-600">{{ .Stats.TotalWinners }}</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-4 shadow">
|
||||
<h3 class="text-sm text-gray-500">Prize Fund Awarded</h3>
|
||||
<p class="text-2xl font-bold text-blue-600">£{{ printf "%.2f" .Stats.TotalPrizeAmount }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-6">
|
||||
<h3 class="text-lg font-semibold mb-2">Recent Ticket Matches</h3>
|
||||
<table class="w-full text-sm border">
|
||||
<thead>
|
||||
<tr class="bg-gray-100">
|
||||
<th class="px-2 py-1">Draw Date</th>
|
||||
<th class="px-2 py-1">Triggered By</th>
|
||||
<th class="px-2 py-1">Matched</th>
|
||||
<th class="px-2 py-1">Winners</th>
|
||||
<th class="px-2 py-1">Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .MatchLogs }}
|
||||
<tr class="border-t">
|
||||
<td class="px-2 py-1">{{ .RunAt }}</td>
|
||||
<td class="px-2 py-1">{{ .TriggeredBy }}</td>
|
||||
<td class="px-2 py-1">{{ .TicketsMatched }}</td>
|
||||
<td class="px-2 py-1">{{ .WinnersFound }}</td>
|
||||
<td class="px-2 py-1 text-xs text-gray-500">{{ .Notes }}</td>
|
||||
</tr>
|
||||
{{ else }}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-2 italic text-gray-400">No match history found</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ end }}
|
||||
9
templates/admin/draws/delete_draw.html
Normal file
9
templates/admin/draws/delete_draw.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{{ define "delete_draw" }}
|
||||
<h2 class="text-xl font-semibold mb-4">Delete Draw</h2>
|
||||
<form method="POST" action="/admin/draws/delete">
|
||||
{{ .CSRFField }}
|
||||
<input type="hidden" name="id" value="{{ .Draw.ID }}">
|
||||
<p>Are you sure you want to delete the draw on <strong>{{ .Draw.DrawDate }}</strong>?</p>
|
||||
<button class="btn bg-red-600 hover:bg-red-700">Delete</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
46
templates/admin/draws/list_draws.html
Normal file
46
templates/admin/draws/list_draws.html
Normal file
@@ -0,0 +1,46 @@
|
||||
{{ define "draw_list" }}
|
||||
<h2 class="text-xl font-bold mb-4">Draws Overview</h2>
|
||||
|
||||
<table class="w-full table-auto border">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Game</th>
|
||||
<th>Date</th>
|
||||
<th>Ball Set</th>
|
||||
<th>Machine</th>
|
||||
<th>Prizes?</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Draws }}
|
||||
<tr class="border-t">
|
||||
<td>{{ .ID }}</td>
|
||||
<td>{{ .GameType }}</td>
|
||||
<td>{{ .DrawDate }}</td>
|
||||
<td>{{ .BallSet }}</td>
|
||||
<td>{{ .Machine }}</td>
|
||||
<td>
|
||||
{{ if .PrizeSet }}
|
||||
✅
|
||||
{{ else }}
|
||||
❌
|
||||
{{ end }}
|
||||
</td>
|
||||
<td>
|
||||
<a href="/admin/draws/modify?id={{ .ID }}" class="text-blue-600">Edit</a> |
|
||||
<a href="/admin/draws/delete?id={{ .ID }}" class="text-red-600">Delete</a> |
|
||||
{{ if .PrizeSet }}
|
||||
<a href="/admin/draws/prizes/modify?draw_date={{ .DrawDate }}" class="text-green-600">Modify Prizes</a>
|
||||
{{ else }}
|
||||
<a href="/admin/draws/prizes/add?draw_date={{ .DrawDate }}" class="text-gray-600">Add Prizes</a>
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{ else }}
|
||||
<tr><td colspan="7" class="text-center text-gray-500">No draws available.</td></tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ end }}
|
||||
12
templates/admin/draws/modify_draw.html
Normal file
12
templates/admin/draws/modify_draw.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{{ define "modify_draw" }}
|
||||
<h2 class="text-xl font-semibold mb-4">Modify Draw</h2>
|
||||
<form method="POST" action="/admin/draws/modify">
|
||||
{{ .CSRFField }}
|
||||
<input type="hidden" name="id" value="{{ .Draw.ID }}">
|
||||
<label class="block">Game Type: <input name="game_type" class="input" value="{{ .Draw.GameType }}"></label>
|
||||
<label class="block">Draw Date: <input name="draw_date" type="date" class="input" value="{{ .Draw.DrawDate }}"></label>
|
||||
<label class="block">Ball Set: <input name="ball_set" class="input" value="{{ .Draw.BallSet }}"></label>
|
||||
<label class="block">Machine: <input name="machine" class="input" value="{{ .Draw.Machine }}"></label>
|
||||
<button class="btn">Update Draw</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
11
templates/admin/draws/new_draw.html
Normal file
11
templates/admin/draws/new_draw.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{{ define "new_draw" }}
|
||||
<h2 class="text-xl font-semibold mb-4">Add New Draw</h2>
|
||||
<form method="POST" action="/admin/draws/new">
|
||||
{{ .CSRFField }}
|
||||
<label class="block">Game Type: <input name="game_type" class="input"></label>
|
||||
<label class="block">Draw Date: <input name="draw_date" type="date" class="input"></label>
|
||||
<label class="block">Ball Set: <input name="ball_set" class="input"></label>
|
||||
<label class="block">Machine: <input name="machine" class="input"></label>
|
||||
<button class="btn">Create Draw</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
13
templates/admin/draws/prizes/add_prizes.html
Normal file
13
templates/admin/draws/prizes/add_prizes.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{{ define "add_prizes" }}
|
||||
<h2 class="text-xl font-semibold mb-4">Add Prize Breakdown</h2>
|
||||
<form method="POST" action="/admin/draws/prizes/add">
|
||||
{{ .CSRFField }}
|
||||
<input type="hidden" name="draw_date" value="{{ .DrawDate }}">
|
||||
{{ range $i, $ := .PrizeLabels }}
|
||||
<label class="block">{{ . }}
|
||||
<input name="prize{{ add $i 1 }}_per_winner" class="input">
|
||||
</label>
|
||||
{{ end }}
|
||||
<button class="btn">Save Prizes</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
13
templates/admin/draws/prizes/modify_prizes.html
Normal file
13
templates/admin/draws/prizes/modify_prizes.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{{ define "modify_prizes" }}
|
||||
<h2 class="text-xl font-semibold mb-4">Modify Prize Breakdown</h2>
|
||||
<form method="POST" action="/admin/draws/prizes/modify">
|
||||
{{ .CSRFField }}
|
||||
<input type="hidden" name="draw_date" value="{{ .DrawDate }}">
|
||||
{{ range $i, $ := .PrizeLabels }}
|
||||
<label class="block">{{ . }}
|
||||
<input name="prize{{ add $i 1 }}_per_winner" class="input" value="{{ index $.Prizes (print "prize" (add $i 1) "_per_winner") }}">
|
||||
</label>
|
||||
{{ end }}
|
||||
<button class="btn">Update Prizes</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
@@ -19,4 +19,11 @@
|
||||
<button>Run All</button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="/admin/triggers" class="mt-4">
|
||||
{{ .CSRFField }}
|
||||
<input type="hidden" name="action" value="refresh_prizes">
|
||||
<button class="bg-indigo-500 text-white px-4 py-2 rounded hover:bg-indigo-600">
|
||||
Refresh Ticket Prizes
|
||||
</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
@@ -6,15 +6,6 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SynLotto</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 40px; }
|
||||
table { border-collapse: collapse; width: 100%; margin-top: 20px; }
|
||||
th, td { padding: 8px 12px; border: 1px solid #ddd; text-align: center; }
|
||||
th { background-color: #f5f5f5; }
|
||||
.form-section { margin-bottom: 20px; }
|
||||
.topbar { margin-bottom: 20px; }
|
||||
.flash { padding: 10px; color: green; background: #e9ffe9; border: 1px solid #c3e6c3; margin-bottom: 15px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="topbar">
|
||||
@@ -24,6 +15,7 @@
|
||||
<p><a href="/login">Login</a></p>
|
||||
{{ end }}
|
||||
</div>
|
||||
<a href="/admin/dashboard" class="hover:underline">Dashboard</a>
|
||||
|
||||
{{ if .Flash }}
|
||||
<div class="flash">{{ .Flash }}</div>
|
||||
|
||||
Reference in New Issue
Block a user