Compare commits
2 Commits
db5352bc9c
...
df6608dda5
| Author | SHA1 | Date | |
|---|---|---|---|
| df6608dda5 | |||
| 053ccf3845 |
234
handlers/syndicate.go
Normal file
234
handlers/syndicate.go
Normal file
@@ -0,0 +1,234 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"synlotto-website/helpers"
|
||||
"synlotto-website/models"
|
||||
"synlotto-website/storage"
|
||||
)
|
||||
|
||||
func CreateSyndicateHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
data := BuildTemplateData(db, w, r)
|
||||
context := helpers.TemplateContext(w, r, data)
|
||||
tmpl := helpers.LoadTemplateFiles("create-syndicate.html", "templates/account/syndicates/create.html")
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
|
||||
case http.MethodPost:
|
||||
name := r.FormValue("name")
|
||||
description := r.FormValue("description")
|
||||
|
||||
userId, ok := helpers.GetCurrentUserID(r)
|
||||
if !ok || name == "" {
|
||||
helpers.SetFlash(w, r, "Invalid data submitted")
|
||||
http.Redirect(w, r, "/account/syndicates/create", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := storage.CreateSyndicate(db, userId, name, description)
|
||||
if err != nil {
|
||||
helpers.SetFlash(w, r, "Failed to create syndicate")
|
||||
} else {
|
||||
helpers.SetFlash(w, r, "Syndicate created successfully")
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/account/syndicates", http.StatusSeeOther)
|
||||
default:
|
||||
helpers.RenderError(w, r, http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ListSyndicatesHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := helpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
helpers.RenderError(w, r, 403) // ToDo need to make this use the handler so i dont need to define errors.
|
||||
return
|
||||
}
|
||||
|
||||
managed := storage.GetSyndicatesByOwner(db, userID)
|
||||
member := storage.GetSyndicatesByMember(db, userID)
|
||||
|
||||
data := BuildTemplateData(db, w, r)
|
||||
context := helpers.TemplateContext(w, r, data)
|
||||
context["ManagedSyndicates"] = managed
|
||||
context["JoinedSyndicates"] = member
|
||||
|
||||
tmpl := helpers.LoadTemplateFiles("syndicates.html", "templates/account/syndicates/index.html")
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
}
|
||||
}
|
||||
|
||||
func ViewSyndicateHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := helpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
helpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
|
||||
syndicate, err := storage.GetSyndicateByID(db, syndicateID)
|
||||
if err != nil || syndicate == nil {
|
||||
helpers.RenderError(w, r, 404)
|
||||
return
|
||||
}
|
||||
|
||||
isManager := userID == syndicate.OwnerID
|
||||
isMember := storage.IsSyndicateMember(db, syndicateID, userID)
|
||||
|
||||
if !isManager && !isMember {
|
||||
helpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
members := storage.GetSyndicateMembers(db, syndicateID)
|
||||
|
||||
data := BuildTemplateData(db, w, r)
|
||||
context := helpers.TemplateContext(w, r, data)
|
||||
context["Syndicate"] = syndicate
|
||||
context["Members"] = members
|
||||
context["IsManager"] = isManager
|
||||
|
||||
tmpl := helpers.LoadTemplateFiles("syndicate-view.html", "templates/syndicates/view.html")
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
}
|
||||
}
|
||||
|
||||
func InviteMemberHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := helpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
helpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
|
||||
syndicate, err := storage.GetSyndicateByID(db, syndicateID)
|
||||
if err != nil || syndicate == nil || syndicate.OwnerID != userID {
|
||||
helpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
data := BuildTemplateData(db, w, r)
|
||||
context := helpers.TemplateContext(w, r, data)
|
||||
context["Syndicate"] = syndicate
|
||||
|
||||
tmpl := helpers.LoadTemplateFiles("invite.html", "templates/syndicates/invite.html")
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
|
||||
case http.MethodPost:
|
||||
username := r.FormValue("username")
|
||||
invitee := storage.GetUserByUsername(db, username)
|
||||
|
||||
if invitee == nil {
|
||||
helpers.SetFlash(w, r, "User not found.")
|
||||
} else if storage.IsSyndicateMember(db, syndicateID, invitee.Id) {
|
||||
helpers.SetFlash(w, r, "User is already a member.")
|
||||
} else {
|
||||
err := storage.AddMemberToSyndicate(db, syndicateID, invitee.Id)
|
||||
if err != nil {
|
||||
helpers.SetFlash(w, r, "Failed to invite user.")
|
||||
} else {
|
||||
helpers.SetFlash(w, r, "User successfully invited.")
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, fmt.Sprintf("/account/syndicates/view?id=%d", syndicateID), http.StatusSeeOther)
|
||||
|
||||
default:
|
||||
helpers.RenderError(w, r, 405)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SyndicateLogTicketHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := helpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
helpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
syndicateId := helpers.Atoi(r.URL.Query().Get("id"))
|
||||
syndicate, err := storage.GetSyndicateByID(db, syndicateId)
|
||||
if err != nil || syndicate.OwnerID != userID {
|
||||
helpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
data := BuildTemplateData(db, w, r)
|
||||
context := helpers.TemplateContext(w, r, data)
|
||||
context["Syndicate"] = syndicate
|
||||
|
||||
tmpl := helpers.LoadTemplateFiles("syndicate-log-ticket.html", "templates/syndicates/log_ticket.html")
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
|
||||
case http.MethodPost:
|
||||
gameType := r.FormValue("game_type")
|
||||
drawDate := r.FormValue("draw_date")
|
||||
method := r.FormValue("purchase_method")
|
||||
|
||||
err := storage.InsertTicket(db, models.Ticket{
|
||||
UserId: userID,
|
||||
GameType: gameType,
|
||||
DrawDate: drawDate,
|
||||
PurchaseMethod: method,
|
||||
SyndicateId: &syndicateId,
|
||||
// ToDo image path
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
helpers.SetFlash(w, r, "Failed to add ticket.")
|
||||
} else {
|
||||
helpers.SetFlash(w, r, "Ticket added for syndicate.")
|
||||
}
|
||||
|
||||
http.Redirect(w, r, fmt.Sprintf("/account/syndicates/view?id=%d", syndicateId), http.StatusSeeOther)
|
||||
|
||||
default:
|
||||
helpers.RenderError(w, r, 405)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SyndicateTicketsHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := helpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
helpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
syndicateID := helpers.Atoi(r.URL.Query().Get("id"))
|
||||
if syndicateID == 0 {
|
||||
helpers.RenderError(w, r, 400)
|
||||
return
|
||||
}
|
||||
|
||||
// Check membership
|
||||
if !storage.IsSyndicateMember(db, syndicateID, userID) {
|
||||
helpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
tickets := storage.GetSyndicateTickets(db, syndicateID)
|
||||
|
||||
data := BuildTemplateData(db, w, r)
|
||||
context := helpers.TemplateContext(w, r, data)
|
||||
context["SyndicateID"] = syndicateID
|
||||
context["Tickets"] = tickets
|
||||
|
||||
tmpl := helpers.LoadTemplateFiles("syndicate-tickets.html", "templates/syndicates/tickets.html")
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
}
|
||||
}
|
||||
80
handlers/syndicate_invites.go
Normal file
80
handlers/syndicate_invites.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"synlotto-website/helpers"
|
||||
"synlotto-website/models"
|
||||
"synlotto-website/storage"
|
||||
)
|
||||
|
||||
func SyndicateInviteHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
syndicateID := helpers.Atoi(r.FormValue("syndicate_id"))
|
||||
recipientUsername := r.FormValue("username")
|
||||
senderID, ok := helpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
helpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
recipient := models.GetUserByUsername(recipientUsername)
|
||||
if recipient == nil {
|
||||
helpers.SetFlash(w, r, "User not found")
|
||||
http.Redirect(w, r, "/account/syndicates", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
err := storage.InviteUserToSyndicate(db, syndicateID, recipient.Id, senderID)
|
||||
if err != nil {
|
||||
helpers.SetFlash(w, r, "Failed to invite user")
|
||||
} else {
|
||||
helpers.SetFlash(w, r, "Invitation sent to "+recipientUsername)
|
||||
}
|
||||
http.Redirect(w, r, "/account/syndicates", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func ViewInvitesHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := helpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
helpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
invites := storage.GetPendingInvites(db, userID)
|
||||
data := BuildTemplateData(db, w, r)
|
||||
context := helpers.TemplateContext(w, r, data)
|
||||
context["Invites"] = invites
|
||||
|
||||
tmpl := helpers.LoadTemplateFiles("invites.html", "templates/syndicates/invites.html")
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
}
|
||||
}
|
||||
|
||||
func AcceptInviteHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
inviteID := helpers.Atoi(r.URL.Query().Get("id"))
|
||||
userID, ok := helpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
helpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
err := storage.AcceptInvite(db, inviteID, userID)
|
||||
if err != nil {
|
||||
helpers.SetFlash(w, r, "Failed to accept invite")
|
||||
} else {
|
||||
helpers.SetFlash(w, r, "You have joined the syndicate")
|
||||
}
|
||||
http.Redirect(w, r, "/account/syndicates", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func DeclineInviteHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
inviteID := helpers.Atoi(r.URL.Query().Get("id"))
|
||||
_ = storage.UpdateInviteStatus(db, inviteID, "declined")
|
||||
http.Redirect(w, r, "/account/syndicates/invites", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
14
main.go
14
main.go
@@ -30,6 +30,7 @@ func main() {
|
||||
setupAdminRoutes(mux, db)
|
||||
setupAccountRoutes(mux, db)
|
||||
setupResultRoutes(mux, db)
|
||||
setupSyndicateRoutes(mux, db)
|
||||
|
||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
||||
|
||||
@@ -81,3 +82,16 @@ func setupAccountRoutes(mux *http.ServeMux, db *sql.DB) {
|
||||
func setupResultRoutes(mux *http.ServeMux, db *sql.DB) {
|
||||
mux.HandleFunc("/results/thunderball", handlers.ResultsThunderball(db))
|
||||
}
|
||||
|
||||
func setupSyndicateRoutes(mux *http.ServeMux, db *sql.DB) {
|
||||
mux.HandleFunc("/account/syndicates", middleware.Auth(true)(handlers.ListSyndicatesHandler(db)))
|
||||
mux.HandleFunc("/account/syndicates/invite", middleware.Auth(true)(handlers.InviteMemberHandler(db)))
|
||||
mux.HandleFunc("/account/syndicates/view", middleware.Auth(true)(handlers.ViewSyndicateHandler(db)))
|
||||
mux.HandleFunc("/account/syndicates/tickets", middleware.Auth(true)(handlers.SyndicateTicketsHandler(db)))
|
||||
mux.HandleFunc("/account/syndicates/tickets/new", middleware.Auth(true)(handlers.SyndicateLogTicketHandler(db)))
|
||||
mux.HandleFunc("/account/syndicates/invite", middleware.Auth(true)(handlers.SyndicateInviteHandler(db)))
|
||||
mux.HandleFunc("/account/syndicates/invites", middleware.Auth(true)(handlers.ViewInvitesHandler(db)))
|
||||
mux.HandleFunc("/account/syndicates/invites/accept", middleware.Auth(true)(handlers.AcceptInviteHandler(db)))
|
||||
mux.HandleFunc("/account/syndicates/invites/decline", middleware.Auth(true)(handlers.DeclineInviteHandler(db)))
|
||||
|
||||
}
|
||||
|
||||
29
models/syndicate.go
Normal file
29
models/syndicate.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Syndicate struct {
|
||||
ID int
|
||||
OwnerID int
|
||||
Name string
|
||||
Description string
|
||||
CreatedBy int
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type SyndicateMember struct {
|
||||
ID int
|
||||
SyndicateID int
|
||||
UserID int
|
||||
Role string
|
||||
JoinedAt time.Time
|
||||
}
|
||||
|
||||
type SyndicateInvite struct {
|
||||
ID int
|
||||
SyndicateID int
|
||||
InvitedUserID int
|
||||
SentByUserID int
|
||||
Status string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package models
|
||||
|
||||
type Ticket struct {
|
||||
Id int
|
||||
UserId int
|
||||
SyndicateId *int
|
||||
GameType string
|
||||
DrawDate string
|
||||
Ball1 int
|
||||
|
||||
@@ -25,6 +25,9 @@ func InitDB(filepath string) *sql.DB {
|
||||
SchemaLogTicketMatching,
|
||||
SchemaAdminAccessLog,
|
||||
SchemaNewAuditLog,
|
||||
SchemaSyndicates,
|
||||
SchemaSyndicateMembers,
|
||||
SchemaSyndicateInvites,
|
||||
}
|
||||
|
||||
for _, stmt := range schemas {
|
||||
|
||||
@@ -108,6 +108,7 @@ CREATE TABLE IF NOT EXISTS my_tickets (
|
||||
is_winner BOOLEAN,
|
||||
prize_amount INTEGER,
|
||||
prize_label TEXT,
|
||||
syndicate_id INTEGER,
|
||||
FOREIGN KEY (userId) REFERENCES users(id)
|
||||
);`
|
||||
|
||||
@@ -173,3 +174,38 @@ CREATE TABLE IF NOT EXISTS audit_log (
|
||||
user_agent TEXT,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);`
|
||||
|
||||
const SchemaSyndicates = `
|
||||
CREATE TABLE IF NOT EXISTS syndicates (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
owner_id INTEGER NOT NULL,
|
||||
join_code TEXT UNIQUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (owner_id) REFERENCES users(id)
|
||||
);`
|
||||
|
||||
const SchemaSyndicateMembers = `
|
||||
CREATE TABLE IF NOT EXISTS syndicate_members (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
syndicate_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
role TEXT DEFAULT 'member', -- owner, manager, member
|
||||
status TEXT DEFAULT 'active', -- pending, accepted, kicked
|
||||
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (syndicate_id) REFERENCES syndicates(id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);`
|
||||
|
||||
const SchemaSyndicateInvites = `
|
||||
CREATE TABLE IF NOT EXISTS syndicate_invites (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
syndicate_id INTEGER NOT NULL,
|
||||
invited_user_id INTEGER NOT NULL,
|
||||
sent_by_user_id INTEGER NOT NULL,
|
||||
status TEXT DEFAULT 'pending', -- 'pending', 'accepted', 'declined'
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(syndicate_id) REFERENCES syndicates(id),
|
||||
FOREIGN KEY(invited_user_id) REFERENCES users(id)
|
||||
);`
|
||||
|
||||
230
storage/syndicate.go
Normal file
230
storage/syndicate.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"synlotto-website/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GetSyndicatesByOwner(db *sql.DB, ownerID int) []models.Syndicate {
|
||||
rows, err := db.Query(`
|
||||
SELECT id, name, description, created_at, owner_id
|
||||
FROM syndicates
|
||||
WHERE owner_id = ?`, ownerID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var syndicates []models.Syndicate
|
||||
for rows.Next() {
|
||||
var s models.Syndicate
|
||||
err := rows.Scan(&s.ID, &s.Name, &s.Description, &s.CreatedAt, &s.OwnerID)
|
||||
if err == nil {
|
||||
syndicates = append(syndicates, s)
|
||||
}
|
||||
}
|
||||
return syndicates
|
||||
}
|
||||
|
||||
func GetSyndicatesByMember(db *sql.DB, userID int) []models.Syndicate {
|
||||
rows, err := db.Query(`
|
||||
SELECT s.id, s.name, s.description, s.created_at, s.owner_id
|
||||
FROM syndicates s
|
||||
JOIN syndicate_members m ON s.id = m.syndicate_id
|
||||
WHERE m.user_id = ?`, userID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var syndicates []models.Syndicate
|
||||
for rows.Next() {
|
||||
var s models.Syndicate
|
||||
err := rows.Scan(&s.ID, &s.Name, &s.Description, &s.CreatedAt, &s.OwnerID)
|
||||
if err == nil {
|
||||
syndicates = append(syndicates, s)
|
||||
}
|
||||
}
|
||||
return syndicates
|
||||
}
|
||||
|
||||
func GetSyndicateByID(db *sql.DB, id int) (*models.Syndicate, error) {
|
||||
row := db.QueryRow(`SELECT id, name, description, owner_id, created_at FROM syndicates WHERE id = ?`, id)
|
||||
var s models.Syndicate
|
||||
err := row.Scan(&s.ID, &s.Name, &s.Description, &s.OwnerID, &s.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func GetSyndicateMembers(db *sql.DB, syndicateID int) []models.SyndicateMember {
|
||||
rows, err := db.Query(`
|
||||
SELECT m.user_id, u.username, m.joined_at
|
||||
FROM syndicate_members m
|
||||
JOIN users u ON u.id = m.user_id
|
||||
WHERE m.syndicate_id = ?
|
||||
`, syndicateID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var members []models.SyndicateMember
|
||||
for rows.Next() {
|
||||
var m models.SyndicateMember
|
||||
err := rows.Scan(&m.UserID, &m.UserID, &m.JoinedAt)
|
||||
if err == nil {
|
||||
members = append(members, m)
|
||||
}
|
||||
}
|
||||
return members
|
||||
}
|
||||
|
||||
func IsSyndicateMember(db *sql.DB, syndicateID, userID int) bool {
|
||||
var count int
|
||||
err := db.QueryRow(`SELECT COUNT(*) FROM syndicate_members WHERE syndicate_id = ? AND user_id = ?`, syndicateID, userID).Scan(&count)
|
||||
return err == nil && count > 0
|
||||
}
|
||||
|
||||
func GetUserByUsername(db *sql.DB, username string) *models.User {
|
||||
row := db.QueryRow(`SELECT id, username, is_admin FROM users WHERE username = ?`, username)
|
||||
var u models.User
|
||||
err := row.Scan(&u.Id, &u.Username, &u.IsAdmin)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &u
|
||||
}
|
||||
|
||||
func AddMemberToSyndicate(db *sql.DB, syndicateID, userID int) error {
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO syndicate_members (syndicate_id, user_id, joined_at)
|
||||
VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||
`, syndicateID, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetSyndicateTickets(db *sql.DB, syndicateID int) []models.Ticket {
|
||||
rows, err := db.Query(`
|
||||
SELECT id, userId, syndicateId, game_type, draw_date, ball1, ball2, ball3, ball4, ball5, ball6,
|
||||
bonus1, bonus2, matched_main, matched_bonus, prize_tier, prize_amount, prize_label, is_winner
|
||||
FROM my_tickets
|
||||
WHERE syndicateId = ?
|
||||
ORDER BY draw_date DESC
|
||||
`, syndicateID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var tickets []models.Ticket
|
||||
for rows.Next() {
|
||||
var t models.Ticket
|
||||
err := rows.Scan(
|
||||
&t.Id, &t.UserId, &t.SyndicateId, &t.GameType, &t.DrawDate,
|
||||
&t.Ball1, &t.Ball2, &t.Ball3, &t.Ball4, &t.Ball5, &t.Ball6,
|
||||
&t.Bonus1, &t.Bonus2, &t.MatchedMain, &t.MatchedBonus,
|
||||
&t.PrizeTier, &t.PrizeAmount, &t.PrizeLabel, &t.IsWinner,
|
||||
)
|
||||
if err == nil {
|
||||
tickets = append(tickets, t)
|
||||
}
|
||||
}
|
||||
return tickets
|
||||
}
|
||||
|
||||
func InviteUserToSyndicate(db *sql.DB, syndicateID, invitedUserID, senderID int) error {
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO syndicate_invites (syndicate_id, invited_user_id, sent_by_user_id)
|
||||
VALUES (?, ?, ?)
|
||||
`, syndicateID, invitedUserID, senderID)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetPendingInvites(db *sql.DB, userID int) []models.SyndicateInvite {
|
||||
rows, err := db.Query(`
|
||||
SELECT id, syndicate_id, invited_user_id, sent_by_user_id, status, created_at
|
||||
FROM syndicate_invites
|
||||
WHERE invited_user_id = ? AND status = 'pending'
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var invites []models.SyndicateInvite
|
||||
for rows.Next() {
|
||||
var i models.SyndicateInvite
|
||||
rows.Scan(&i.ID, &i.SyndicateID, &i.InvitedUserID, &i.SentByUserID, &i.Status, &i.CreatedAt)
|
||||
invites = append(invites, i)
|
||||
}
|
||||
return invites
|
||||
}
|
||||
|
||||
func UpdateInviteStatus(db *sql.DB, inviteID int, status string) error {
|
||||
_, err := db.Exec(`
|
||||
UPDATE syndicate_invites
|
||||
SET status = ?
|
||||
WHERE id = ?
|
||||
`, status, inviteID)
|
||||
return err
|
||||
}
|
||||
|
||||
func AcceptInvite(db *sql.DB, inviteID, userID int) error {
|
||||
var syndicateID int
|
||||
err := db.QueryRow(`
|
||||
SELECT syndicate_id FROM syndicate_invites
|
||||
WHERE id = ? AND invited_user_id = ? AND status = 'pending'
|
||||
`, inviteID, userID).Scan(&syndicateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := UpdateInviteStatus(db, inviteID, "accepted"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = db.Exec(`
|
||||
INSERT INTO syndicate_members (syndicate_id, user_id, joined_at)
|
||||
VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||
`, syndicateID, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
func CreateSyndicate(db *sql.DB, ownerID int, name, description string) (int64, error) {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
result, err := tx.Exec(`
|
||||
INSERT INTO syndicates (name, description, owner_id, created_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`, name, description, ownerID, time.Now())
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to create syndicate: %w", err)
|
||||
}
|
||||
|
||||
syndicateID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get syndicate ID: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
INSERT INTO syndicate_members (syndicate_id, user_id, accepted)
|
||||
VALUES (?, ?, 1)
|
||||
`, syndicateID, ownerID)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to add owner as member: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return 0, fmt.Errorf("commit failed: %w", err)
|
||||
}
|
||||
|
||||
return syndicateID, nil
|
||||
}
|
||||
41
templates/syndicates/index.html
Normal file
41
templates/syndicates/index.html
Normal file
@@ -0,0 +1,41 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-5">
|
||||
<h2>Your Syndicates</h2>
|
||||
|
||||
{{ if .ManagedSyndicates }}
|
||||
<h4 class="mt-4">Managed</h4>
|
||||
<ul class="list-group mb-3">
|
||||
{{ range .ManagedSyndicates }}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>{{ .Name }}</strong><br>
|
||||
<small class="text-muted">{{ .Description }}</small>
|
||||
</div>
|
||||
<a href="/account/syndicates/view?id={{ .ID }}" class="btn btn-outline-primary btn-sm">Manage</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
|
||||
{{ if .JoinedSyndicates }}
|
||||
<h4 class="mt-4">Member</h4>
|
||||
<ul class="list-group mb-3">
|
||||
{{ range .JoinedSyndicates }}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>{{ .Name }}</strong><br>
|
||||
<small class="text-muted">{{ .Description }}</small>
|
||||
</div>
|
||||
<a href="/account/syndicates/view?id={{ .ID }}" class="btn btn-outline-secondary btn-sm">View</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
|
||||
{{ if not .ManagedSyndicates | and (not .JoinedSyndicates) }}
|
||||
<div class="alert alert-info">You are not part of any syndicates yet.</div>
|
||||
{{ end }}
|
||||
|
||||
<a href="/account/syndicates/create" class="btn btn-primary mt-3">Create New Syndicate</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
18
templates/syndicates/invite.html
Normal file
18
templates/syndicates/invite.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-5">
|
||||
<h2>Invite Member to "{{ .Syndicate.Name }}"</h2>
|
||||
{{ if .Flash }}
|
||||
<div class="alert alert-info">{{ .Flash }}</div>
|
||||
{{ end }}
|
||||
|
||||
<form method="POST">
|
||||
{{ .CSRFField }}
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username to Invite</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Send Invite</button>
|
||||
<a href="/account/syndicates/view?id={{ .Syndicate.ID }}" class="btn btn-secondary ms-2">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
34
templates/syndicates/log_ticket.html
Normal file
34
templates/syndicates/log_ticket.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-5">
|
||||
<h2>Add Ticket for {{ .Syndicate.Name }}</h2>
|
||||
|
||||
<form method="POST">
|
||||
{{ .CSRFField }}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="game_type" class="form-label">Game Type</label>
|
||||
<select class="form-select" id="game_type" name="game_type" required>
|
||||
<option value="Thunderball">Thunderball</option>
|
||||
<option value="Lotto">Lotto</option>
|
||||
<!-- Add more as needed -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="draw_date" class="form-label">Draw Date</label>
|
||||
<input type="date" class="form-control" id="draw_date" name="draw_date" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="purchase_method" class="form-label">Purchase Method</label>
|
||||
<input type="text" class="form-control" id="purchase_method" name="purchase_method">
|
||||
</div>
|
||||
|
||||
<!-- Ball Inputs -->
|
||||
{{ template "ballInputs" . }}
|
||||
|
||||
<button type="submit" class="btn btn-success">Submit Ticket</button>
|
||||
<a href="/account/syndicates/view?id={{ .Syndicate.ID }}" class="btn btn-secondary ms-2">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
42
templates/syndicates/tickets.html
Normal file
42
templates/syndicates/tickets.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-4">
|
||||
<h2>Syndicate Tickets</h2>
|
||||
|
||||
{{ if .Tickets }}
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Game</th>
|
||||
<th>Draw Date</th>
|
||||
<th>Numbers</th>
|
||||
<th>Matched</th>
|
||||
<th>Prize</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Tickets }}
|
||||
<tr>
|
||||
<td>{{ .GameType }}</td>
|
||||
<td>{{ .DrawDate }}</td>
|
||||
<td>
|
||||
{{ .Ball1 }} {{ .Ball2 }} {{ .Ball3 }} {{ .Ball4 }} {{ .Ball5 }} {{ .Ball6 }}
|
||||
{{ if .Bonus1 }}+{{ .Bonus1 }}{{ end }}
|
||||
{{ if .Bonus2 }} {{ .Bonus2 }}{{ end }}
|
||||
</td>
|
||||
<td>{{ .MatchedMain }} + {{ .MatchedBonus }}</td>
|
||||
<td>
|
||||
{{ if .IsWinner }}
|
||||
💷 {{ .PrizeLabel }}
|
||||
{{ else }}
|
||||
<span class="text-muted">–</span>
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ else }}
|
||||
<div class="alert alert-info">No tickets found for this syndicate.</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
28
templates/syndicates/view.html
Normal file
28
templates/syndicates/view.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-5">
|
||||
<h2>{{ .Syndicate.Name }}</h2>
|
||||
<p class="text-muted">{{ .Syndicate.Description }}</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Members</h4>
|
||||
<ul class="list-group mb-3">
|
||||
{{ range .Members }}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<span>{{ .Username }}</span>
|
||||
<small class="text-muted">Joined: {{ .JoinedAt.Format "02 Jan 2006" }}</small>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
|
||||
{{ if .IsManager }}
|
||||
<div class="alert alert-warning">
|
||||
<strong>Manager Controls</strong><br>
|
||||
You can add or remove members, and manage tickets.
|
||||
</div>
|
||||
<a href="/account/syndicates/invite?id={{ .Syndicate.ID }}" class="btn btn-outline-primary">Invite Members</a>
|
||||
{{ end }}
|
||||
|
||||
<a href="/account/syndicates" class="btn btn-secondary mt-3">← Back to Syndicates</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
Reference in New Issue
Block a user