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)
|
setupAdminRoutes(mux, db)
|
||||||
setupAccountRoutes(mux, db)
|
setupAccountRoutes(mux, db)
|
||||||
setupResultRoutes(mux, db)
|
setupResultRoutes(mux, db)
|
||||||
|
setupSyndicateRoutes(mux, db)
|
||||||
|
|
||||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
|
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) {
|
func setupResultRoutes(mux *http.ServeMux, db *sql.DB) {
|
||||||
mux.HandleFunc("/results/thunderball", handlers.ResultsThunderball(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 {
|
type Ticket struct {
|
||||||
Id int
|
Id int
|
||||||
|
UserId int
|
||||||
|
SyndicateId *int
|
||||||
GameType string
|
GameType string
|
||||||
DrawDate string
|
DrawDate string
|
||||||
Ball1 int
|
Ball1 int
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ func InitDB(filepath string) *sql.DB {
|
|||||||
SchemaLogTicketMatching,
|
SchemaLogTicketMatching,
|
||||||
SchemaAdminAccessLog,
|
SchemaAdminAccessLog,
|
||||||
SchemaNewAuditLog,
|
SchemaNewAuditLog,
|
||||||
|
SchemaSyndicates,
|
||||||
|
SchemaSyndicateMembers,
|
||||||
|
SchemaSyndicateInvites,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, stmt := range schemas {
|
for _, stmt := range schemas {
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ CREATE TABLE IF NOT EXISTS my_tickets (
|
|||||||
is_winner BOOLEAN,
|
is_winner BOOLEAN,
|
||||||
prize_amount INTEGER,
|
prize_amount INTEGER,
|
||||||
prize_label TEXT,
|
prize_label TEXT,
|
||||||
|
syndicate_id INTEGER,
|
||||||
FOREIGN KEY (userId) REFERENCES users(id)
|
FOREIGN KEY (userId) REFERENCES users(id)
|
||||||
);`
|
);`
|
||||||
|
|
||||||
@@ -173,3 +174,38 @@ CREATE TABLE IF NOT EXISTS audit_log (
|
|||||||
user_agent TEXT,
|
user_agent TEXT,
|
||||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
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