yusiff commited on
Commit
e73cabd
·
verified ·
1 Parent(s): 0d39e52

Update views/profile.py

Browse files
Files changed (1) hide show
  1. views/profile.py +238 -0
views/profile.py CHANGED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """👤 ملف الموظف — النقاط، الترتيب، الدروع، الألقاب، الـ AI Score، الولاء، سجل الحركات."""
2
+
3
+ import pandas as pd
4
+ import streamlit as st
5
+
6
+ from clickup_source import get_task_source
7
+ from data_layer import get_repository
8
+ from logic import (
9
+ ACTION_ICONS,
10
+ ACTION_LABELS_AR,
11
+ AI_LEVEL_META,
12
+ BADGE_TIERS,
13
+ ai_scores,
14
+ employee_rank,
15
+ leaderboard,
16
+ loyalty_status,
17
+ next_badge_progress,
18
+ titles_for_employee,
19
+ )
20
+ from auth import current_employee_id, is_manager
21
+ from ui import page_header, sidebar_note
22
+
23
+ repo = get_repository()
24
+ source = get_task_source()
25
+
26
+ employees = repo.get_employees()
27
+ names = dict(zip(employees["employee_id"], employees["name"]))
28
+
29
+ # المدير يختار أي موظف؛ الموظف مقفول على نفسه فقط ولا يرى قائمة زملائه.
30
+ if is_manager():
31
+ page_header("👤 ملفات الموظفين", "كل ما يخص الموظف: رصيده، دروعه، ألقابه، وحالة ولائه")
32
+ selected_id = st.selectbox(
33
+ "👥 اختر الموظف",
34
+ options=list(names.keys()),
35
+ format_func=lambda eid: f"{names[eid]} — {eid}",
36
+ )
37
+ else:
38
+ selected_id = current_employee_id()
39
+ page_header("👤 ملفي الشخصي", "نقاطي، دروعي، ألقابي، تقييمي، وحالة ولائي")
40
+
41
+ if selected_id not in names:
42
+ st.error("🚫 لا يمكن عرض هذا الملف.")
43
+ st.stop()
44
+
45
+ emp = employees[employees["employee_id"] == selected_id].iloc[0]
46
+ balance = int(emp["current_points"])
47
+ rank = employee_rank(repo, selected_id)
48
+ total_employees = len(employees)
49
+
50
+ # ---------------- بطاقة الهوية ----------------
51
+ lb = leaderboard(repo)
52
+ level_emoji = lb[lb["employee_id"] == selected_id]["level_emoji"].iloc[0]
53
+
54
+ st.markdown(
55
+ f"""
56
+ <div class="card" style="border-color: rgba(124,92,255,.4);
57
+ background: linear-gradient(160deg, rgba(124,92,255,.16), rgba(255,255,255,.02));">
58
+ <div style="display:flex; align-items:center; gap:1.2rem; flex-wrap:wrap;">
59
+ <div style="font-size:3.4rem;">{level_emoji}</div>
60
+ <div style="flex:1; min-width:180px;">
61
+ <div style="font-size:1.5rem; font-weight:700;">{emp['name']}</div>
62
+ <div class="card-sub">{emp['department']} · {selected_id} ·
63
+ تاريخ التعيين <span class="ltr">{emp['hire_date']}</span></div>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ """,
68
+ unsafe_allow_html=True,
69
+ )
70
+
71
+ # ---------------- المؤشرات ----------------
72
+ scores = ai_scores(repo, source)
73
+ my_score = scores[scores["employee_id"] == selected_id].iloc[0]
74
+ score_icon, score_color = AI_LEVEL_META[my_score["level"]]
75
+
76
+ m1, m2, m3, m4 = st.columns(4)
77
+ m1.metric("⭐ الرصيد الحالي", f"{balance:,}")
78
+ m2.metric("📊 الترتيب العام", f"#{rank}", delta=f"من {total_employees}", delta_color="off")
79
+ m3.metric("🛡️ المستوى", f"{emp['current_level']}")
80
+ m4.metric(f"{score_icon} الـ AI Score", f"{my_score['score']:.1f}", delta=my_score["level"], delta_color="off")
81
+
82
+ st.markdown("---")
83
+
84
+ col_left, col_right = st.columns([3, 2])
85
+
86
+ # ================= العمود الأيمن (الأول في RTL) =================
87
+ with col_left:
88
+ # ---------------- الدروع ----------------
89
+ st.markdown("### 🛡️ الدروع")
90
+ badges = repo.get_badges()
91
+ my_badges = badges[badges["employee_id"] == selected_id]
92
+ earned_names = set(my_badges["badge_name"])
93
+ awarded_at = dict(zip(my_badges["badge_name"], my_badges["awarded_at"]))
94
+
95
+ tiles = ""
96
+ for threshold, badge_name, _, emoji in BADGE_TIERS:
97
+ is_earned = badge_name in earned_names
98
+ state = "earned" if is_earned else "locked"
99
+ sub = (str(awarded_at[badge_name])[:10] if is_earned else f"{threshold:,} نقطة")
100
+ tiles += (
101
+ f'<div class="badge-tile {state}"><div class="b-emoji">{emoji}</div>'
102
+ f'<div class="b-name">{badge_name.replace(" Shield", "")}</div>'
103
+ f'<div class="b-name ltr" style="font-size:.6rem;">{sub}</div></div>'
104
+ )
105
+ st.markdown(f'<div style="display:flex; flex-wrap:wrap;">{tiles}</div>', unsafe_allow_html=True)
106
+
107
+ # ---------------- الدرع القادم ----------------
108
+ nxt = next_badge_progress(balance)
109
+ if nxt:
110
+ st.markdown(f"#### {nxt['emoji']} الدرع القادم: {nxt['badge_name']}")
111
+ st.progress(nxt["progress"])
112
+ pct = nxt["progress"] * 100
113
+ st.markdown(
114
+ f'<div class="card-sub">باقي <b style="color:#F5C542;">{nxt["remaining"]:,}</b> نقطة '
115
+ f'للوصول إلى <b>{nxt["threshold"]:,}</b> — أنجزت {pct:.0f}% من المسافة</div>',
116
+ unsafe_allow_html=True,
117
+ )
118
+ else:
119
+ st.success("👑 وصل إلى أعلى درع في النظام — Legend Shield")
120
+
121
+ st.markdown("---")
122
+
123
+ # ---------------- سجل الحركات ----------------
124
+ st.markdown("### 📜 سجل حركات النقاط")
125
+ points = repo.get_points()
126
+ mine = points[points["employee_id"] == selected_id].copy()
127
+
128
+ if mine.empty:
129
+ st.info("لا توجد حركات نقاط مسجّلة لهذا الموظف بعد.")
130
+ else:
131
+ mine["created_at"] = pd.to_datetime(mine["created_at"], errors="coerce")
132
+ mine = mine.sort_values("created_at", ascending=False)
133
+
134
+ st.caption(f"إجمالي **{len(mine)}** حركة · مجموعها "
135
+ f"**{int(mine['points'].sum()):+d}** نقطة في الفترة المسجّلة")
136
+
137
+ for _, row in mine.iterrows():
138
+ pts = int(row["points"])
139
+ icon = ACTION_ICONS.get(row["action"], "•")
140
+ label = ACTION_LABELS_AR.get(row["action"], row["action"])
141
+ cls = "tl-pos" if pts >= 0 else "tl-neg"
142
+ when = row["created_at"].strftime("%Y-%m-%d %H:%M") if pd.notna(row["created_at"]) else "—"
143
+ st.markdown(
144
+ f'<div class="tl-item"><div class="tl-icon">{icon}</div>'
145
+ f'<div class="tl-body"><div class="tl-action">{label}</div>'
146
+ f'<div class="tl-date">{when} · {row["action"]}</div></div>'
147
+ f'<div class="tl-pts {cls}">{pts:+d}</div></div>',
148
+ unsafe_allow_html=True,
149
+ )
150
+
151
+ # ================= العمود الأيسر =================
152
+ with col_right:
153
+ # ---------------- الألقاب ----------------
154
+ st.markdown("### 🎖️ الألقاب")
155
+ my_titles = titles_for_employee(repo, source, selected_id)
156
+ if my_titles:
157
+ for t in my_titles:
158
+ st.markdown(
159
+ f'<div class="card" style="border-color:rgba(245,197,66,.4);'
160
+ f'background:linear-gradient(160deg, rgba(245,197,66,.12), rgba(255,255,255,.02));">'
161
+ f'<div style="font-size:1.6rem;">{t["emoji"]}</div>'
162
+ f'<div class="card-title">{t["title"]} — {t["title_ar"]}</div>'
163
+ f'<div class="card-sub">{t["criterion"]}</div>'
164
+ f'<div style="margin-top:.4rem; color:#F5C542; font-weight:600; font-size:.85rem;">'
165
+ f'{t["value"]}</div></div>',
166
+ unsafe_allow_html=True,
167
+ )
168
+ else:
169
+ st.markdown(
170
+ '<div class="card"><div class="card-sub">لم يحصل على أي لقب بعد — '
171
+ 'الألقاب تُمنح لصاحب المركز الأول في كل معيار.</div></div>',
172
+ unsafe_allow_html=True,
173
+ )
174
+
175
+ # ---------------- الـ AI Score ----------------
176
+ st.markdown("### 🤖 تفصيل الـ AI Score")
177
+ st.markdown(
178
+ f'<div class="card" style="text-align:center;">'
179
+ f'<div style="font-size:2.6rem; font-weight:700; color:{score_color}; direction:ltr;">'
180
+ f'{my_score["score"]:.1f}</div>'
181
+ f'<div class="card-sub">من 100</div>'
182
+ f'<div class="chip chip-gold" style="margin-top:.5rem;">{score_icon} {my_score["level"]}</div>'
183
+ f"</div>",
184
+ unsafe_allow_html=True,
185
+ )
186
+ for label, key, weight in [
187
+ ("✅ إنجاز المهام", "completion", 40),
188
+ ("⏱️ الالتزام بالمواعيد", "on_time", 25),
189
+ ("⭐ الجودة", "quality", 20),
190
+ ("🤝 التعاون", "collaboration", 15),
191
+ ]:
192
+ st.markdown(
193
+ f'<div class="card-sub" style="margin-bottom:.15rem;">{label} '
194
+ f'<span class="ltr">({weight}%)</span> — '
195
+ f'<b style="color:#E8ECF8;">{my_score[key]:.0f}%</b></div>',
196
+ unsafe_allow_html=True,
197
+ )
198
+ st.progress(float(my_score[key]) / 100)
199
+
200
+ st.markdown("---")
201
+
202
+ # ---------------- برنامج الولاء ----------------
203
+ st.markdown("### 🎗️ برنامج الولاء")
204
+ loyalty = loyalty_status(emp["hire_date"])
205
+
206
+ st.markdown(
207
+ f'<div class="card">'
208
+ f'<div class="card-title">{loyalty["current_status"]}</div>'
209
+ f'<div class="card-sub">في تشويش منذ '
210
+ f'<span class="ltr">{loyalty["years_employed"]}</span> سنة '
211
+ f'(<span class="ltr">{loyalty["days_employed"]:,}</span> يوم)</div></div>',
212
+ unsafe_allow_html=True,
213
+ )
214
+
215
+ for m in loyalty["achieved"]:
216
+ st.markdown(
217
+ f'<div class="rank-row" style="border-color:rgba(52,211,153,.35);">'
218
+ f'<div style="font-size:1.1rem;">✅</div>'
219
+ f'<div class="rank-name" style="font-size:.85rem;">{m["reward"]}'
220
+ f'<div class="rank-dept">{m["label"]} · تحققت في '
221
+ f'<span class="ltr">{m["achieved_on"].strftime("%Y-%m-%d")}</span></div></div></div>',
222
+ unsafe_allow_html=True,
223
+ )
224
+
225
+ nxt_loyalty = loyalty["next"]
226
+ if nxt_loyalty:
227
+ st.markdown(f'#### ⏳ القادم: {nxt_loyalty["reward"]}')
228
+ st.progress(nxt_loyalty["progress"])
229
+ st.markdown(
230
+ f'<div class="card-sub">باقي '
231
+ f'<b style="color:#F5C542;" class="ltr">{nxt_loyalty["remaining_days"]:,}</b> يوم '
232
+ f'للوصول إلى مرحلة <b>{nxt_loyalty["label"]}</b></div>',
233
+ unsafe_allow_html=True,
234
+ )
235
+ else:
236
+ st.success("👑 أكمل كل مراحل برنامج الولاء — VIP Employee")
237
+
238
+ sidebar_note()