Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:PlayerDashboard: Difference between revisions

From eSportsAmaze
No edit summary
No edit summary
Line 1: Line 1:
-- Module:PlayerDashboard v2
-- ================================================================
-- All 8 fixes applied.
-- Module:PlayerDashboard v3
-- Full rewrite with all fixes:
--  • Circular photo, vertically centered hero
--  • Current team: logo (light/dark) + display_name
--  • Team history: logos (light/dark)
--  • Individual awards table (from PrizeMoney where player= set)
--  • Team results table:
--      - Place from new Tournament_Results cargo table
--      - Prize/award from PrizeMoney (team rows)
--      - Shows ALL tournaments player appeared in
--      - Place/Award column shows chip OR award badge
--  • Flag emojis for nationality
--  • Dark mode link + badge overrides via CSS variables
--  • Mobile friendly
-- ================================================================


local p    = {}
local p    = {}
Line 6: Line 20:
local html  = mw.html
local html  = mw.html
local lang  = mw.getContentLanguage()
local lang  = mw.getContentLanguage()
-- ── Helpers ──────────────────────────────────────────────────────


local function esc(s)
local function esc(s)
     if not s then return "" end
     if not s then return "" end
     return s:gsub("\\","\\\\"):gsub("'","\\'")
     return s:gsub("\\", "\\\\"):gsub("'", "\\'")
end
end


local function fmtCurrency(n)
local function fmtCurrency(n)
     if not n or n == 0 then return "—" end
     if not n or n == 0 then return "—" end
     local s = tostring(math.floor(n))
     local s   = tostring(math.floor(n))
     local l3 = s:sub(-3)
     local l3   = s:sub(-3)
     local rest = s:sub(1, -4)
     local rest = s:sub(1, -4)
     local fmt = rest:reverse():gsub("(%d%d)", "%1,"):reverse()
     local fmt = rest:reverse():gsub("(%d%d)", "%1,"):reverse()
     if fmt ~= "" then l3 = "," .. l3 end
     if fmt ~= "" then l3 = "," .. l3 end
     return "₹ " .. fmt .. l3
     return "₹ " .. fmt .. l3
Line 29: Line 45:
-- Flag emoji map
-- Flag emoji map
local FLAGS = {
local FLAGS = {
     india       = "🇮🇳", bangladesh = "🇧🇩", pakistan   = "🇵🇰",
     india="🇮🇳", bangladesh="🇧🇩", pakistan="🇵🇰", nepal="🇳🇵",
    nepal       = "🇳🇵", srilanka   = "🇱🇰", myanmar     = "🇲🇲",
    srilanka="🇱🇰", myanmar="🇲🇲", indonesia="🇮🇩", thailand="🇹🇭",
    indonesia   = "🇮🇩", thailand   = "🇹🇭", malaysia   = "🇲🇾",
    malaysia="🇲🇾", philippines="🇵🇭", vietnam="🇻🇳", singapore="🇸🇬",
    philippines = "🇵🇭", vietnam   = "🇻🇳", singapore   = "🇸🇬",
     china="🇨🇳", japan="🇯🇵", korea="🇰🇷", usa="🇺🇸",
     china       = "🇨🇳", japan       = "🇯🇵", korea       = "🇰🇷",
    uk="🇬🇧", germany="🇩🇪", brazil="🇧🇷", australia="🇦🇺"
    usa         = "🇺🇸", uk         = "🇬🇧", germany     = "🇩🇪",
    brazil     = "🇧🇷", australia   = "🇦🇺"
}
}
local function getFlag(country)
local function getFlag(country)
     if not country or country == "" then return "" end
     if not country or country == "" then return "" end
     local key = country:lower():gsub("%s+", "")
     local key = country:lower():gsub("%s+",""):gsub("-","")
     return (FLAGS[key] or "") .. " "
     return (FLAGS[key] or "") .. " "
end
end
Line 45: Line 59:
local function statusStyle(s)
local function statusStyle(s)
     s = (s or ""):lower()
     s = (s or ""):lower()
     if s == "active"  then return "#4ade80", "rgba(34,197,94,.2)",   "rgba(34,197,94,.35)" end
     if s == "active"  then return "#4ade80","rgba(34,197,94,.2)", "rgba(34,197,94,.35)"   end
     if s == "inactive" then return "#fbbf24", "rgba(251,191,36,.2)", "rgba(251,191,36,.35)" end
     if s == "inactive" then return "#fbbf24","rgba(251,191,36,.2)", "rgba(251,191,36,.35)" end
     if s == "retired"  then return "#94a3b8", "rgba(148,163,184,.2)", "rgba(148,163,184,.35)"end
     if s == "retired"  then return "#94a3b8","rgba(148,163,184,.2)","rgba(148,163,184,.35)" end
     if s == "banned"  then return "#f87171", "rgba(239,68,68,.2)",   "rgba(239,68,68,.35)" end
     if s == "banned"  then return "#f87171","rgba(239,68,68,.2)", "rgba(239,68,68,.35)"   end
     return "#94a3b8", "rgba(148,163,184,.2)", "rgba(148,163,184,.35)"
     return "#94a3b8","rgba(148,163,184,.2)","rgba(148,163,184,.35)"
end
end


-- Fetch team logo URL from Teams table
-- ── Team info + logo helpers ──────────────────────────────────────
 
