Dhom1 commited on
Commit
c02638d
·
verified ·
1 Parent(s): 08592e7

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +142 -101
src/streamlit_app.py CHANGED
@@ -1,12 +1,14 @@
1
  """
2
- Health Matrix – AI Command Center (Desktop Wide + AI Cards + Showcase Mode)
3
  - Sticky Left Timeline (Chris Wright–style, animated)
4
  - KPI: Animated gradient-border cards
5
- - Employee Results: Glass + Aurora cards (per-employee)
6
- - Showcase Mode: يضمن (Assign / Notify / Skip) لعرض قدرات الوكيل + يضيف موظف ثالث تجريبي فقط للعرض
7
- - خيار Lenient Eligibility (OR) في وضع العرض
 
 
8
  - Full desktop width, responsive, WCAG-friendly
9
- - المنطق الأصلي محفوظ عند إيقاف وضع العرض
10
 
11
  Usage: streamlit run streamlit_app.py
12
  """
@@ -37,6 +39,9 @@ _EMBEDDED_CSS = """
37
  --bg:#FFFFFF; --soft:#F8FAFC; --line:#E5E7EB;
38
  --t1:#0F172A; --t2:#1F2937; --muted:#64748B;
39
  --blue:#004C97; --green:#36BA01; --ok:#16a34a; --warn:#D97706; --fail:#DC2626;
 
 
 
40
  }
41
 
42
  /* ==== Full-width page on desktop ==== */
@@ -122,35 +127,27 @@ p{ color:var(--t2) }
122
  .kpi-card .lbl{ color:#334155 }
123
  .kpi-card .val{ font-size:1.8rem; font-weight:900; line-height:1 }
124
 
125
- /* ================= Employee (Result) Cards – Glass + Aurora ================= */
126
- .emp-grid{
127
- display:grid; gap:14px; margin-top:12px;
128
- grid-template-columns: repeat(12, 1fr);
129
- }
130
  .emp-card{
131
- grid-column: span 4; /* 3 per row desktop */
132
  position:relative; overflow:hidden;
133
- background: rgba(255,255,255,.75);
134
  border:1px solid #E5E7EB; border-radius:16px; backdrop-filter: blur(10px);
135
  box-shadow:0 10px 26px rgba(0,0,0,.08);
136
- padding:14px 14px 12px; transform: translateY(6px); opacity:0;
137
  animation: cardIn .35s ease-out forwards;
138
  }
139
- .emp-card:nth-child(2){ animation-delay:.03s }
140
- .emp-card:nth-child(3){ animation-delay:.06s }
141
- .emp-card:nth-child(4){ animation-delay:.09s }
142
  .emp-card::before{ /* Aurora glow */
143
  content:""; position:absolute; inset:-30% -10% auto -10%; height:60%;
144
  background:
145
- radial-gradient(600px 280px at 20% 30%, rgba(54,186,1,.25), transparent 60%),
146
- radial-gradient(520px 220px at 80% 20%, rgba(0,76,151,.22), transparent 60%);
147
- filter: blur(28px); opacity:.8; z-index:0; pointer-events:none;
148
  animation: aurora 18s linear infinite;
149
  }
150
  .emp-head, .emp-body, .emp-foot{ position:relative; z-index:1 }
151
- .emp-head{ display:flex; align-items:center; gap:10px }
152
  .emp-avatar{
153
- width:42px; height:42px; border-radius:999px; background:#F1F5F9;
154
  display:flex; align-items:center; justify-content:center; font-weight:800; color:#0f172a
155
  }
156
  .emp-name{ font-weight:900 }
@@ -177,8 +174,34 @@ p{ color:var(--t2) }
177
  .emp-tag{ font-size:.8rem; padding:6px 10px; border-radius:999px; border:1px solid #E5E7EB; background:#fff }
178
  .emp-meta{ margin-left:auto; color:#64748B; font-size:.85rem }
179
 
180
- @media (max-width: 1320px){ .emp-card{ grid-column: span 6 } } /* 2 per row */
181
- @media (max-width: 820px){ .emp-card{ grid-column: span 12 } .emp-body{ grid-template-columns: 1fr } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
 
183
  /* ======================= Chris Wright Timeline (adapted) ======================= */
184
  .cw-rail{ position:sticky; top:84px } /* pinned under Streamlit header */
@@ -414,9 +437,7 @@ def is_eligible_lenient(row: pd.Series, shift: pd.Series) -> bool:
414
  ]).lower()
415
 
416
  role_ok = (row.get("JobRole","").strip().lower() == role_req) or (role_req and role_req in text_all)
417
- # specialty: اكتفينا بوجود "icu" أو الكلمة الرئيسية داخل النص الكامل
418
  spec_ok = (not specialty) or any(tok in text_all for tok in [specialty, specialty.replace(" ", ""), "icu"])
419
- # training: أي ظهور لـ BLS/ALS في النص يكفي
420
  training_ok = (not mandatory) or all(any(mtok in text_all for mtok in [m.lower(), m.lower().replace(" certified","")]) for m in mandatory)
421
  ward_ok = (not ward) or (ward in text_all)
422
  lang_ok = (not lang_req) or (lang_req in [l.lower() for l in (row.get("Languages") or [])])
@@ -558,7 +579,6 @@ def run_agent(
558
  t_eval = time.perf_counter()
559
  events.append({"title":"Evaluate open shifts", "desc":"Matching employees vs. role/specialty/training/ward…", "status":"done"})
560
 
561
- # eligibility function
562
  elig_fn = is_eligible_lenient if lenient else is_eligible_strict
563
 
564
  for _, shift in df_shifts.iterrows():
@@ -567,11 +587,9 @@ def run_agent(
567
  # 3) AI decision (or showcase distribution)
568
  t_ai = time.perf_counter()
569
  if showcase:
570
- # Rank all by score
571
  ranked = df_employees.copy()
572
  ranked["__score"] = ranked.apply(lambda r: _score_match(r, shift), axis=1)
573
  ranked = ranked.sort_values("__score", ascending=False).reset_index(drop=True)
574
- # pick distinct employees for assign/notify/skip
575
  cand_assign = ranked.iloc[0] if len(ranked) > 0 else None
576
  cand_notify = ranked.iloc[1] if len(ranked) > 1 else None
577
  cand_skip = ranked.iloc[-1] if len(ranked) > 2 else None
@@ -611,9 +629,8 @@ def run_agent(
611
  events.append({"title":"Skipped", "desc":"No eligible employees or decision skipped", "status":"done"})
612
  shift_assignment_results.append(("❌ No eligible", shift["ShiftID"], "⚠️ Skipped"))
613
 
614
- # Reasoning rows (explanatory) — نستخدم المنطق الصارم/المخفف حسب الاختيار
615
  for _, emp_row in df_employees.iterrows():
616
- # عناصر التحقق
617
  role_match = str(shift.get("RoleRequired","")).strip().lower() == emp_row.get("JobRole","").strip().lower()
618
  spec = str(shift.get("Specialty","")).replace("-", " ").strip().lower()
619
  text_all = (emp_row.get("JobRole","")+" "+emp_row.get("organizationPath","")+" "+" ".join(emp_row.get("Certifications",[]))).lower()
@@ -666,13 +683,65 @@ def render_kpis(assigned:int, notified:int, skipped:int) -> str:
666
  </div>
667
  """
