Compare commits

...

3 Commits

24 changed files with 625 additions and 49 deletions

View 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
View 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
}
})
}

View File

@@ -43,6 +43,14 @@ func AdminTriggersHandler(db *sql.DB) http.HandlerFunc {
} }
flashMsg = "✅ Missing prizes updated." 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": case "run_all":
stats, err := services.RunTicketMatching(db, "manual") stats, err := services.RunTicketMatching(db, "manual")
if err != nil { if err != nil {

70
handlers/admin/prizes.go Normal file
View 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)
})
}

View File

@@ -107,3 +107,41 @@ func Submit(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusSeeOther) 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)
})
}

View File

@@ -280,7 +280,7 @@ func GetMyTickets(db *sql.DB) http.HandlerFunc {
ball1, ball2, ball3, ball4, ball5, ball6, ball1, ball2, ball3, ball4, ball5, ball6,
bonus1, bonus2, bonus1, bonus2,
purchase_method, purchase_date, image_path, duplicate, 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 FROM my_tickets
WHERE userid = ? WHERE userid = ?
ORDER BY draw_date DESC, created_at DESC 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 matchedMain, matchedBonus sql.NullInt64
var prizeTier sql.NullString var prizeTier sql.NullString
var isWinner sql.NullBool var isWinner sql.NullBool
var prizeLabel sql.NullString
var prizeAmount sql.NullFloat64
err := rows.Scan( err := rows.Scan(
&t.Id, &t.GameType, &t.DrawDate, &t.Id, &t.GameType, &t.DrawDate,
&b1, &b2, &b3, &b4, &b5, &b6, &b1, &b2, &b3, &b4, &b5, &b6,
&bo1, &bo2, &bo1, &bo2,
&t.PurchaseMethod, &t.PurchaseDate, &t.ImagePath, &t.Duplicate, &t.PurchaseMethod, &t.PurchaseDate, &t.ImagePath, &t.Duplicate,
&matchedMain, &matchedBonus, &prizeTier, &isWinner, &matchedMain, &matchedBonus, &prizeTier, &isWinner, &prizeLabel, &prizeAmount,
) )
if err != nil { if err != nil {
log.Println("⚠️ Failed to scan ticket row:", err) log.Println("⚠️ Failed to scan ticket row:", err)
@@ -335,7 +337,12 @@ func GetMyTickets(db *sql.DB) http.HandlerFunc {
if isWinner.Valid { if isWinner.Valid {
t.IsWinner = isWinner.Bool t.IsWinner = isWinner.Bool
} }
if prizeLabel.Valid {
t.PrizeLabel = prizeLabel.String
}
if prizeAmount.Valid {
t.PrizeAmount = prizeAmount.Float64
}
// Build balls slices (for template use) // Build balls slices (for template use)
t.Balls = helpers.BuildBallsSlice(t) t.Balls = helpers.BuildBallsSlice(t)
t.BonusBalls = helpers.BuildBonusSlice(t) t.BonusBalls = helpers.BuildBonusSlice(t)

View File

@@ -1,6 +1,9 @@
package helpers package helpers
import "synlotto-website/models" import (
"database/sql"
"synlotto-website/models"
)
func BuildBallsSlice(t models.Ticket) []int { func BuildBallsSlice(t models.Ticket) []int {
balls := []int{t.Ball1, t.Ball2, t.Ball3, t.Ball4, t.Ball5} balls := []int{t.Ball1, t.Ball2, t.Ball3, t.Ball4, t.Ball5}
@@ -22,3 +25,26 @@ func BuildBonusSlice(t models.Ticket) []int {
return bonuses 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
View File

@@ -1,10 +1,11 @@
package main package main
import ( import (
"database/sql"
"log" "log"
"net/http" "net/http"
"synlotto-website/handlers" "synlotto-website/handlers"
services "synlotto-website/handlers/admin" admin "synlotto-website/handlers/admin"
"synlotto-website/helpers" "synlotto-website/helpers"
"synlotto-website/middleware" "synlotto-website/middleware"
"synlotto-website/models" "synlotto-website/models"
@@ -24,27 +25,40 @@ func main() {
) )
mux := http.NewServeMux() 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.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
mux.HandleFunc("/", handlers.Home(db)) 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 log.Println("🌐 Running on http://localhost:8080")
mux.HandleFunc("/results/thunderball", handlers.ResultsThunderball(db)) 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("/login", middleware.Auth(false)(handlers.Login))
mux.HandleFunc("/logout", handlers.Logout) mux.HandleFunc("/logout", handlers.Logout)
mux.HandleFunc("/signup", middleware.Auth(false)(handlers.Signup)) mux.HandleFunc("/signup", middleware.Auth(false)(handlers.Signup))
mux.HandleFunc("/account/tickets/add_ticket", handlers.AddTicket(db)) mux.HandleFunc("/account/tickets/add_ticket", handlers.AddTicket(db))
mux.HandleFunc("/account/tickets/my_tickets", handlers.GetMyTickets(db)) mux.HandleFunc("/account/tickets/my_tickets", handlers.GetMyTickets(db))
}
// Admin Pages
mux.HandleFunc("/admin/triggers", services.AdminTriggersHandler(db)) func setupResultRoutes(mux *http.ServeMux, db *sql.DB) {
mux.HandleFunc("/results/thunderball", handlers.ResultsThunderball(db))
log.Println("🌐 Running on http://localhost:8080")
http.ListenAndServe(":8080", helpers.RateLimit(csrfMiddleware(mux)))
} }

View File

@@ -12,7 +12,7 @@ func MatchTicketToDraw(ticket models.MatchTicket, draw models.DrawResult, rules
mainMatches := helpers.CountMatches(ticket.Balls, draw.Balls) mainMatches := helpers.CountMatches(ticket.Balls, draw.Balls)
bonusMatches := helpers.CountMatches(ticket.BonusBalls, draw.BonusBalls) bonusMatches := helpers.CountMatches(ticket.BonusBalls, draw.BonusBalls)
prizeTier := getPrizeTier(ticket.GameType, mainMatches, bonusMatches, rules) prizeTier := GetPrizeTier(ticket.GameType, mainMatches, bonusMatches, rules)
isWinner := prizeTier != "" isWinner := prizeTier != ""
result := models.MatchResult{ result := models.MatchResult{
@@ -43,7 +43,7 @@ func MatchTicketToDraw(ticket models.MatchTicket, draw models.DrawResult, rules
return result 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 { for _, rule := range rules {
if rule.Game == game && rule.MainMatches == main && rule.BonusMatches == bonus { if rule.Game == game && rule.MainMatches == main && rule.BonusMatches == bonus {
return rule.Tier return rule.Tier

View File

@@ -1,5 +1,14 @@
package models package models
type DrawSummary struct {
Id int
GameType string
DrawDate string
BallSet string
Machine string
PrizeSet bool
}
type ThunderballResult struct { type ThunderballResult struct {
Id int Id int
DrawDate string DrawDate string

View File

@@ -38,3 +38,12 @@ type MatchRunStats struct {
TicketsMatched int TicketsMatched int
WinnersFound int WinnersFound int
} }
type MatchLog struct {
ID int
TriggeredBy string
RunAt string
TicketsMatched int
WinnersFound int
Notes string
}

View File

@@ -22,23 +22,23 @@ var ThunderballPrizeRules = []models.PrizeRule{
func GetThunderballPrizeIndex(main, bonus int) (int, bool) { func GetThunderballPrizeIndex(main, bonus int) (int, bool) {
switch { switch {
case main == 5 && bonus == 1: case main == 0 && bonus == 1:
return 9, true return 9, true
case main == 5 && bonus == 0: case main == 1 && bonus == 1:
return 8, true return 8, true
case main == 4 && bonus == 1: case main == 2 && bonus == 1:
return 7, true return 7, true
case main == 4 && bonus == 0: case main == 3 && bonus == 0:
return 6, true return 6, true
case main == 3 && bonus == 1: case main == 3 && bonus == 1:
return 5, true return 5, true
case main == 3 && bonus == 0: case main == 4 && bonus == 0:
return 4, true return 4, true
case main == 2 && bonus == 1: case main == 4 && bonus == 1:
return 3, true return 3, true
case main == 1 && bonus == 1: case main == 5 && bonus == 0:
return 2, true return 2, true
case main == 0 && bonus == 1: case main == 5 && bonus == 1:
return 1, true return 1, true
default: default:
return 0, false return 0, false

View File

@@ -6,9 +6,10 @@ import (
"log" "log"
"synlotto-website/handlers" "synlotto-website/handlers"
"synlotto-website/helpers" "synlotto-website/helpers"
"synlotto-website/matcher"
"synlotto-website/models" "synlotto-website/models"
"synlotto-website/rules" thunderballrules "synlotto-website/rules"
draws "synlotto-website/services/draws" services "synlotto-website/services/draws"
) )
func RunTicketMatching(db *sql.DB, triggeredBy string) (models.MatchRunStats, error) { 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), BonusBalls: helpers.BuildBonusSlice(t),
} }
draw := draws.GetDrawResultForTicket(db, t.GameType, t.DrawDate) draw := services.GetDrawResultForTicket(db, t.GameType, t.DrawDate)
result := handlers.MatchTicketToDraw(matchTicket, draw, rules.ThunderballPrizeRules) result := handlers.MatchTicketToDraw(matchTicket, draw, thunderballrules.ThunderballPrizeRules)
if result.MatchedDrawID == 0 { if result.MatchedDrawID == 0 {
continue continue
@@ -130,7 +131,7 @@ func UpdateMissingPrizes(db *sql.DB) error {
continue continue
} }
idx, ok := rules.GetThunderballPrizeIndex(t.Main, t.Bonus) idx, ok := thunderballrules.GetThunderballPrizeIndex(t.Main, t.Bonus)
if !ok { if !ok {
log.Printf("❌ No index for %d main, %d bonus", t.Main, t.Bonus) log.Printf("❌ No index for %d main, %d bonus", t.Main, t.Bonus)
continue continue
@@ -163,3 +164,96 @@ func UpdateMissingPrizes(db *sql.DB) error {
return nil 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
}

View File

@@ -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 { .ball {
display: inline-flex; display: inline-flex;
justify-content: center; justify-content: center;

View File

@@ -73,15 +73,13 @@
</td> </td>
<td> <td>
{{ if $ticket.IsWinner }} {{ if $ticket.IsWinner }}
{{ if $ticket.PrizeLabel }} {{ if eq $ticket.PrizeLabel "" }}
{{ if eq $ticket.PrizeLabel "Free Ticket" }} <span class="text-yellow-500 italic">pending</span>
{{ else if eq $ticket.PrizeLabel "Free Ticket" }}
🎟️ {{ $ticket.PrizeLabel }} 🎟️ {{ $ticket.PrizeLabel }}
{{ else }} {{ else }}
💷 {{ $ticket.PrizeLabel }} 💷 {{ $ticket.PrizeLabel }}
{{ end }} {{ end }}
{{ else }}
<span class="text-gray-400 italic">pending</span>
{{ end }}
{{ else }} {{ else }}
{{ end }} {{ end }}

View 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 }}

View 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 }}

View 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 }}

View 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 }}

View 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 }}

View 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 }}

View 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 }}

View File

@@ -19,4 +19,11 @@
<button>Run All</button> <button>Run All</button>
</form> </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 }} {{ end }}

View File

@@ -6,15 +6,6 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>SynLotto</title> <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> </head>
<body> <body>
<div class="topbar"> <div class="topbar">
@@ -24,6 +15,7 @@
<p><a href="/login">Login</a></p> <p><a href="/login">Login</a></p>
{{ end }} {{ end }}
</div> </div>
<a href="/admin/dashboard" class="hover:underline">Dashboard</a>
{{ if .Flash }} {{ if .Flash }}
<div class="flash">{{ .Flash }}</div> <div class="flash">{{ .Flash }}</div>