local teamInfoCache = {}
local function getTeamInfo(teamName)
local function getTeamInfo(teamName)
     if not teamName or teamName == "" then return nil end
     if not teamName or teamName == "" then return nil end
    if teamInfoCache[teamName] then return teamInfoCache[teamName] end
     local rows = cargo.query("Teams",
     local rows = cargo.query("Teams",
         "display_name,image,image_dark",
         "display_name,image,image_dark",
         { where = "name='" .. esc(teamName) .. "'", limit = 1 })
         { where = "name='" .. esc(teamName) .. "'", limit = 1 })
     if rows and #rows > 0 then return rows[1] end
     local result = (rows and #rows > 0) and rows[1] or nil
     return nil
    teamInfoCache[teamName] = result
     return result
end
end


-- Single image wikitext (player photo etc.)
local function logoWikitext(imageFile, size, classes)
local function logoWikitext(imageFile, size, classes)
     if not imageFile or imageFile == "" then return nil end
     if not imageFile or imageFile == "" then return nil end
     size = size or "26px"
     size   = size   or "26px"
     classes = classes or "pd-team-logo-img"
     classes = classes or "pd-team-logo-img"
     return "[[File:" .. imageFile .. "|" .. size .. "|link=|class=" .. classes .. "]]"
     return "[[File:" .. imageFile .. "|" .. size .. "|link=|class=" .. classes .. "]]"
end
end
-- Team logo with light/dark switching
-- Uses your existing logo-lightmode / logo-darkmode CSS classes
local function teamLogoWikitext(ti, size)
    if not ti then return nil end
    size = size or "26px"
    local light = (ti.image      and ti.image      ~= "") and ti.image      or nil
    local dark  = (ti.image_dark and ti.image_dark ~= "") and ti.image_dark or light
    if not light then return nil end
    return "[[File:"..light.."|"..size.."|link=|class=pd-team-logo-img logo-lightmode]]"
        .. "[[File:"..dark .."|"..size.."|link=|class=pd-team-logo-img logo-darkmode]]"
end
-- ── Main ─────────────────────────────────────────────────────────


function p.main(frame)
function p.main(frame)
     local pageName   = mw.title.getCurrentTitle().text
     local pageName = mw.title.getCurrentTitle().text
     local sqlPage   = esc(pageName)
     local sqlPage = esc(pageName)


     -- 1. Player row
     -- ── 1. Player row ─────────────────────────────────────────────
     local pRows = cargo.query("Players",
     local pRows = cargo.query("Players",
         "id,real_name,image,current_team,nationality,status,game," ..
         "id,real_name,image,current_team,nationality,status,game,"
        "birth_date,role,instagram,youtube,twitter,discord,facebook",
        .. "birth_date,role,instagram,youtube,twitter,discord,facebook",
         { where = "_pageName='" .. sqlPage .. "'", limit = 1 })
         { where = "_pageName='" .. sqlPage .. "'", limit = 1 })
     local pl = (pRows and #pRows > 0) and pRows[1] or {}
     local pl         = (pRows and #pRows > 0) and pRows[1] or {}
     local displayName = pl.id or mw.title.getCurrentTitle().subpageText
     local displayName = pl.id or mw.title.getCurrentTitle().subpageText
     local sqlName = esc(displayName)
     local sqlName     = esc(displayName)


     -- 2. Krafton rank
     -- ── 2. Krafton rank ───────────────────────────────────────────
     local rankVal = nil
     local rankVal = nil
     local rRows = cargo.query("Krafton_Rankings", "rank", {
     local rRows = cargo.query("Krafton_Rankings", "rank", {
         where = string.format("(name='%s' OR name='%s') AND type='Player'",
         where = string.format(
            sqlPage, sqlName), limit = 1 })
            "(name='%s' OR name='%s') AND type='Player'", sqlPage, sqlName),
        limit = 1 })
     if rRows and #rRows > 0 then rankVal = "#" .. rRows[1].rank end
     if rRows and #rRows > 0 then rankVal = "#" .. rRows[1].rank end


     -- 3. Earnings
     -- ── 3. All tournaments player appeared in ─────────────────────
    local indEarnings, teamEarnings = 0, 0
    local eRows = cargo.query("PrizeMoney", "SUM(prize)=total", {
        where = string.format("(player='%s' OR player='%s')", sqlPage, sqlName) })
    if eRows and #eRows > 0 then indEarnings = tonumber(eRows[1].total) or 0 end
 
    -- All tournaments player appeared in
     local participation = cargo.query("Tournament_Teams", "tournament,team", {
     local participation = cargo.query("Tournament_Teams", "tournament,team", {
         where = string.format(
         where = string.format(
             "p1='%s' OR p1='%s' OR p2='%s' OR p2='%s' OR " ..
             "p1='%s' OR p1='%s' OR p2='%s' OR p2='%s' OR "
            "p3='%s' OR p3='%s' OR p4='%s' OR p4='%s' OR " ..
            .."p3='%s' OR p3='%s' OR p4='%s' OR p4='%s' OR "
            "p5='%s' OR p5='%s' OR p6='%s' OR p6='%s'",
            .."p5='%s' OR p5='%s' OR p6='%s' OR p6='%s'",
             sqlPage,sqlName, sqlPage,sqlName, sqlPage,sqlName,
             sqlPage,sqlName, sqlPage,sqlName, sqlPage,sqlName,
             sqlPage,sqlName, sqlPage,sqlName, sqlPage,sqlName),
             sqlPage,sqlName, sqlPage,sqlName, sqlPage,sqlName),
         limit = 500 })
         limit = 500 })
    -- ── 4. Earnings ───────────────────────────────────────────────
    local indEarnings, teamEarnings = 0, 0
    local eRows = cargo.query("PrizeMoney", "SUM(prize)=total", {
        where = string.format(
            "(player='%s' OR player='%s')", sqlPage, sqlName) })
    if eRows and #eRows > 0 then
        indEarnings = tonumber(eRows[1].total) or 0
    end


     if participation and #participation > 0 then
     if participation and #participation > 0 then
Line 110: Line 147:
             if r.tournament and r.team then
             if r.tournament and r.team then
                 table.insert(conds, string.format(
                 table.insert(conds, string.format(
                     "(tournament='%s' AND team='%s')", esc(r.tournament), esc(r.team)))
                     "(tournament='%s' AND team='%s')",
                    esc(r.tournament), esc(r.team)))
             end
             end
         end
         end
         if #conds > 0 then
         if #conds > 0 then
             local tPrizes = cargo.query("PrizeMoney", "SUM(prize)=team_total", {
             local tPrizes = cargo.query("PrizeMoney", "SUM(prize)=team_total", {
                 where = "(" .. table.concat(conds, " OR ") .. ")" ..
                 where = "(" .. table.concat(conds, " OR ") .. ")"
                        " AND (player='' OR player IS NULL)" })
                    .. " AND (player='' OR player IS NULL)" })
             if tPrizes and #tPrizes > 0 then
             if tPrizes and #tPrizes > 0 then
                 teamEarnings = tonumber(tPrizes[1].team_total) or 0
                 teamEarnings = tonumber(tPrizes[1].team_total) or 0
Line 124: Line 162:
     local totalEarnings = indEarnings + teamEarnings
     local totalEarnings = indEarnings + teamEarnings


     -- 4. Team history with logos
     -- ── 5. Team history ───────────────────────────────────────────
     local teamHist = cargo.query("Player_Former_Teams",
     local teamHist = cargo.query("Player_Former_Teams",
         "team,join_date,leave_date",
         "team,join_date,leave_date",
         { where = "player_id='" .. sqlPage .. "'",
         { where   = "player_id='" .. sqlPage .. "'",
           orderBy = "join_date DESC", limit = 20 })
           orderBy = "join_date DESC", limit = 20 })
     if #teamHist == 0 then
     if #teamHist == 0 then
         teamHist = cargo.query("Player_Former_Teams",
         teamHist = cargo.query("Player_Former_Teams",
             "team,join_date,leave_date",
             "team,join_date,leave_date",
             { where = "player_id='" .. sqlName .. "'",
             { where   = "player_id='" .. sqlName .. "'",
               orderBy = "join_date DESC", limit = 20 })
               orderBy = "join_date DESC", limit = 20 })
     end
     end


     -- 5a. Individual awards (player= set in PrizeMoney)
     -- ── 6a. Individual awards ─────────────────────────────────────
    -- PrizeMoney rows where player= is set (individual awards)
     local indAwards = cargo.query("PrizeMoney",
     local indAwards = cargo.query("PrizeMoney",
         "tournament,award,prize",
         "tournament,award,prize",
         { where = string.format("player='%s' OR player='%s'", sqlPage, sqlName),
         { where   = string.format(
            "(player='%s' OR player='%s') AND player != ''",
            sqlPage, sqlName),
           orderBy = "tournament DESC", limit = 100 })
           orderBy = "tournament DESC", limit = 100 })


     -- 5b. Team results — all tournaments from participation, with standings + prize
     -- ── 6b. Team tournament results ───────────────────────────────
    -- Source 1: Tournament_Results new invisible table, stores place per team per tournament
    -- Source 2: PrizeMoney — prize and award for team rows (player= empty)
    -- Shows ALL tournaments from participation, even if no prize/placement
 
     local teamResults = {}
     local teamResults = {}
     local seen = {}
     local seenKey    = {}
 
     if participation and #participation > 0 then
     if participation and #participation > 0 then
         local ttConds = {}
        -- Build unique tournament+team pairs
         local pairs_list = {}
         for _, r in ipairs(participation) do
         for _, r in ipairs(participation) do
             if r.tournament and r.team then
             if r.tournament and r.team then
                 local key = r.tournament .. "|" .. r.team
                 local k = r.tournament .. "|" .. r.team
                 if not seen[key] then
                 if not seenKey[k] then
                     seen[key] = true
                     seenKey[k] = true
                     table.insert(ttConds, string.format(
                     table.insert(pairs_list, { tournament = r.tournament, team = r.team })
                        "(tournament='%s' AND team='%s')", esc(r.tournament), esc(r.team)))
                 end
                 end
             end
             end
        end
        -- Build WHERE conditions for batch queries
        local ttConds = {}
        for _, r in ipairs(pairs_list) do
            table.insert(ttConds, string.format(
                "(tournament='%s' AND team='%s')",
                esc(r.tournament), esc(r.team)))
         end
         end


         if #ttConds > 0 then
         if #ttConds > 0 then
            -- Get standings (best stage = lowest rank = Finals > Semis etc.)
             local whereStr = table.concat(ttConds, " OR ")
             local stRows = cargo.query("StageStandings",
                "tournament,stage,team,totalpts,result",
                { where = table.concat(ttConds, " OR "),
                  orderBy = "tournament DESC", limit = 500 })


             -- index standings by tournament|team, keep last stage entry
             -- Get placements from Tournament_Results table
            local stIndex = {}
            local placeIndex = {}
             for _, row in ipairs(stRows) do
            local placeRows = cargo.query("Tournament_Results",
                local k = row.tournament .. "|" .. row.team
                "tournament,team,place",
                stIndex[k] = row -- overwrites so last stage wins
                { where = whereStr, limit = 500 })
             if placeRows then
                for _, row in ipairs(placeRows) do
                    placeIndex[row.tournament .. "|" .. row.team] = tonumber(row.place)
                end
             end
             end


             -- Get prize per tournament+team
             -- Get prize + award from PrizeMoney (team rows only, player= empty)
             local prizeIndex = {}
             local prizeIndex = {}
             local pRows2 = cargo.query("PrizeMoney",
             local prizeRows = cargo.query("PrizeMoney",
                 "tournament,team,SUM(prize)=total,placement,award",
                 "tournament,team,prize,placement,award",
                 { where = "(" .. table.concat(ttConds, " OR ") .. ")" ..
                 { where = "(" .. whereStr .. ")"
                          " AND (player='' OR player IS NULL)",
                    .. " AND (player='' OR player IS NULL)",
                   groupBy = "tournament,team", limit = 500 })
                   limit = 500 })
             for _, row in ipairs(pRows2) do
             if prizeRows then
                prizeIndex[row.tournament .. "|" .. row.team] = row
                for _, row in ipairs(prizeRows) do
                    local k = row.tournament .. "|" .. row.team
                    -- keep highest prize row if multiple entries
                    local existing = prizeIndex[k]
                    local newPrize = tonumber(row.prize) or 0
                    if not existing or newPrize > (existing.prize or 0) then
                        prizeIndex[k] = {
                            prize    = newPrize,
                            placement = row.placement or "",
                            award    = row.award    or "",
                        }
                    end
                end
             end
             end


             -- Build final list
             -- Assemble final list
             for _, r in ipairs(participation) do
             for _, r in ipairs(pairs_list) do
                 if r.tournament and r.team then
                 local k = r.tournament .. "|" .. r.team
                    local k   = r.tournament .. "|" .. r.team
                local pr = prizeIndex[k]
                    local st  = stIndex[k]
                local pl_num = placeIndex[k]
                    local pr  = prizeIndex[k]


                    -- get rank within stage if standings exist
                -- fallback: try to parse place from PrizeMoney placement string
                    local place = nil
                if not pl_num and pr and pr.placement ~= "" then
                    if st then
                    local n = pr.placement:match("(%d+)")
                        local rankQ = cargo.query("StageStandings", "COUNT(team)=cnt", {
                    if n then pl_num = tonumber(n) end
                            where = string.format(
                end
                                "tournament='%s' AND stage='%s' AND totalpts>=%s",
                                esc(st.tournament), esc(st.stage),
                                tonumber(st.totalpts) or 0) })
                        if rankQ and #rankQ > 0 then
                            place = tonumber(rankQ[1].cnt) or nil
                        end
                    end
                    if pr and pr.placement and pr.placement ~= "" then
                        local n = pr.placement:match("(%d+)")
                        if n then place = place or tonumber(n) end
                    end


                    table.insert(teamResults, {
                table.insert(teamResults, {
                        tournament = r.tournament,
                    tournament = r.tournament,
                        team      = r.team,
                    team      = r.team,
                        place      = place,
                    place      = pl_num,
                        award      = pr and pr.award or "",
                    award      = (pr and pr.award     ~= "" and pr.award)     or nil,
                        result    = st and (st.result or "") or "",
                    prize      = (pr and pr.prize      > 0 and pr.prize)     or 0,
                        prize      = pr and (tonumber(pr.total) or 0) or 0,
                })
                    })
                end
             end
             end
         end
         end
     end
     end


     -- Remove duplicates (participation may repeat tournament+team)
     -- Sort: placed entries first (by place), then unplaced
    local dedupedResults, seenR = {}, {}
     table.sort(teamResults, function(a, b)
     for _, r in ipairs(teamResults) do
         if a.place and b.place then return a.place < b.place end
         local k = r.tournament .. "|" .. r.team
        if a.place then return true end
         if not seenR[k] then
         if b.place then return false end
            seenR[k] = true
        return (a.tournament or "") > (b.tournament or "") -- recent first
            table.insert(dedupedResults, r)
    end)
        end
 
    end
     -- ── BUILD HTML ────────────────────────────────────────────────
     teamResults = dedupedResults


    -- ── Build HTML ────────────────────────────────────────────
     local sc, sbg, sborder = statusStyle(pl.status)
     local sc, sbg, sborder = statusStyle(pl.status)
     local initials = displayName:sub(1, 2):upper()
     local initials = displayName:sub(1, 2):upper()
     local root = html.create('div'):addClass('pd-dashboard')
     local root     = html.create('div'):addClass('pd-dashboard')
 
    -- ════ HERO ════


    -- HERO
     local hero = root:tag('div'):addClass('pd-hero')
     local hero = root:tag('div'):addClass('pd-hero')


     -- Photo (circular, image or initials)
     -- Photo
     local photo = hero:tag('div'):addClass('pd-hero-photo')
     local photo = hero:tag('div'):addClass('pd-hero-photo')
     if pl.image and pl.image ~= "" then
     if pl.image and pl.image ~= "" then
         photo:wikitext("[[File:" .. pl.image .. "|100px|link=|class=pd-hero-img]]")
         photo:wikitext("[[File:" .. pl.image
            .. "|96px|link=|class=pd-hero-img]]")
     else
     else
         photo:wikitext(initials)
         photo:wikitext(initials)
     end
     end


    -- Name + tags
     local heroInfo = hero:tag('div'):addClass('pd-hero-info')
     local heroInfo = hero:tag('div'):addClass('pd-hero-info')
     heroInfo:tag('div'):addClass('pd-hero-name'):wikitext(displayName)
     heroInfo:tag('div'):addClass('pd-hero-name'):wikitext(displayName)
Line 256: Line 309:
     if pl.status and pl.status ~= "" then
     if pl.status and pl.status ~= "" then
         tags:tag('span'):addClass('pd-tag')
         tags:tag('span'):addClass('pd-tag')
             :css('color', sc):css('background', sbg):css('border', '1px solid ' .. sborder)
             :css('color', sc):css('background', sbg)
            :css('border', '1px solid ' .. sborder)
             :wikitext('● ' .. pl.status)
             :wikitext('● ' .. pl.status)
     end
     end
     if pl.role     and pl.role ~= ""     then tags:tag('span'):addClass('pd-tag pd-tag-role'):wikitext(pl.role) end
     if pl.role and pl.role ~= "" then
     if pl.game     and pl.game ~= ""     then tags:tag('span'):addClass('pd-tag pd-tag-game'):wikitext(pl.game) end
        tags:tag('span'):addClass('pd-tag pd-tag-role'):wikitext(pl.role)
 
    end
     if pl.game and pl.game ~= "" then
        tags:tag('span'):addClass('pd-tag pd-tag-game'):wikitext(pl.game)
    end
     if rankVal then
     if rankVal then
         heroInfo:tag('div'):addClass('pd-rank-chip')
         heroInfo:tag('div'):addClass('pd-rank-chip')
Line 268: Line 325:
     end
     end


    -- Earnings
     if totalEarnings > 0 then
     if totalEarnings > 0 then
         local er = hero:tag('div'):addClass('pd-hero-right')
         local er = hero:tag('div'):addClass('pd-hero-right')
Line 281: Line 339:
     end
     end


     -- BODY
     -- ════ BODY ════
 
     local body = root:tag('div'):addClass('pd-body')
     local body = root:tag('div'):addClass('pd-body')
     local left = body:tag('div'):addClass('pd-left')
     local left = body:tag('div'):addClass('pd-left')
Line 303: Line 362:
     infoRow('Game', pl.game)
     infoRow('Game', pl.game)


     -- Current team with logo
     -- Current team with logo + display_name
     if pl.current_team and pl.current_team ~= "" then
     if pl.current_team and pl.current_team ~= "" then
         local ti = getTeamInfo(pl.current_team)
         local ti         = getTeamInfo(pl.current_team)
         local teamDisplay = (ti and ti.display_name and ti.display_name ~= "")
         local teamDisplay = (ti and ti.display_name and ti.display_name ~= "")
             and ti.display_name or pl.current_team
             and ti.display_name or pl.current_team
         local teamLogoFile = ti and ti.image and ti.image ~= "" and ti.image or nil
         local logoHtml    = teamLogoWikitext(ti, "22px")


         local row = left:tag('div'):addClass('pd-info-row')
         local row = left:tag('div'):addClass('pd-info-row')
         row:tag('span'):addClass('pd-info-label'):wikitext('Current Team')
         row:tag('span'):addClass('pd-info-label'):wikitext('Current Team')
         local val = row:tag('span'):addClass('pd-info-val')
         local val = row:tag('span'):addClass('pd-info-val')
         local wrap = val:tag('span'):addClass('pd-current-team')
         local wrap = val:tag('span'):addClass('pd-current-team')
         if teamLogoFile then
         if logoHtml then wrap:wikitext(logoHtml) end
            wrap:wikitext("[[File:" .. teamLogoFile .. "|22px|link=|class=pd-current-team-logo]]")
        end
         wrap:tag('span'):addClass('pd-current-team-name')
         wrap:tag('span'):addClass('pd-current-team-name')
             :wikitext("[[" .. pl.current_team .. "|" .. teamDisplay .. "]]")
             :wikitext("[[" .. pl.current_team .. "|" .. teamDisplay .. "]]")
Line 325: Line 382:
     -- Socials
     -- Socials
     local socials = {
     local socials = {
         {k='instagram', file='Icon_instagram.png', base='https://instagram.com/'},
         { k='instagram', file='Icon_instagram.png', base='https://instagram.com/' },
         {k='twitter',  file='Icon_twitter.png',  base='https://twitter.com/'},
         { k='twitter',  file='Icon_twitter.png',  base='https://twitter.com/'   },
         {k='youtube',  file='Icon_youtube.png',  base='https://youtube.com/'},
         { k='youtube',  file='Icon_youtube.png',  base='https://youtube.com/'   },
         {k='discord',  file='Icon_discord.png',  base='https://discord.gg/'},
         { k='discord',  file='Icon_discord.png',  base='https://discord.gg/'   },
         {k='facebook',  file='Icon_facebook.png',  base='https://facebook.com/'},
         { k='facebook',  file='Icon_facebook.png',  base='https://facebook.com/' },
     }
     }
     local socDiv = left:tag('div'):addClass('pd-socials')
     local socDiv = left:tag('div'):addClass('pd-socials')
Line 336: Line 393:
         if v and v ~= "" then
         if v and v ~= "" then
             local url = v:match("^https?://") and v or (s.base .. v)
             local url = v:match("^https?://") and v or (s.base .. v)
             socDiv:wikitext("[[File:"..s.file.."|24px|link="..url.."|class=social-img]]")
             socDiv:wikitext("[[File:" .. s.file
                .. "|32px|link=" .. url .. "|class=social-img]]")
         end
         end
     end
     end
Line 342: Line 400:
     -- Team history with logos
     -- Team history with logos
     if #teamHist > 0 then
     if #teamHist > 0 then
         left:tag('div'):addClass('pd-section-title pd-section-gap'):wikitext('Team History')
         left:tag('div'):addClass('pd-section-title pd-section-gap')
            :wikitext('Team History')
         for _, h in ipairs(teamHist) do
         for _, h in ipairs(teamHist) do
             local ti = getTeamInfo(h.team or "")
             local ti     = getTeamInfo(h.team or "")
             local tr = left:tag('div'):addClass('pd-team-row')
            local logoHtml = teamLogoWikitext(ti, "24px")
             if ti and ti.image and ti.image ~= "" then
             local tr     = left:tag('div'):addClass('pd-team-row')
                 tr:wikitext("[[File:"..ti.image.."|26px|link=|class=pd-team-logo-img]]")
 
             if logoHtml then
                 tr:wikitext(logoHtml)
             else
             else
                 tr:tag('div'):addClass('pd-team-logo-fb')
                 tr:tag('div'):addClass('pd-team-logo-fb')
                     :wikitext((h.team or "?"):sub(1, 3):upper())
                     :wikitext((h.team or "?"):sub(1, 3):upper())
             end
             end
             tr:tag('div'):addClass('pd-team-name')
             tr:tag('div'):addClass('pd-team-name')
                 :wikitext("[[" .. (h.team or "") .. "]]")
                 :wikitext("[[" .. (h.team or "") .. "]]")
             tr:tag('div'):addClass('pd-team-dates')
             tr:tag('div'):addClass('pd-team-dates')
                 :wikitext(fmtDate(h.join_date) .. " – " ..
                 :wikitext(fmtDate(h.join_date) .. " – "
                    (h.leave_date and h.leave_date ~= ""
                    .. (h.leave_date and h.leave_date ~= ""
                         and fmtDate(h.leave_date) or "Now"))
                         and fmtDate(h.leave_date) or "Now"))
         end
         end
     end
     end


     -- RIGHT PANEL
     -- ════ RIGHT PANEL ════
 
     local right = body:tag('div'):addClass('pd-right')
     local right = body:tag('div'):addClass('pd-right')


Line 387: Line 450:
     local sec2 = right:tag('div'):addClass('pd-prize-section')
     local sec2 = right:tag('div'):addClass('pd-prize-section')
     sec2:tag('div'):addClass('pd-table-title'):wikitext('🎖 Team Tournament Results')
     sec2:tag('div'):addClass('pd-table-title'):wikitext('🎖 Team Tournament Results')
     if #teamResults > 0 then
     if #teamResults > 0 then
         local tbl2 = sec2:tag('table'):addClass('pd-tourn-table')
         local tbl2 = sec2:tag('table'):addClass('pd-tourn-table')
Line 392: Line 456:
         hdr2:tag('th'):wikitext('Tournament')
         hdr2:tag('th'):wikitext('Tournament')
         hdr2:tag('th'):wikitext('Team')
         hdr2:tag('th'):wikitext('Team')
         hdr2:tag('th'):wikitext('Place')
         hdr2:tag('th'):wikitext('Place / Award') -- combined heading
        hdr2:tag('th'):wikitext('Result')
         hdr2:tag('th'):wikitext('Prize')
         hdr2:tag('th'):wikitext('Prize')
         for _, t in ipairs(teamResults) do
         for _, t in ipairs(teamResults) do
             local tr2 = tbl2:tag('tr')
             local tr2 = tbl2:tag('tr')
Line 400: Line 464:
             tr2:tag('td'):wikitext("[[" .. t.team .. "]]")
             tr2:tag('td'):wikitext("[[" .. t.team .. "]]")


             -- Place chip or award name
             -- Place chip OR award badge OR dash
             local placeTd = tr2:tag('td')
             local placeTd = tr2:tag('td')
             if t.place then
             if t.place then
Line 408: Line 472:
                         or 'pd-place-chip place-other'
                         or 'pd-place-chip place-other'
                 placeTd:tag('span'):addClass(pc):wikitext(tostring(t.place))
                 placeTd:tag('span'):addClass(pc):wikitext(tostring(t.place))
             elseif t.award and t.award ~= "" then
             elseif t.award then
                 placeTd:tag('span'):addClass('pd-award-badge'):wikitext(t.award)
                 placeTd:tag('span'):addClass('pd-award-badge'):wikitext(t.award)
             else
             else
Line 414: Line 478:
             end
             end


             local res = (t.result or ""):lower()
             tr2:tag('td')
            local resTd = tr2:tag('td')
                :addClass(t.prize > 0 and 'pd-prize-val' or '')
            if    res == 'q'  then resTd:tag('span'):addClass('pd-result-q'):wikitext('Qualified')
            elseif res == 'q2' then resTd:tag('span'):addClass('pd-result-q'):wikitext('Qualified')
            elseif res == 'e'  then resTd:tag('span'):addClass('pd-result-e'):wikitext('Eliminated')
            else resTd:wikitext("—") end
 
            tr2:tag('td'):addClass(t.prize > 0 and 'pd-prize-val' or '')
                 :wikitext(t.prize > 0 and fmtCurrency(t.prize) or "—")
                 :wikitext(t.prize > 0 and fmtCurrency(t.prize) or "—")
         end
         end
     else
     else
         sec2:tag('div'):addClass('pd-empty'):wikitext('No tournament data found.')
         sec2:tag('div'):addClass('pd-empty')
            :wikitext('No tournament data found.')
     end
     end



Revision as of 04:17, 7 March 2026

Documentation for this module may be created at Module:PlayerDashboard/doc

-- ================================================================
-- Module:PlayerDashboard v3
-- Full rewrite with all fixes:
--   • Circular photo, vertically centered hero
--   • Current team: logo (light/dark) + display_name
--   • Team history: logos (light/dark)
--   • Individual awards table (from PrizeMoney where player= set)
--   • Team results table:
--       - Place from new Tournament_Results cargo table
--       - Prize/award from PrizeMoney (team rows)
--       - Shows ALL tournaments player appeared in
--       - Place/Award column shows chip OR award badge
--   • Flag emojis for nationality
--   • Dark mode link + badge overrides via CSS variables
--   • Mobile friendly
-- ================================================================

local p     = {}
local cargo = mw.ext.cargo
local html  = mw.html
local lang  = mw.getContentLanguage()

-- ── Helpers ──────────────────────────────────────────────────────

local function esc(s)
    if not s then return "" end
    return s:gsub("\\", "\\\\"):gsub("'", "\\'")
end

local function fmtCurrency(n)
    if not n or n == 0 then return "—" end
    local s    = tostring(math.floor(n))
    local l3   = s:sub(-3)
    local rest = s:sub(1, -4)
    local fmt  = rest:reverse():gsub("(%d%d)", "%1,"):reverse()
    if fmt ~= "" then l3 = "," .. l3 end
    return "₹ " .. fmt .. l3
end

local function fmtDate(d)
    if not d or d == "" then return "?" end
    return lang:formatDate("M y", d)
end

-- Flag emoji map
local FLAGS = {
    india="🇮🇳", bangladesh="🇧🇩", pakistan="🇵🇰", nepal="🇳🇵",
    srilanka="🇱🇰", myanmar="🇲🇲", indonesia="🇮🇩", thailand="🇹🇭",
    malaysia="🇲🇾", philippines="🇵🇭", vietnam="🇻🇳", singapore="🇸🇬",
    china="🇨🇳", japan="🇯🇵", korea="🇰🇷", usa="🇺🇸",
    uk="🇬🇧", germany="🇩🇪", brazil="🇧🇷", australia="🇦🇺"
}
local function getFlag(country)
    if not country or country == "" then return "" end
    local key = country:lower():gsub("%s+",""):gsub("-","")
    return (FLAGS[key] or "") .. " "
end

local function statusStyle(s)
    s = (s or ""):lower()
    if s == "active"   then return "#4ade80","rgba(34,197,94,.2)",  "rgba(34,197,94,.35)"   end
    if s == "inactive" then return "#fbbf24","rgba(251,191,36,.2)", "rgba(251,191,36,.35)"  end
    if s == "retired"  then return "#94a3b8","rgba(148,163,184,.2)","rgba(148,163,184,.35)" end
    if s == "banned"   then return "#f87171","rgba(239,68,68,.2)",  "rgba(239,68,68,.35)"   end
    return "#94a3b8","rgba(148,163,184,.2)","rgba(148,163,184,.35)"
end

-- ── Team info + logo helpers ──────────────────────────────────────

local teamInfoCache = {}
local function getTeamInfo(teamName)
    if not teamName or teamName == "" then return nil end
    if teamInfoCache[teamName] then return teamInfoCache[teamName] end
    local rows = cargo.query("Teams",
        "display_name,image,image_dark",
        { where = "name='" .. esc(teamName) .. "'", limit = 1 })
    local result = (rows and #rows > 0) and rows[1] or nil
    teamInfoCache[teamName] = result
    return result
end

-- Single image wikitext (player photo etc.)
local function logoWikitext(imageFile, size, classes)
    if not imageFile or imageFile == "" then return nil end
    size    = size    or "26px"
    classes = classes or "pd-team-logo-img"
    return "[[File:" .. imageFile .. "|" .. size .. "|link=|class=" .. classes .. "]]"
end

-- Team logo with light/dark switching
-- Uses your existing logo-lightmode / logo-darkmode CSS classes
local function teamLogoWikitext(ti, size)
    if not ti then return nil end
    size = size or "26px"
    local light = (ti.image      and ti.image      ~= "") and ti.image      or nil
    local dark  = (ti.image_dark and ti.image_dark ~= "") and ti.image_dark or light
    if not light then return nil end
    return "[[File:"..light.."|"..size.."|link=|class=pd-team-logo-img logo-lightmode]]"
        .. "[[File:"..dark .."|"..size.."|link=|class=pd-team-logo-img logo-darkmode]]"
end

-- ── Main ─────────────────────────────────────────────────────────

function p.main(frame)
    local pageName = mw.title.getCurrentTitle().text
    local sqlPage  = esc(pageName)

    -- ── 1. Player row ─────────────────────────────────────────────
    local pRows = cargo.query("Players",
        "id,real_name,image,current_team,nationality,status,game,"
        .. "birth_date,role,instagram,youtube,twitter,discord,facebook",
        { where = "_pageName='" .. sqlPage .. "'", limit = 1 })
    local pl          = (pRows and #pRows > 0) and pRows[1] or {}
    local displayName = pl.id or mw.title.getCurrentTitle().subpageText
    local sqlName     = esc(displayName)

    -- ── 2. Krafton rank ───────────────────────────────────────────
    local rankVal = nil
    local rRows = cargo.query("Krafton_Rankings", "rank", {
        where = string.format(
            "(name='%s' OR name='%s') AND type='Player'", sqlPage, sqlName),
        limit = 1 })
    if rRows and #rRows > 0 then rankVal = "#" .. rRows[1].rank end

    -- ── 3. All tournaments player appeared in ─────────────────────
    local participation = cargo.query("Tournament_Teams", "tournament,team", {
        where = string.format(
            "p1='%s' OR p1='%s' OR p2='%s' OR p2='%s' OR "
            .."p3='%s' OR p3='%s' OR p4='%s' OR p4='%s' OR "
            .."p5='%s' OR p5='%s' OR p6='%s' OR p6='%s'",
            sqlPage,sqlName, sqlPage,sqlName, sqlPage,sqlName,
            sqlPage,sqlName, sqlPage,sqlName, sqlPage,sqlName),
        limit = 500 })

    -- ── 4. Earnings ───────────────────────────────────────────────
    local indEarnings, teamEarnings = 0, 0
    local eRows = cargo.query("PrizeMoney", "SUM(prize)=total", {
        where = string.format(
            "(player='%s' OR player='%s')", sqlPage, sqlName) })
    if eRows and #eRows > 0 then
        indEarnings = tonumber(eRows[1].total) or 0
    end

    if participation and #participation > 0 then
        local conds = {}
        for _, r in ipairs(participation) do
            if r.tournament and r.team then
                table.insert(conds, string.format(
                    "(tournament='%s' AND team='%s')",
                    esc(r.tournament), esc(r.team)))
            end
        end
        if #conds > 0 then
            local tPrizes = cargo.query("PrizeMoney", "SUM(prize)=team_total", {
                where = "(" .. table.concat(conds, " OR ") .. ")"
                    .. " AND (player='' OR player IS NULL)" })
            if tPrizes and #tPrizes > 0 then
                teamEarnings = tonumber(tPrizes[1].team_total) or 0
            end
        end
    end
    local totalEarnings = indEarnings + teamEarnings

    -- ── 5. Team history ───────────────────────────────────────────
    local teamHist = cargo.query("Player_Former_Teams",
        "team,join_date,leave_date",
        { where   = "player_id='" .. sqlPage .. "'",
          orderBy = "join_date DESC", limit = 20 })
    if #teamHist == 0 then
        teamHist = cargo.query("Player_Former_Teams",
            "team,join_date,leave_date",
            { where   = "player_id='" .. sqlName .. "'",
              orderBy = "join_date DESC", limit = 20 })
    end

    -- ── 6a. Individual awards ─────────────────────────────────────
    -- PrizeMoney rows where player= is set (individual awards)
    local indAwards = cargo.query("PrizeMoney",
        "tournament,award,prize",
        { where   = string.format(
            "(player='%s' OR player='%s') AND player != ''",
            sqlPage, sqlName),
          orderBy = "tournament DESC", limit = 100 })

    -- ── 6b. Team tournament results ───────────────────────────────
    -- Source 1: Tournament_Results — new invisible table, stores place per team per tournament
    -- Source 2: PrizeMoney — prize and award for team rows (player= empty)
    -- Shows ALL tournaments from participation, even if no prize/placement

    local teamResults = {}
    local seenKey     = {}

    if participation and #participation > 0 then
        -- Build unique tournament+team pairs
        local pairs_list = {}
        for _, r in ipairs(participation) do
            if r.tournament and r.team then
                local k = r.tournament .. "|" .. r.team
                if not seenKey[k] then
                    seenKey[k] = true
                    table.insert(pairs_list, { tournament = r.tournament, team = r.team })
                end
            end
        end

        -- Build WHERE conditions for batch queries
        local ttConds = {}
        for _, r in ipairs(pairs_list) do
            table.insert(ttConds, string.format(
                "(tournament='%s' AND team='%s')",
                esc(r.tournament), esc(r.team)))
        end

        if #ttConds > 0 then
            local whereStr = table.concat(ttConds, " OR ")

            -- Get placements from Tournament_Results table
            local placeIndex = {}
            local placeRows = cargo.query("Tournament_Results",
                "tournament,team,place",
                { where = whereStr, limit = 500 })
            if placeRows then
                for _, row in ipairs(placeRows) do
                    placeIndex[row.tournament .. "|" .. row.team] = tonumber(row.place)
                end
            end

            -- Get prize + award from PrizeMoney (team rows only, player= empty)
            local prizeIndex = {}
            local prizeRows = cargo.query("PrizeMoney",
                "tournament,team,prize,placement,award",
                { where = "(" .. whereStr .. ")"
                    .. " AND (player='' OR player IS NULL)",
                  limit = 500 })
            if prizeRows then
                for _, row in ipairs(prizeRows) do
                    local k = row.tournament .. "|" .. row.team
                    -- keep highest prize row if multiple entries
                    local existing = prizeIndex[k]
                    local newPrize = tonumber(row.prize) or 0
                    if not existing or newPrize > (existing.prize or 0) then
                        prizeIndex[k] = {
                            prize     = newPrize,
                            placement = row.placement or "",
                            award     = row.award     or "",
                        }
                    end
                end
            end

            -- Assemble final list
            for _, r in ipairs(pairs_list) do
                local k  = r.tournament .. "|" .. r.team
                local pr = prizeIndex[k]
                local pl_num = placeIndex[k]

                -- fallback: try to parse place from PrizeMoney placement string
                if not pl_num and pr and pr.placement ~= "" then
                    local n = pr.placement:match("(%d+)")
                    if n then pl_num = tonumber(n) end
                end

                table.insert(teamResults, {
                    tournament = r.tournament,
                    team       = r.team,
                    place      = pl_num,
                    award      = (pr and pr.award     ~= "" and pr.award)     or nil,
                    prize      = (pr and pr.prize      > 0  and pr.prize)     or 0,
                })
            end
        end
    end

    -- Sort: placed entries first (by place), then unplaced
    table.sort(teamResults, function(a, b)
        if a.place and b.place then return a.place < b.place end
        if a.place then return true end
        if b.place then return false end
        return (a.tournament or "") > (b.tournament or "") -- recent first
    end)

    -- ── BUILD HTML ────────────────────────────────────────────────

    local sc, sbg, sborder = statusStyle(pl.status)
    local initials = displayName:sub(1, 2):upper()
    local root     = html.create('div'):addClass('pd-dashboard')

    -- ════ HERO ════

    local hero = root:tag('div'):addClass('pd-hero')

    -- Photo
    local photo = hero:tag('div'):addClass('pd-hero-photo')
    if pl.image and pl.image ~= "" then
        photo:wikitext("[[File:" .. pl.image
            .. "|96px|link=|class=pd-hero-img]]")
    else
        photo:wikitext(initials)
    end

    -- Name + tags
    local heroInfo = hero:tag('div'):addClass('pd-hero-info')
    heroInfo:tag('div'):addClass('pd-hero-name'):wikitext(displayName)
    if pl.real_name and pl.real_name ~= "" then
        heroInfo:tag('div'):addClass('pd-hero-realname'):wikitext(pl.real_name)
    end

    local tags = heroInfo:tag('div'):addClass('pd-hero-tags')
    if pl.status and pl.status ~= "" then
        tags:tag('span'):addClass('pd-tag')
            :css('color', sc):css('background', sbg)
            :css('border', '1px solid ' .. sborder)
            :wikitext('● ' .. pl.status)
    end
    if pl.role and pl.role ~= "" then
        tags:tag('span'):addClass('pd-tag pd-tag-role'):wikitext(pl.role)
    end
    if pl.game and pl.game ~= "" then
        tags:tag('span'):addClass('pd-tag pd-tag-game'):wikitext(pl.game)
    end
    if rankVal then
        heroInfo:tag('div'):addClass('pd-rank-chip')
            :tag('span'):wikitext('Krafton Rank'):done()
            :tag('b'):wikitext(rankVal)
    end

    -- Earnings
    if totalEarnings > 0 then
        local er = hero:tag('div'):addClass('pd-hero-right')
        er:tag('div'):addClass('pd-earnings-label'):wikitext('Approx. Earnings')
        er:tag('div'):addClass('pd-earnings-val'):wikitext(fmtCurrency(totalEarnings))
        local bd = er:tag('div'):addClass('pd-earnings-breakdown')
        bd:tag('div')
            :tag('div'):addClass('pd-earn-sub-label'):wikitext('Team'):done()
            :tag('div'):addClass('pd-earn-sub-val'):wikitext(fmtCurrency(teamEarnings))
        bd:tag('div')
            :tag('div'):addClass('pd-earn-sub-label'):wikitext('Individual'):done()
            :tag('div'):addClass('pd-earn-sub-val'):wikitext(fmtCurrency(indEarnings))
    end

    -- ════ BODY ════

    local body = root:tag('div'):addClass('pd-body')
    local left = body:tag('div'):addClass('pd-left')
    left:tag('div'):addClass('pd-section-title'):wikitext('Player Info')

    local function infoRow(label, val)
        if not val or val == "" then return end
        left:tag('div'):addClass('pd-info-row')
            :tag('span'):addClass('pd-info-label'):wikitext(label):done()
            :tag('span'):addClass('pd-info-val'):wikitext(val)
    end

    infoRow('Real Name', pl.real_name)
    if pl.birth_date and pl.birth_date ~= "" then
        infoRow('Born', lang:formatDate("d F Y", pl.birth_date))
    end
    if pl.nationality and pl.nationality ~= "" then
        infoRow('Nationality', getFlag(pl.nationality) .. pl.nationality)
    end
    infoRow('Role', pl.role)
    infoRow('Game', pl.game)

    -- Current team with logo + display_name
    if pl.current_team and pl.current_team ~= "" then
        local ti          = getTeamInfo(pl.current_team)
        local teamDisplay = (ti and ti.display_name and ti.display_name ~= "")
            and ti.display_name or pl.current_team
        local logoHtml    = teamLogoWikitext(ti, "22px")

        local row  = left:tag('div'):addClass('pd-info-row')
        row:tag('span'):addClass('pd-info-label'):wikitext('Current Team')
        local val  = row:tag('span'):addClass('pd-info-val')
        local wrap = val:tag('span'):addClass('pd-current-team')
        if logoHtml then wrap:wikitext(logoHtml) end
        wrap:tag('span'):addClass('pd-current-team-name')
            :wikitext("[[" .. pl.current_team .. "|" .. teamDisplay .. "]]")
    end

    if rankVal then infoRow('Krafton Rank', rankVal) end

    -- Socials
    local socials = {
        { k='instagram', file='Icon_instagram.png', base='https://instagram.com/' },
        { k='twitter',   file='Icon_twitter.png',   base='https://twitter.com/'   },
        { k='youtube',   file='Icon_youtube.png',   base='https://youtube.com/'   },
        { k='discord',   file='Icon_discord.png',   base='https://discord.gg/'    },
        { k='facebook',  file='Icon_facebook.png',  base='https://facebook.com/'  },
    }
    local socDiv = left:tag('div'):addClass('pd-socials')
    for _, s in ipairs(socials) do
        local v = pl[s.k]
        if v and v ~= "" then
            local url = v:match("^https?://") and v or (s.base .. v)
            socDiv:wikitext("[[File:" .. s.file
                .. "|32px|link=" .. url .. "|class=social-img]]")
        end
    end

    -- Team history with logos
    if #teamHist > 0 then
        left:tag('div'):addClass('pd-section-title pd-section-gap')
            :wikitext('Team History')
        for _, h in ipairs(teamHist) do
            local ti      = getTeamInfo(h.team or "")
            local logoHtml = teamLogoWikitext(ti, "24px")
            local tr      = left:tag('div'):addClass('pd-team-row')

            if logoHtml then
                tr:wikitext(logoHtml)
            else
                tr:tag('div'):addClass('pd-team-logo-fb')
                    :wikitext((h.team or "?"):sub(1, 3):upper())
            end

            tr:tag('div'):addClass('pd-team-name')
                :wikitext("[[" .. (h.team or "") .. "]]")
            tr:tag('div'):addClass('pd-team-dates')
                :wikitext(fmtDate(h.join_date) .. " – "
                    .. (h.leave_date and h.leave_date ~= ""
                        and fmtDate(h.leave_date) or "Now"))
        end
    end

    -- ════ RIGHT PANEL ════

    local right = body:tag('div'):addClass('pd-right')

    -- Individual awards table
    if indAwards and #indAwards > 0 then
        local sec = right:tag('div'):addClass('pd-prize-section')
        sec:tag('div'):addClass('pd-table-title'):wikitext('🏆 Individual Awards')
        local tbl = sec:tag('table'):addClass('pd-tourn-table')
        local hdr = tbl:tag('tr')
        hdr:tag('th'):wikitext('Tournament')
        hdr:tag('th'):wikitext('Award')
        hdr:tag('th'):wikitext('Prize')
        for _, a in ipairs(indAwards) do
            local tr = tbl:tag('tr')
            tr:tag('td'):wikitext("[[" .. (a.tournament or "") .. "]]")
            tr:tag('td'):tag('span'):addClass('pd-award-badge')
                :wikitext(a.award and a.award ~= "" and a.award or "Award")
            tr:tag('td'):addClass('pd-prize-val')
                :wikitext(tonumber(a.prize) and tonumber(a.prize) > 0
                    and fmtCurrency(tonumber(a.prize)) or "—")
        end
    end

    -- Team results table
    local sec2 = right:tag('div'):addClass('pd-prize-section')
    sec2:tag('div'):addClass('pd-table-title'):wikitext('🎖 Team Tournament Results')

    if #teamResults > 0 then
        local tbl2 = sec2:tag('table'):addClass('pd-tourn-table')
        local hdr2 = tbl2:tag('tr')
        hdr2:tag('th'):wikitext('Tournament')
        hdr2:tag('th'):wikitext('Team')
        hdr2:tag('th'):wikitext('Place / Award')  -- combined heading
        hdr2:tag('th'):wikitext('Prize')

        for _, t in ipairs(teamResults) do
            local tr2 = tbl2:tag('tr')
            tr2:tag('td'):wikitext("[[" .. t.tournament .. "]]")
            tr2:tag('td'):wikitext("[[" .. t.team .. "]]")

            -- Place chip OR award badge OR dash
            local placeTd = tr2:tag('td')
            if t.place then
                local pc = t.place == 1 and 'pd-place-chip place-1'
                        or t.place == 2 and 'pd-place-chip place-2'
                        or t.place == 3 and 'pd-place-chip place-3'
                        or 'pd-place-chip place-other'
                placeTd:tag('span'):addClass(pc):wikitext(tostring(t.place))
            elseif t.award then
                placeTd:tag('span'):addClass('pd-award-badge'):wikitext(t.award)
            else
                placeTd:wikitext("—")
            end

            tr2:tag('td')
                :addClass(t.prize > 0 and 'pd-prize-val' or '')
                :wikitext(t.prize > 0 and fmtCurrency(t.prize) or "—")
        end
    else
        sec2:tag('div'):addClass('pd-empty')
            :wikitext('No tournament data found.')
    end

    -- Stats placeholder
    right:tag('div'):addClass('pd-stats-placeholder')
        :tag('b'):wikitext('📊 Performance Stats'):done()
        :wikitext('Coming soon — per-match stats will appear here.')

    return tostring(root)
end

return p