668
 
669
- def render_employee_cards(shift_results: List[tuple], reasoning_rows: List[Dict[str,Any]]) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
670
  """
671
- Builds a grid of AI-styled glass cards per employee.
672
- shift_results: list[(Employee, ShiftID, Status)] from agent.
673
- reasoning_rows: list of dicts with keys Employee, ShiftID, Eligible, Reasoning, Certifications.
674
  """
675
- # Build maps
676
  action_by_emp: Dict[str, str] = {}
677
  shift_by_emp: Dict[str, str] = {}
678
  for emp, sid, status in shift_results:
@@ -680,83 +749,53 @@ def render_employee_cards(shift_results: List[tuple], reasoning_rows: List[Dict[
680
  action_by_emp[str(emp)] = status
681
  shift_by_emp[str(emp)] = str(sid)
682
 
683
- # Aggregate reasoning per employee (first row per employee is enough here)
684
  seen = set()
685
  employees: List[Dict[str,Any]] = []
686
  for r in reasoning_rows:
687
  emp = (r.get("Employee") or "").strip()
688
- if not emp or emp in seen:
689
- continue
690
  seen.add(emp)
691
  employees.append({
692
  "name": emp,
693
  "shift": r.get("ShiftID",""),
694
  "eligible": r.get("Eligible",""),
695
  "reasoning": r.get("Reasoning",""),
696
- "certs": r.get("Certifications","")
 
697
  })
698
 
699
  if not employees:
700
  return '<div class="table-wrap"><div style="padding:10px;color:#64748B">No employee details available.</div></div>'
701
 
702
- # Build cards
703
- def chip_class(txt:str)->str:
704
- t = (txt or "").lower()
705
- if "assigned" in t: return "ok"
706
- if "notify" in t: return "info"
707
- if "skip" in t: return "warn"
708
- if "eligible" in t and "" in (txt or ""): return "ok"
709
- if "eligible" in t and "❌" in (txt or ""): return "fail"
710
- return "info"
711
-
712
- def name_initials(n:str)->str:
713
- n = (n or "").strip()
714
- if not n: return "?"
715
- parts = [p for p in n.split(" ") if p]
716
- if len(parts)==1: return parts[0][:2].upper()
717
- return (parts[0][0]+parts[-1][0]).upper()
718
-
719
- cards = []
720
- for emp in employees:
721
- name = emp["name"]
722
- action = action_by_emp.get(name)
723
- if not action:
724
- action = "✅ Eligible" if str(emp["eligible"]).startswith("✅") else "❌ Not Eligible"
725
- chip = action
726
- c_class = chip_class(chip)
727
- checks = [x.strip() for x in str(emp["reasoning"] or "").split("|") if x and x.strip()]
728
- check_html = []
729
- for ch in checks:
730
- ok = "✅" in ch or ch.endswith("✅")
731
- label = ch.replace("✅","").replace("❌","").strip()
732
- check_html.append(f'<div class="emp-badge {"ok" if ok else "fail"}"><span class="dot"></span><span>{label}</span></div>')
733
- certs = emp["certs"] or "—"
734
- shift = shift_by_emp.get(name, emp["shift"] or "—")
735
- cards.append(f"""
736
- <div class="emp-card" role="group" aria-label="{name}">
737
- <div class="emp-head">
738
- <div class="emp-avatar">{name_initials(name)}</div>
739
- <div>
740
- <div class="emp-name">{name}</div>
741
- <div style="color:#64748B;font-size:.85rem">Shift: {shift}</div>
742
- </div>
743
- <div class="emp-chip {c_class}" title="Decision/Status">{chip}</div>
744
- </div>
745
 
746
- <div class="emp-line"></div>
747
-
748
- <div class="emp-body">
749
- {''.join(check_html) if check_html else '<div class="emp-badge info"><span class="dot" style="background:#60a5fa"></span><span>No reasoning available</span></div>'}
750
- </div>
751
-
752
- <div class="emp-foot">
753
- <div class="emp-tag">🪪 Certifications: {certs}</div>
754
- <div class="emp-meta">Updated just now</div>
755
- </div>
 
 
756
  </div>
757
- """)
758
 
759
- return '<div class="emp-grid">' + "".join(cards) + '</div>'
 
 
 
 
760
 
761
 
762
  # =============================================================================
@@ -772,7 +811,7 @@ def main() -> None:
772
  <div class="hero" dir="auto">
773
  <img src="https://www.healthmatrixcorp.com/web/image/website/1/logo/Health%20Matrix?unique=956ad7b" alt="Health Matrix" />
774
  <h1>AI Command Center – Smart Staffing & Actions</h1>
775
- <p>Desktop-first view. Sticky left runline + AI-styled KPI & employee cards. Showcase mode guarantees Assign/Notify/Skip for executive demo.</p>
776
  </div>
777
  """, unsafe_allow_html=True
778
  )
@@ -789,11 +828,12 @@ def main() -> None:
789
  </div>
790
  """, unsafe_allow_html=True)
791
 
792
- # Demo controls (لا تؤثر على الإنتاج عند إيقافها)
793
  with st.expander("🔧 Demo / Showcase Controls", expanded=False):
794
  show_showcase = st.checkbox("Ensure Assign + Notify + Skip (adds a demo nurse if needed)", value=True)
795
  lenient_mode = st.checkbox("Lenient eligibility (OR) for demo", value=True)
796
- st.caption("عند إيقاف هذه الخيارات يعود السلوك إلى المنطق الأصلي الصارم (AND) بدون أي موظف إضافي.")
 
797
 
798
  # Status
799
  status_placeholder = st.empty()
@@ -862,7 +902,7 @@ S101,ICU,Registered Nurse,ICU-trained,BLS/ALS certified,Night,12h,A&E,Arabic,202
862
  st.markdown(render_cw_timeline(events), unsafe_allow_html=True)
863
  st.markdown('</div>', unsafe_allow_html=True)
864
 
865
- # RIGHT: Overview + KPIs + Employee Cards
866
  with right:
867
  total_steps = len(events)
868
  total_ms = sum(int(e.get("dur_ms", 0) or 0) for e in events)
@@ -883,10 +923,11 @@ S101,ICU,Registered Nurse,ICU-trained,BLS/ALS certified,Night,12h,A&E,Arabic,202
883
 
884
  st.markdown(render_kpis(assigned, notified, skipped), unsafe_allow_html=True)
885
 
886
- # ====== Employee glass cards (instead of a plain table) ======
887
  st.markdown("### 👥 Employee Results")
888
- emp_cards_html = render_employee_cards(shift_assignment_results, reasoning_rows)
889
- st.markdown(emp_cards_html, unsafe_allow_html=True)
 
890
 
891
  # Optional: raw tables (hidden by default) for auditing/export
892
  with st.expander("Raw Summary (optional)", expanded=False):
 
1
  """
2
+ Health Matrix – AI Command Center (Desktop Wide + AI Cards + Showcase Mode + Kanban Board)
3
  - Sticky Left Timeline (Chris Wright–style, animated)
4
  - KPI: Animated gradient-border cards
5
+ - Employee Results:
6
+ * Board View (Kanban: Assigned / Notify / Skipped) واضح ومناسب للأعداد الكبيرة
7
+ * Grid View (النسخة السابقة) خيار إضافي
8
+ - Showcase Mode: يضمن (Assign / Notify / Skip) لعرض قدرات الوكيل + يضيف موظف ثالث تجريبي (اختياري)
9
+ - Lenient Eligibility (OR) خيار للعرض
10
  - Full desktop width, responsive, WCAG-friendly
11
+ - لا تغييرات على المنطق/الـAPIs عند إيقاف وضع العرض
12
 
13
  Usage: streamlit run streamlit_app.py
14
  """
 
39
  --bg:#FFFFFF; --soft:#F8FAFC; --line:#E5E7EB;
40
  --t1:#0F172A; --t2:#1F2937; --muted:#64748B;
41
  --blue:#004C97; --green:#36BA01; --ok:#16a34a; --warn:#D97706; --fail:#DC2626;
42
+ --tint-assign:#E8F7ED; --tint-assign-b:#B6E2C4;
43
+ --tint-notify:#EFF6FF; --tint-notify-b:#BFDBFE;
44
+ --tint-skip:#FFF7ED; --tint-skip-b:#FED7AA;
45
  }
46
 
47
  /* ==== Full-width page on desktop ==== */
 
127
  .kpi-card .lbl{ color:#334155 }
128
  .kpi-card .val{ font-size:1.8rem; font-weight:900; line-height:1 }
129
 
130
+ /* ================= Employee Cards – base ================= */
 
 
 
 
131
  .emp-card{
 
132
  position:relative; overflow:hidden;
133
+ background: rgba(255,255,255,.9);
134
  border:1px solid #E5E7EB; border-radius:16px; backdrop-filter: blur(10px);
135
  box-shadow:0 10px 26px rgba(0,0,0,.08);
136
+ padding:12px; transform: translateY(6px); opacity:0;
137
  animation: cardIn .35s ease-out forwards;
138
  }
 
 
 
139
  .emp-card::before{ /* Aurora glow */
140
  content:""; position:absolute; inset:-30% -10% auto -10%; height:60%;
141
  background:
142
+ radial-gradient(600px 280px at 20% 30%, rgba(54,186,1,.18), transparent 60%),
143
+ radial-gradient(520px 220px at 80% 20%, rgba(0,76,151,.16), transparent 60%);
144
+ filter: blur(28px); opacity:.7; z-index:0; pointer-events:none;
145
  animation: aurora 18s linear infinite;
146
  }
147
  .emp-head, .emp-body, .emp-foot{ position:relative; z-index:1 }
148
+ .emp-head{ display:flex; align-items:center; gap:10px; padding:6px 8px; border-radius:12px }
149
  .emp-avatar{
150
+ width:40px; height:40px; border-radius:999px; background:#F1F5F9;
151
  display:flex; align-items:center; justify-content:center; font-weight:800; color:#0f172a
152
  }
153
  .emp-name{ font-weight:900 }
 
174
  .emp-tag{ font-size:.8rem; padding:6px 10px; border-radius:999px; border:1px solid #E5E7EB; background:#fff }
175
  .emp-meta{ margin-left:auto; color:#64748B; font-size:.85rem }
176
 
177
+ /* ====== GRID mode (old view, tidy) ====== */
178
+ .emp-grid{
179
+ display:grid; gap:14px; margin-top:12px;
180
+ grid-template-columns: repeat(12, 1fr);
181
+ }
182
+ .emp-grid .emp-card{ grid-column: span 4 } /* 3 per row desktop */
183
+ @media (max-width: 1320px){ .emp-grid .emp-card{ grid-column: span 6 } } /* 2 per row */
184
+ @media (max-width: 820px){ .emp-grid .emp-card{ grid-column: span 12 } .emp-body{ grid-template-columns: 1fr } }
185
+
186
+ /* ====== BOARD (Kanban) ====== */
187
+ .board{ display:grid; grid-template-columns:repeat(3, 1fr); gap:14px; margin-top:10px }
188
+ .lane{
189
+ background:#fff; border:1px solid var(--line); border-radius:16px;
190
+ box-shadow:0 6px 16px rgba(0,0,0,.06); padding:12px
191
+ }
192
+ .lane-header{
193
+ display:flex; align-items:center; gap:8px; margin-bottom:10px;
194
+ font-weight:900; font-size:1.05rem;
195
+ }
196
+ .lane-header .count{ margin-left:auto; font-size:.9rem; color:#64748B; background:var(--soft); padding:4px 10px; border-radius:999px; border:1px solid var(--line) }
197
+
198
+ .lane-assign{ background:var(--tint-assign) }
199
+ .lane-notify{ background:var(--tint-notify) }
200
+ .lane-skip{ background:var(--tint-skip) }
201
+
202
+ .emp-card.status-assign .emp-head{ background:var(--tint-assign); border:1px solid var(--tint-assign-b) }
203
+ .emp-card.status-notify .emp-head{ background:var(--tint-notify); border:1px solid var(--tint-notify-b) }
204
+ .emp-card.status-skip .emp-head{ background:var(--tint-skip); border:1px solid var(--tint-skip-b) }
205
 
206
  /* ======================= Chris Wright Timeline (adapted) ======================= */
207
  .cw-rail{ position:sticky; top:84px } /* pinned under Streamlit header */
 
437
  ]).lower()
438
 
439
  role_ok = (row.get("JobRole","").strip().lower() == role_req) or (role_req and role_req in text_all)
 
440
  spec_ok = (not specialty) or any(tok in text_all for tok in [specialty, specialty.replace(" ", ""), "icu"])
 
441
  training_ok = (not mandatory) or all(any(mtok in text_all for mtok in [m.lower(), m.lower().replace(" certified","")]) for m in mandatory)
442
  ward_ok = (not ward) or (ward in text_all)
443
  lang_ok = (not lang_req) or (lang_req in [l.lower() for l in (row.get("Languages") or [])])
 
579
  t_eval = time.perf_counter()
580
  events.append({"title":"Evaluate open shifts", "desc":"Matching employees vs. role/specialty/training/ward…", "status":"done"})
581
 
 
582
  elig_fn = is_eligible_lenient if lenient else is_eligible_strict
583
 
584
  for _, shift in df_shifts.iterrows():
 
587
  # 3) AI decision (or showcase distribution)
588
  t_ai = time.perf_counter()
589
  if showcase:
 
590
  ranked = df_employees.copy()
591
  ranked["__score"] = ranked.apply(lambda r: _score_match(r, shift), axis=1)
592
  ranked = ranked.sort_values("__score", ascending=False).reset_index(drop=True)
 
593
  cand_assign = ranked.iloc[0] if len(ranked) > 0 else None
594
  cand_notify = ranked.iloc[1] if len(ranked) > 1 else None
595
  cand_skip = ranked.iloc[-1] if len(ranked) > 2 else None
 
629
  events.append({"title":"Skipped", "desc":"No eligible employees or decision skipped", "status":"done"})
630
  shift_assignment_results.append(("❌ No eligible", shift["ShiftID"], "⚠️ Skipped"))
631
 
632
+ # Reasoning rows
633
  for _, emp_row in df_employees.iterrows():
 
634
  role_match = str(shift.get("RoleRequired","")).strip().lower() == emp_row.get("JobRole","").strip().lower()
635
  spec = str(shift.get("Specialty","")).replace("-", " ").strip().lower()
636
  text_all = (emp_row.get("JobRole","")+" "+emp_row.get("organizationPath","")+" "+" ".join(emp_row.get("Certifications",[]))).lower()
 
683
  </div>
684
  """
685
 
686
+ def _name_initials(n:str)->str:
687
+ n = (n or "").strip()
688
+ if not n: return "?"
689
+ parts = [p for p in n.split(" ") if p]
690
+ if len(parts)==1: return parts[0][:2].upper()
691
+ return (parts[0][0]+parts[-1][0]).upper()
692
+
693
+ def _chip_class(txt:str)->str:
694
+ t = (txt or "").lower()
695
+ if "auto-filled" in t or "assigned" in t: return "ok"
696
+ if "notify" in t: return "info"
697
+ if "skip" in t: return "warn"
698
+ if "eligible" in t and "✅" in (txt or ""): return "ok"
699
+ if "eligible" in t and "❌" in (txt or ""): return "fail"
700
+ return "info"
701
+
702
+ def _status_lane_for(emp_action:str, eligible_txt:str)->str:
703
+ """assign|notify|skip"""
704
+ a = (emp_action or "").lower()
705
+ if "auto-filled" in a or "assign" in a: return "assign"
706
+ if "notify" in a: return "notify"
707
+ if a: return "skip"
708
+ # لا يوجد قرار صريح: انقل المؤهل إلى notify للتدقيق، والباقي skip
709
+ return "notify" if str(eligible_txt).startswith("✅") else "skip"
710
+
711
+ def _build_emp_card(name:str, shift:str, chip:str, reasoning:str, certs:str, lane:str)->str:
712
+ checks = [x.strip() for x in str(reasoning or "").split("|") if x and x.strip()]
713
+ check_html = []
714
+ for ch in checks:
715
+ ok = "✅" in ch or ch.endswith("✅")
716
+ label = ch.replace("✅","").replace("❌","").strip()
717
+ check_html.append(f'<div class="emp-badge {"ok" if ok else "fail"}"><span class="dot"></span><span>{label}</span></div>')
718
+ chip_class = _chip_class(chip)
719
+ return f"""
720
+ <div class="emp-card status-{lane}" role="group" aria-label="{name}">
721
+ <div class="emp-head">
722
+ <div class="emp-avatar">{_name_initials(name)}</div>
723
+ <div>
724
+ <div class="emp-name">{name}</div>
725
+ <div style="color:#64748B;font-size:.85rem">Shift: {shift or "—"}</div>
726
+ </div>
727
+ <div class="emp-chip {chip_class}" title="Decision/Status">{chip}</div>
728
+ </div>
729
+ <div class="emp-line"></div>
730
+ <div class="emp-body">
731
+ {''.join(check_html) if check_html else '<div class="emp-badge info"><span class="dot" style="background:#60a5fa"></span><span>No reasoning available</span></div>'}
732
+ </div>
733
+ <div class="emp-foot">
734
+ <div class="emp-tag">🪪 Certifications: {certs or "—"}</div>
735
+ <div class="emp-meta">Updated just now</div>
736
+ </div>
737
+ </div>
738
+ """
739
+
740
+ def render_employee_view(shift_results: List[tuple], reasoning_rows: List[Dict[str,Any]], mode:str="board") -> str:
741
  """
742
+ mode = 'board' (Kanban) or 'grid'
 
 
743
  """
744
+ # map actions
745
  action_by_emp: Dict[str, str] = {}
746
  shift_by_emp: Dict[str, str] = {}
747
  for emp, sid, status in shift_results:
 
749
  action_by_emp[str(emp)] = status
750
  shift_by_emp[str(emp)] = str(sid)
751
 
752
+ # aggregate employees (first reasoning row per employee)
753
  seen = set()
754
  employees: List[Dict[str,Any]] = []
755
  for r in reasoning_rows:
756
  emp = (r.get("Employee") or "").strip()
757
+ if not emp or emp in seen: continue
 
758
  seen.add(emp)
759
  employees.append({
760
  "name": emp,
761
  "shift": r.get("ShiftID",""),
762
  "eligible": r.get("Eligible",""),
763
  "reasoning": r.get("Reasoning",""),
764
+ "certs": r.get("Certifications",""),
765
+ "action": action_by_emp.get(emp,"")
766
  })
767
 
768
  if not employees:
769
  return '<div class="table-wrap"><div style="padding:10px;color:#64748B">No employee details available.</div></div>'
770
 
771
+ if mode == "grid":
772
+ cards = []
773
+ for emp in employees:
774
+ lane = _status_lane_for(emp["action"], emp["eligible"])
775
+ chip = emp["action"] or ("✅ Eligible" if str(emp["eligible"]).startswith("✅") else "❌ Not Eligible")
776
+ cards.append(_build_emp_card(emp["name"], emp["shift"], chip, emp["reasoning"], emp["certs"], lane))
777
+ return '<div class="emp-grid">' + "".join(cards) + '</div>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
778
 
779
+ # Board (Kanban)
780
+ lanes = {"assign": [], "notify": [], "skip": []}
781
+ for emp in employees:
782
+ lane = _status_lane_for(emp["action"], emp["eligible"])
783
+ chip = emp["action"] or ("✅ Eligible" if str(emp["eligible"]).startswith("✅") else "❌ Not Eligible")
784
+ lanes[lane].append(_build_emp_card(emp["name"], emp["shift"], chip, emp["reasoning"], emp["certs"], lane))
785
+
786
+ def lane_html(title:str, icon:str, klass:str, items:List[str]) -> str:
787
+ return f"""
788
+ <div class="lane lane-{klass}">
789
+ <div class="lane-header">{icon}&nbsp;{title} <span class="count">{len(items)}</span></div>
790
+ {''.join(items) if items else '<div style="color:#64748B;padding:10px">No employees in this lane.</div>'}
791
  </div>
792
+ """
793
 
794
+ return '<div class="board">' + \
795
+ lane_html("Assigned", "✅", "assign", lanes["assign"]) + \
796
+ lane_html("Notify", "📬", "notify", lanes["notify"]) + \
797
+ lane_html("Skipped", "⚠️", "skip", lanes["skip"]) + \
798
+ '</div>'
799
 
800
 
801
  # =============================================================================
 
811
  <div class="hero" dir="auto">
812
  <img src="https://www.healthmatrixcorp.com/web/image/website/1/logo/Health%20Matrix?unique=956ad7b" alt="Health Matrix" />
813
  <h1>AI Command Center – Smart Staffing & Actions</h1>
814
+ <p>Desktop-first view. Sticky left runline + KPI + <b>Kanban board</b> لنتائج الموظفين واضح للأعداد الكبيرة. Showcase اختياري.</p>
815
  </div>
816
  """, unsafe_allow_html=True
817
  )
 
828
  </div>
829
  """, unsafe_allow_html=True)
830
 
831
+ # Demo controls
832
  with st.expander("🔧 Demo / Showcase Controls", expanded=False):
833
  show_showcase = st.checkbox("Ensure Assign + Notify + Skip (adds a demo nurse if needed)", value=True)
834
  lenient_mode = st.checkbox("Lenient eligibility (OR) for demo", value=True)
835
+ view_mode = st.radio("Employee results view", options=["Board (Kanban)", "Grid"], index=0, horizontal=True)
836
+ st.caption("عند إيقاف هذه الخيارات يعود السلوك إلى المنطق الأصلي الصارم (AND).")
837
 
838
  # Status
839
  status_placeholder = st.empty()
 
902
  st.markdown(render_cw_timeline(events), unsafe_allow_html=True)
903
  st.markdown('</div>', unsafe_allow_html=True)
904
 
905
+ # RIGHT: Overview + KPIs + Employee View
906
  with right:
907
  total_steps = len(events)
908
  total_ms = sum(int(e.get("dur_ms", 0) or 0) for e in events)
 
923
 
924
  st.markdown(render_kpis(assigned, notified, skipped), unsafe_allow_html=True)
925
 
926
+ # ====== Employee board/grid ======
927
  st.markdown("### 👥 Employee Results")
928
+ mode = "board" if view_mode.startswith("Board") else "grid"
929
+ emp_html = render_employee_view(shift_assignment_results, reasoning_rows, mode=mode)
930
+ st.markdown(emp_html, unsafe_allow_html=True)
931
 
932
  # Optional: raw tables (hidden by default) for auditing/export
933
  with st.expander("Raw Summary (optional)", expanded=False):