Compare commits

...

2 Commits

Author SHA1 Message Date
df6608dda5 forgot to stage 2025-04-02 23:53:40 +01:00
053ccf3845 **Untested! ** Add restore functionality for archived messages
- Added `RestoreMessageHandler` and route at `/account/messages/restore`
- Updated `users_messages` table to support `archived_at` reset
- Added restore button to archived messages template
- Ensures archived messages can be moved back into inbox
2025-04-02 23:53:29 +01:00
13 changed files with 791 additions and 0 deletions

234
handlers/syndicate.go Normal file
View 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)
}
}

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

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

View File

@@ -2,6 +2,8 @@ package models
type Ticket struct {
Id int
UserId int
SyndicateId *int
GameType string
DrawDate string
Ball1 int

View File

@@ -25,6 +25,9 @@ func InitDB(filepath string) *sql.DB {
SchemaLogTicketMatching,
SchemaAdminAccessLog,
SchemaNewAuditLog,
SchemaSyndicates,
SchemaSyndicateMembers,
SchemaSyndicateInvites,
}
for _, stmt := range schemas {

View File

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

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

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

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

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

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