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 v3
-- Module:PlayerDashboard v4
-- Changes from v3:
--  • Krafton Rank moved from hero tags to left info panel
--  • Earnings on mobile: stacks below name, not floating right
--  • New "Teammates" section in left panel from Tournament_Teams
-- ================================================================
-- ================================================================


Line 18: Line 22:
     if not n or n == 0 then return "—" end
     if not n or n == 0 then return "—" end
     local num = math.floor(tonumber(n) or 0)
     local num = math.floor(tonumber(n) or 0)
    -- Convert to Indian number format: last 3 digits, then groups of 2
     local s   = tostring(num)
     local s = tostring(num)
     if #s <= 3 then return "₹ " .. s end
     if #s <= 3 then
     local result   = s:sub(-3)
        return "₹ " .. s
     local remaining = s:sub(1, -4)
    end
     local result = s:sub(-3)           -- last 3 digits always together
     local remaining = s:sub(1, -4)     -- everything before last 3
    -- Group remaining digits in pairs from the right
     while #remaining > 2 do
     while #remaining > 2 do
         result = remaining:sub(-2) .. "," .. result
         result   = remaining:sub(-2) .. "," .. result
         remaining = remaining:sub(1, -3)
         remaining = remaining:sub(1, -3)
     end
     end
     result = remaining .. "," .. result
     return "₹ " .. remaining .. "," .. result
    return "₹ " .. result
end
end


Line 39: Line 38:
end
end


-- Matches getTierClass used in Module:Team and Module:Tournament
local function getTierClass(tier)
local function getTierClass(tier)
     if not tier then return "tier-def" end
     if not tier then return "tier-def" end
Line 50: Line 48:
end
end


