Spaces:
Sleeping
Sleeping
Update views/leaderboard.py
Browse files- views/leaderboard.py +110 -0
views/leaderboard.py
CHANGED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""🏆 Leaderboard — الترتيب العام + منصة الأوائل + ترتيب الأقسام."""
|
| 2 |
+
|
| 3 |
+
import streamlit as st
|
| 4 |
+
|
| 5 |
+
from auth import require_manager
|
| 6 |
+
from data_layer import get_repository
|
| 7 |
+
from logic import department_ranking, leaderboard
|
| 8 |
+
from ui import page_header, sidebar_note
|
| 9 |
+
|
| 10 |
+
require_manager()
|
| 11 |
+
|
| 12 |
+
repo = get_repository()
|
| 13 |
+
lb = leaderboard(repo)
|
| 14 |
+
|
| 15 |
+
page_header("🏆 لوحة الترتيب", "الترتيب حسب الرصيد التراكمي للنقاط")
|
| 16 |
+
|
| 17 |
+
# ---------------- الفلتر ----------------
|
| 18 |
+
departments = ["كل الأقسام"] + sorted(lb["department"].unique().tolist())
|
| 19 |
+
col_f, col_i = st.columns([1, 2])
|
| 20 |
+
with col_f:
|
| 21 |
+
selected = st.selectbox("🏢 فلترة بالقسم", departments)
|
| 22 |
+
|
| 23 |
+
view = lb if selected == "كل الأقسام" else lb[lb["department"] == selected].copy()
|
| 24 |
+
|
| 25 |
+
if selected != "كل الأقسام":
|
| 26 |
+
# ترتيب داخل القسم مع إبقاء الترتيب العام ظاهرًا
|
| 27 |
+
view = view.reset_index(drop=True)
|
| 28 |
+
view["dept_rank"] = view.index + 1
|
| 29 |
+
with col_i:
|
| 30 |
+
st.info(f"عرض **{len(view)}** موظف في قسم **{selected}** — الأرقام بين قوسين هي الترتيب العام.")
|
| 31 |
+
|
| 32 |
+
# ---------------- منصة الأوائل ----------------
|
| 33 |
+
if selected == "كل الأقسام" and len(view) >= 3:
|
| 34 |
+
st.markdown("### 🎖️ منصة الأوائل")
|
| 35 |
+
first, second, third = view.iloc[0], view.iloc[1], view.iloc[2]
|
| 36 |
+
|
| 37 |
+
def podium_html(row, place: int, medal: str) -> str:
|
| 38 |
+
crown = '<div class="podium-crown">👑</div>' if place == 1 else ""
|
| 39 |
+
return (
|
| 40 |
+
f'<div class="podium podium-{place}">{crown}'
|
| 41 |
+
f'<div class="podium-medal">{medal}</div>'
|
| 42 |
+
f'<div class="podium-name">{row["name"]}</div>'
|
| 43 |
+
f'<div class="podium-dept">{row["department"]}</div>'
|
| 44 |
+
f'<div class="podium-points">{int(row["current_points"]):,}</div>'
|
| 45 |
+
f'<div class="podium-dept">{row["level_emoji"]} {row["current_level"]}</div>'
|
| 46 |
+
f'</div>'
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
# ترتيب بصري: الثاني — الأول — الثالث
|
| 50 |
+
st.markdown(
|
| 51 |
+
'<div class="podium-wrap">'
|
| 52 |
+
+ podium_html(second, 2, "🥈")
|
| 53 |
+
+ podium_html(first, 1, "🥇")
|
| 54 |
+
+ podium_html(third, 3, "🥉")
|
| 55 |
+
+ "</div>",
|
| 56 |
+
unsafe_allow_html=True,
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
# ---------------- الترتيب الكامل ----------------
|
| 60 |
+
st.markdown("### 📊 الترتيب الكامل")
|
| 61 |
+
|
| 62 |
+
if view.empty:
|
| 63 |
+
st.warning("لا يوجد موظفون في هذا القسم.")
|
| 64 |
+
else:
|
| 65 |
+
for _, row in view.iterrows():
|
| 66 |
+
rank = int(row["dept_rank"]) if "dept_rank" in view.columns else int(row["rank"])
|
| 67 |
+
overall = f' <span class="card-sub">(عام #{int(row["rank"])})</span>' if "dept_rank" in view.columns else ""
|
| 68 |
+
highlight = " me" if rank == 1 else ""
|
| 69 |
+
st.markdown(
|
| 70 |
+
f"""
|
| 71 |
+
<div class="rank-row{highlight}">
|
| 72 |
+
<div class="rank-num">{rank}</div>
|
| 73 |
+
<div style="font-size:1.4rem;">{row['level_emoji']}</div>
|
| 74 |
+
<div class="rank-name">{row['name']}{overall}
|
| 75 |
+
<div class="rank-dept">{row['department']} · {row['current_level']}</div>
|
| 76 |
+
</div>
|
| 77 |
+
<div class="rank-points">{int(row['current_points']):,}</div>
|
| 78 |
+
</div>
|
| 79 |
+
""",
|
| 80 |
+
unsafe_allow_html=True,
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
st.markdown("---")
|
| 84 |
+
|
| 85 |
+
# ---------------- ترتيب الأقسام ----------------
|
| 86 |
+
st.markdown("### 🏢 ترتيب الأقسام")
|
| 87 |
+
st.caption("الترتيب حسب **متوسط** نقاط الموظف الواحد — حتى لا تتفوق الأقسام الكبيرة لمجرد عدد أفرادها.")
|
| 88 |
+
|
| 89 |
+
depts = department_ranking(repo)
|
| 90 |
+
max_avg = depts["avg_points"].max() or 1
|
| 91 |
+
|
| 92 |
+
cols = st.columns(min(len(depts), 5))
|
| 93 |
+
for i, (_, row) in enumerate(depts.iterrows()):
|
| 94 |
+
with cols[i % len(cols)]:
|
| 95 |
+
medal = ["🥇", "🥈", "🥉"][i] if i < 3 else "🏢"
|
| 96 |
+
st.markdown(
|
| 97 |
+
f'<div class="card" style="text-align:center; min-height:150px;">'
|
| 98 |
+
f'<div style="font-size:1.7rem;">{medal}</div>'
|
| 99 |
+
f'<div class="card-title" style="font-size:.92rem;">{row["department"]}</div>'
|
| 100 |
+
f'<div style="font-size:1.5rem; font-weight:700; color:#F5C542; direction:ltr;">'
|
| 101 |
+
f'{row["avg_points"]:.0f}</div>'
|
| 102 |
+
f'<div class="card-sub">متوسط النقاط</div>'
|
| 103 |
+
f'<div class="card-sub" style="margin-top:.3rem;">'
|
| 104 |
+
f'{int(row["members"])} موظف · {int(row["total_points"]):,} إجمالي</div>'
|
| 105 |
+
f"</div>",
|
| 106 |
+
unsafe_allow_html=True,
|
| 107 |
+
)
|
| 108 |
+
st.progress(float(row["avg_points"] / max_avg))
|
| 109 |
+
|
| 110 |
+
sidebar_note()
|