-- Flag emoji map
local FLAGS = {
local FLAGS = {
     india="🇮🇳", bangladesh="🇧🇩", pakistan="🇵🇰", nepal="🇳🇵",
     india="🇮🇳", bangladesh="🇧🇩", pakistan="🇵🇰", nepal="🇳🇵",
Line 87: Line 84:
end
end


-- Team logo: outputs both light + dark versions, CSS switches them
-- Outputs both light + dark logo, CSS switches via logo-lightmode/logo-darkmode
-- Uses your existing .logo-lightmode / .logo-darkmode classes
local function teamLogoWikitext(ti, size)
local function teamLogoWikitext(ti, size)
     if not ti then return nil end
     if not ti then return nil end
Line 99: Line 95:
end
end


-- Place badge — matches achievements table style exactly
-- Place badge matching achievements table style
-- (uses .place-badge, .place-1, .place-2, .place-3, .place-def)
local function renderPlaceBadge(td, placeNum)
local function renderPlaceBadge(td, placeNum)
     local badgeClass = "place-def"
     local badgeClass = "place-def"
Line 135: Line 130:


     -- ── 3. All tournaments player appeared in ─────────────────────
     -- ── 3. All tournaments player appeared in ─────────────────────
     local participation = cargo.query("Tournament_Teams", "tournament,team", {
     local participation = cargo.query("Tournament_Teams",
         where = string.format(
        "tournament,team,p1,p2,p3,p4,p5,p6",
         { 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 "
Line 145: Line 141:
     participation = participation or {}
     participation = participation or {}


     -- Quick lookup: tournament -> team the player was on (for individual awards)
     -- Tournament -> team lookup (for individual awards team column)
     local tournTeamMap = {}
     local tournTeamMap = {}
     for _, r in ipairs(participation) do
     for _, r in ipairs(participation) do
Line 153: Line 149:
     end
     end


     -- ── 4. Earnings ───────────────────────────────────────────────
     -- ── 4. Teammates ──────────────────────────────────────────────
    -- Count how many tournaments each co-player appeared in with this player
    -- Skip coach/analyst — only p1-p6. Skip the player himself.
    local teammateCount = {}
    local slots = {"p1","p2","p3","p4","p5","p6"}
    for _, r in ipairs(participation) do
        for _, slot in ipairs(slots) do
            local name = r[slot]
            if name and name ~= "" and name ~= pageName and name ~= displayName then
                teammateCount[name] = (teammateCount[name] or 0) + 1
            end
        end
    end
 
    -- Sort teammates by frequency desc, take top 10
    local teammates = {}
    for name, count in pairs(teammateCount) do
        table.insert(teammates, { name = name, count = count })
    end
    table.sort(teammates, function(a, b) return a.count > b.count end)
    if #teammates > 10 then
        local trimmed = {}
        for i = 1, 10 do trimmed[i] = teammates[i] end
        teammates = trimmed
    end
 
    -- ── 5. Earnings ───────────────────────────────────────────────
     local indEarnings, teamEarnings = 0, 0
     local indEarnings, teamEarnings = 0, 0


Line 183: Line 205:
     local totalEarnings = indEarnings + teamEarnings
     local totalEarnings = indEarnings + teamEarnings


     -- ── 5. Team history ───────────────────────────────────────────
     -- ── 6. 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",
Line 196: Line 218:
     teamHist = teamHist or {}
     teamHist = teamHist or {}


     -- ── 6a. Individual awards ─────────────────────────────────────
     -- ── 7a. Individual awards ─────────────────────────────────────
     local indAwards = cargo.query("PrizeMoney",
     local indAwards = cargo.query("PrizeMoney",
         "tournament,award,prize",
         "tournament,award,prize",
Line 205: Line 227:
     indAwards = indAwards or {}
     indAwards = indAwards or {}


     -- ── 6b. Team tournament results ───────────────────────────────
     -- ── 7b. Team tournament results ───────────────────────────────
     local teamResults = {}
     local teamResults = {}
     local seenKey    = {}
     local seenKey    = {}
Line 230: Line 252:
             local whereStr = table.concat(ttConds, " OR ")
             local whereStr = table.concat(ttConds, " OR ")


            -- Placements from Tournament_Results table
             local placeIndex = {}
             local placeIndex = {}
             local placeRows  = cargo.query("Tournament_Results",
             local placeRows  = cargo.query("Tournament_Results",
Line 241: Line 262:
             end
             end


            -- Prize + award from PrizeMoney (team rows only, no player)
             local prizeIndex = {}
             local prizeIndex = {}
             local prizeRows  = cargo.query("PrizeMoney",
             local prizeRows  = cargo.query("PrizeMoney",
Line 267: Line 287:
                 local pr    = prizeIndex[k]
                 local pr    = prizeIndex[k]
                 local pl_num = placeIndex[k]
                 local pl_num = placeIndex[k]
                -- fallback: parse number from PrizeMoney placement string
                 if not pl_num and pr and pr.placement ~= "" then
                 if not pl_num and pr and pr.placement ~= "" then
                     local n = pr.placement:match("(%d+)")
                     local n = pr.placement:match("(%d+)")
                     if n then pl_num = tonumber(n) end
                     if n then pl_num = tonumber(n) end
                 end
                 end
                 table.insert(teamResults, {
                 table.insert(teamResults, {
                     tournament = r.tournament,
                     tournament = r.tournament,
Line 285: Line 302:
     end
     end


    -- Sort: placed first (ascending), then unplaced by tournament name desc
     table.sort(teamResults, function(a, b)
     table.sort(teamResults, function(a, b)
         if a.place and b.place then return a.place < b.place end
         if a.place and b.place then return a.place < b.place end
Line 293: Line 309:
     end)
     end)


     -- ── 7. Batch-fetch tournament metadata (tier + end_date) ──────
     -- ── 8. Tournament metadata ────────────────────────────────────
    -- Uses end_date, exactly like the achievements table does
     local allTournNames = {}
     local allTournNames = {}
     local tournSeen    = {}
     local tournSeen    = {}
Line 306: Line 321:
     for _, t in ipairs(teamResults) do addTourn(t.tournament) end
     for _, t in ipairs(teamResults) do addTourn(t.tournament) end


     local tournMeta = {}  -- [tournament name] = { tier, date }
     local tournMeta = {}
     if #allTournNames > 0 then
     if #allTournNames > 0 then
         local metaRows = cargo.query("Tournaments",
         local metaRows = cargo.query("Tournaments", "name,tier,end_date", {
            "name,tier,end_date",
             where = "name IN (" .. table.concat(allTournNames, ",") .. ")",
             { where = "name IN (" .. table.concat(allTournNames, ",") .. ")",
            limit = 200 })
              limit = 200 })
         if metaRows then
         if metaRows then
             for _, row in ipairs(metaRows) do
             for _, row in ipairs(metaRows) do
Line 326: Line 340:


     -- ════ HERO ════
     -- ════ HERO ════
    -- Layout: [photo] [name + realname + tags] [earnings — hidden on mobile]
    -- Krafton rank is NO LONGER here — moved to left info panel


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


    -- 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 .. "|96px|link=|class=pd-hero-img]]")
         photo:wikitext("[[File:" .. pl.image .. "|120px|link=|class=pd-hero-img]]")
     else
     else
         photo:wikitext(initials)
         photo:wikitext(initials)
     end
     end


    -- Name + tags (no rank chip here anymore)
     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 341: Line 359:
         heroInfo:tag('div'):addClass('pd-hero-realname'):wikitext(pl.real_name)
         heroInfo:tag('div'):addClass('pd-hero-realname'):wikitext(pl.real_name)
     end
     end
     local tags = heroInfo:tag('div'):addClass('pd-hero-tags')
     local tags = heroInfo:tag('div'):addClass('pd-hero-tags')
     if pl.status and pl.status ~= "" then
     if pl.status and pl.status ~= "" then
Line 354: Line 371:
     if pl.game and pl.game ~= "" then
     if pl.game and pl.game ~= "" then
         tags:tag('span'):addClass('pd-tag pd-tag-game'):wikitext(pl.game)
         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
     end


    -- Earnings: right side on desktop, separate row on mobile via CSS
     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 412: Line 425:
     end
     end


    -- Krafton rank now sits naturally in the info panel
     if rankVal then infoRow('Krafton Rank', rankVal) end
     if rankVal then infoRow('Krafton Rank', rankVal) end


Line 432: Line 446:
     end
     end


     -- Team history with logos
     -- Team history
     if #teamHist > 0 then
     if #teamHist > 0 then
         left:tag('div'):addClass('pd-section-title pd-section-gap')
         left:tag('div'):addClass('pd-section-title pd-section-gap')
Line 452: Line 466:
                     .. (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
    -- ── Teammates section ─────────────────────────────────────────
    -- Players who shared the most tournaments with this player
    -- Source: Tournament_Teams p1-p6, coach/analyst excluded
    if #teammates > 0 then
        left:tag('div'):addClass('pd-section-title pd-section-gap')
            :wikitext('Teammates')
        for _, tm in ipairs(teammates) do
            local tr = left:tag('div'):addClass('pd-team-row')
            tr:tag('div'):addClass('pd-teammate-name')
                :wikitext("[[" .. tm.name .. "]]")
            tr:tag('div'):addClass('pd-teammate-count')
                :wikitext(tostring(tm.count)
                    .. (tm.count == 1 and " tournament" or " tournaments"))
         end
         end
     end
     end
Line 459: Line 489:
     local right = body:tag('div'):addClass('pd-right')
     local right = body:tag('div'):addClass('pd-right')


    -- Shared cell renderers (match achievements table exactly)
     local function renderTierCell(td, tier)
     local function renderTierCell(td, tier)
         if tier and tier ~= "" then
         if tier and tier ~= "" then
Line 470: Line 499:


     local function renderDateCell(td, date)
     local function renderDateCell(td, date)
        -- achievements table shows raw end_date string (e.g. "2025-03-15")
         td:addClass('ac-date'):wikitext(date and date ~= "" and date or "—")
         td:addClass('ac-date'):wikitext(date and date ~= "" and date or "—")
     end
     end


     -- ── Individual Awards ─────────────────────────────────────────
     -- Individual Awards
    -- Columns: Date | Tier | Tournament | Team | Award | Prize
     if #indAwards > 0 then
     if #indAwards > 0 then
         local sec = right:tag('div'):addClass('pd-prize-section')
         local sec = right:tag('div'):addClass('pd-prize-section')
         sec:tag('div'):addClass('pd-table-title'):wikitext('🎖 Individual Awards')
         sec:tag('div'):addClass('pd-table-title'):wikitext('🏆 Individual Awards')
         local tbl = sec:tag('table'):addClass('pd-tourn-table')
         local tbl = sec:tag('table'):addClass('pd-tourn-table')
         local hdr = tbl:tag('tr')
         local hdr = tbl:tag('tr')
Line 495: Line 522:
             tr:tag('td'):css('font-weight','600')
             tr:tag('td'):css('font-weight','600')
                 :wikitext("[[" .. (a.tournament or "") .. "]]")
                 :wikitext("[[" .. (a.tournament or "") .. "]]")
            -- Team column: which team was the player on during this tournament
             local playerTeam = tournTeamMap[a.tournament or ""]
             local playerTeam = tournTeamMap[a.tournament or ""]
             tr:tag('td'):wikitext(playerTeam and ("[[" .. playerTeam .. "]]") or "—")
             tr:tag('td'):wikitext(playerTeam and ("[[" .. playerTeam .. "]]") or "—")
Line 506: Line 532:
     end
     end


     -- ── Team Tournament Results ───────────────────────────────────
     -- Team Tournament Results
    -- Columns: Date | Tier | Tournament | Team | Place/Award | Prize
     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')
Line 518: Line 543:
         hdr2:tag('th'):wikitext('Tournament')
         hdr2:tag('th'):wikitext('Tournament')
         hdr2:tag('th'):wikitext('Team')
         hdr2:tag('th'):wikitext('Team')
         hdr2:tag('th'):addClass('ac-place'):wikitext('Place/Award')
         hdr2:tag('th'):addClass('ac-place'):wikitext('Place / Award')
         hdr2:tag('th'):css('text-align','right'):wikitext('Prize')
         hdr2:tag('th'):css('text-align','right'):wikitext('Prize')


Line 547: Line 572:
     end
     end


    -- Stats placeholder
     right:tag('div'):addClass('pd-stats-placeholder')
     right:tag('div'):addClass('pd-stats-placeholder')
         :tag('b'):wikitext('📊 Performance Stats'):done()
         :tag('b'):wikitext('📊 Performance Stats'):done()

Revision as of 00:55, 9 March 2026

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

-- ================================================================
-- Module:PlayerDashboard v4
-- Changes from v3:
--   • Krafton Rank moved from hero tags to left info panel
--   • Earnings on mobile: stacks below name, not floating right
--   • New "Teammates" section in left panel from Tournament_Teams
-- ================================================================

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 num = math.floor(tonumber(n) or 0)
    local s   = tostring(num)
    if #s <= 3 then return "₹ " .. s end
    local result    = s:sub(-3)
    local remaining = s:sub(1, -4)
    while #remaining > 2 do
        result    = remaining:sub(-2) .. "," .. result
        remaining = remaining:sub(1, -3)
    end
    return "₹ " .. remaining .. "," .. result
end

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

local function getTierClass(tier)
    if not tier then return "tier-def" end
    local t = tier:lower()
    if t:find("s") and t:find("tier") then return "tier-s" end
    if t:find("a") and t:find("tier") then return "tier-a" end
    if t:find("b") and t:find("tier") then return "tier-b" end
    if t:find("c") and t:find("tier") then return "tier-c" end
    return "tier-def"
end

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

-- Outputs both light + dark logo, CSS switches via logo-lightmode/logo-darkmode
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

-- Place badge matching achievements table style
local function renderPlaceBadge(td, placeNum)
    local badgeClass = "place-def"
    local placeText  = tostring(placeNum) .. "th"
    if placeNum == 1 then badgeClass = "place-1"; placeText = "1st"
    elseif placeNum == 2 then badgeClass = "place-2"; placeText = "2nd"
    elseif placeNum == 3 then badgeClass = "place-3"; placeText = "3rd"
    end
    td:tag('span'):addClass('place-badge ' .. badgeClass):wikitext(placeText)
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,p1,p2,p3,p4,p5,p6",
        { 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 })
    participation = participation or {}

    -- Tournament -> team lookup (for individual awards team column)
    local tournTeamMap = {}
    for _, r in ipairs(participation) do
        if r.tournament and r.team and not tournTeamMap[r.tournament] then
            tournTeamMap[r.tournament] = r.team
        end
    end

    -- ── 4. Teammates ──────────────────────────────────────────────
    -- Count how many tournaments each co-player appeared in with this player
    -- Skip coach/analyst — only p1-p6. Skip the player himself.
    local teammateCount = {}
    local slots = {"p1","p2","p3","p4","p5","p6"}
    for _, r in ipairs(participation) do
        for _, slot in ipairs(slots) do
            local name = r[slot]
            if name and name ~= "" and name ~= pageName and name ~= displayName then
                teammateCount[name] = (teammateCount[name] or 0) + 1
            end
        end
    end

    -- Sort teammates by frequency desc, take top 10
    local teammates = {}
    for name, count in pairs(teammateCount) do
        table.insert(teammates, { name = name, count = count })
    end
    table.sort(teammates, function(a, b) return a.count > b.count end)
    if #teammates > 10 then
        local trimmed = {}
        for i = 1, 10 do trimmed[i] = teammates[i] end
        teammates = trimmed
    end

    -- ── 5. Earnings ───────────────────────────────────────────────
    local indEarnings, teamEarnings = 0, 0

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

    if #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

    -- ── 6. 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 not teamHist or #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
    teamHist = teamHist or {}

    -- ── 7a. 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 })
    indAwards = indAwards or {}

    -- ── 7b. Team tournament results ───────────────────────────────
    local teamResults = {}
    local seenKey     = {}

    if #participation > 0 then
        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

        if #pairs_list > 0 then
            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
            local whereStr = table.concat(ttConds, " OR ")

            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

            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
                    local newPrize = tonumber(row.prize) or 0
                    local existing = prizeIndex[k]
                    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

            for _, r in ipairs(pairs_list) do
                local k      = r.tournament .. "|" .. r.team
                local pr     = prizeIndex[k]
                local pl_num = placeIndex[k]
                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

    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 "")
    end)

    -- ── 8. Tournament metadata ────────────────────────────────────
    local allTournNames = {}
    local tournSeen     = {}
    local function addTourn(name)
        if name and name ~= "" and not tournSeen[name] then
            tournSeen[name] = true
            table.insert(allTournNames, "'" .. esc(name) .. "'")
        end
    end
    for _, a in ipairs(indAwards)   do addTourn(a.tournament) end
    for _, t in ipairs(teamResults) do addTourn(t.tournament) end

    local tournMeta = {}
    if #allTournNames > 0 then
        local metaRows = cargo.query("Tournaments", "name,tier,end_date", {
            where = "name IN (" .. table.concat(allTournNames, ",") .. ")",
            limit = 200 })
        if metaRows then
            for _, row in ipairs(metaRows) do
                tournMeta[row.name] = { tier = row.tier, date = row.end_date }
            end
        end
    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 ════
    -- Layout: [photo] [name + realname + tags] [earnings — hidden on mobile]
    -- Krafton rank is NO LONGER here — moved to left info panel

    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 .. "|120px|link=|class=pd-hero-img]]")
    else
        photo:wikitext(initials)
    end

    -- Name + tags (no rank chip here anymore)
    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

    -- Earnings: right side on desktop, separate row on mobile via CSS
    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: 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 wrap = row:tag('span'):addClass('pd-info-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

    -- Krafton rank now sits naturally in the info panel
    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
    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

    -- ── Teammates section ─────────────────────────────────────────
    -- Players who shared the most tournaments with this player
    -- Source: Tournament_Teams p1-p6, coach/analyst excluded
    if #teammates > 0 then
        left:tag('div'):addClass('pd-section-title pd-section-gap')
            :wikitext('Teammates')
        for _, tm in ipairs(teammates) do
            local tr = left:tag('div'):addClass('pd-team-row')
            tr:tag('div'):addClass('pd-teammate-name')
                :wikitext("[[" .. tm.name .. "]]")
            tr:tag('div'):addClass('pd-teammate-count')
                :wikitext(tostring(tm.count)
                    .. (tm.count == 1 and " tournament" or " tournaments"))
        end
    end

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

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

    local function renderTierCell(td, tier)
        if tier and tier ~= "" then
            td:addClass('ac-tier')
              :tag('span'):addClass('tier-badge ' .. getTierClass(tier)):wikitext(tier)
        else
            td:wikitext("—")
        end
    end

    local function renderDateCell(td, date)
        td:addClass('ac-date'):wikitext(date and date ~= "" and date or "—")
    end

    -- Individual Awards
    if #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('Date')
        hdr:tag('th'):wikitext('Tier')
        hdr:tag('th'):wikitext('Tournament')
        hdr:tag('th'):wikitext('Team')
        hdr:tag('th'):wikitext('Award')
        hdr:tag('th'):css('text-align','right'):wikitext('Prize')

        for _, a in ipairs(indAwards) do
            local meta = tournMeta[a.tournament or ""] or {}
            local tr   = tbl:tag('tr')
            renderDateCell(tr:tag('td'), meta.date)
            renderTierCell(tr:tag('td'), meta.tier)
            tr:tag('td'):css('font-weight','600')
                :wikitext("[[" .. (a.tournament or "") .. "]]")
            local playerTeam = tournTeamMap[a.tournament or ""]
            tr:tag('td'):wikitext(playerTeam and ("[[" .. playerTeam .. "]]") 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('ac-prize')
                :wikitext(tonumber(a.prize) and tonumber(a.prize) > 0
                    and fmtCurrency(tonumber(a.prize)) or "—")
        end
    end

    -- Team Tournament Results
    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('Date')
        hdr2:tag('th'):wikitext('Tier')
        hdr2:tag('th'):wikitext('Tournament')
        hdr2:tag('th'):wikitext('Team')
        hdr2:tag('th'):addClass('ac-place'):wikitext('Place / Award')
        hdr2:tag('th'):css('text-align','right'):wikitext('Prize')

        for _, t in ipairs(teamResults) do
            local meta = tournMeta[t.tournament or ""] or {}
            local tr2  = tbl2:tag('tr')
            renderDateCell(tr2:tag('td'), meta.date)
            renderTierCell(tr2:tag('td'), meta.tier)
            tr2:tag('td'):css('font-weight','600')
                :wikitext("[[" .. t.tournament .. "]]")
            tr2:tag('td'):wikitext("[[" .. t.team .. "]]")

            local pTd = tr2:tag('td'):addClass('ac-place')
            if t.place then
                renderPlaceBadge(pTd, t.place)
            elseif t.award then
                pTd:tag('span'):addClass('pd-award-badge'):wikitext(t.award)
            else
                pTd:wikitext("—")
            end

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

    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