ShinyaJ commited on
Commit
3a9767f
·
verified ·
1 Parent(s): 47a507a

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +102 -162
app.py CHANGED
@@ -12,16 +12,16 @@ try:
12
  except Exception:
13
  HAS_FUZZ = False
14
 
15
- APP_TITLE = "Ward Ranking Random Assigner"
16
  DESCRIPTION = """
17
  **Flow**
18
  1) Upload .csv/.xlsx
19
  2) Choose wards + set capacity
20
  3) Check Available columns
21
  4) Map by Auto-detect (Thai/English + fuzzy) or by numbers (1-based)
22
- 5) Clean → keep NAME/ID + selected wards; convert ranks to integers
23
- 6) Assignround-by-rank with random tie-breaking; respect capacity
24
- - Check: #students <= total capacity (shortage allowed, not exceed)
25
  """
26
 
27
  WARD_CHOICES = [
@@ -56,9 +56,9 @@ AUTO_MAP = {
56
  "NAME": ["ชื่อ-สกุล", "ชื่อ - สกุล", "fullname", "full name", "name", "student name"],
57
  "ID": ["รหัสนักศึกษา", "รหัส", "student id", "id", "studentid"],
58
  "Medical": ["อายุรศาสตร์", "medical"],
59
- "Medical_1": ["อายุรศาสตร์_1", "medical_1", "med_1", "med1"],
60
- "Medical_2": ["อายุรศาสตร์_2", "medical_2", "med_2", "med2"],
61
- "Surgical": ["ศัลยศาสตร์", "surgical", "surgery", "surg"],
62
  "Pediatric": ["เด็ก", "pediatric", "pediatrics"],
63
  "Community": ["ชุมชน", "community"],
64
  "Psychiatric": ["จิตเวช", "psychiatric"],
@@ -176,20 +176,92 @@ def build_cleaned_from_indices(df: pd.DataFrame,
176
  cleaned = cleaned[ordered]
177
  return cleaned
178
 
179
- def random_assign(cleaned: pd.DataFrame,
180
- capacities: Dict[str, int]) -> Tuple[pd.DataFrame, pd.DataFrame, Dict[str, int]]:
181
  wards = [w for w in cleaned.columns if w not in ("NAME", "ID")]
182
- cap = {w: int(capacities.get(w, 0)) for w in wards}
183
-
184
- assigned = pd.Series(index=cleaned.index, data=pd.NA, dtype="object")
185
- choice_no = pd.Series(index=cleaned.index, data=pd.NA, dtype="Int64")
186
-
187
  mr = 0
188
  for w in wards:
189
  m = cleaned[w].max(skipna=True)
190
  if pd.notna(m):
191
  mr = max(mr, int(m))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  for r in range(1, mr + 1):
194
  if all(c <= 0 for c in cap.values()):
195
  break
@@ -211,112 +283,9 @@ def random_assign(cleaned: pd.DataFrame,
211
  result = cleaned.copy()
212
  result["AssignedWard"] = assigned
213
  result["ChoiceNumber"] = choice_no
214
-
215
  not_assigned = result[result["AssignedWard"].isna()].copy()
216
  return result.fillna(""), not_assigned.fillna(""), cap
217
 
218
- # ===== Reporting helpers =====
219
- def ward_display(ward_key: str) -> str:
220
- en, th = WARD_LABELS.get(ward_key, (ward_key, ward_key))
221
- return f"{en} ({th})"
222
-
223
- def max_rank_in(cleaned: pd.DataFrame) -> int:
224
- wards = [w for w in cleaned.columns if w not in ("NAME", "ID")]
225
- mr = 0
226
- for w in wards:
227
- m = cleaned[w].max(skipna=True)
228
- if pd.notna(m):
229
- mr = max(mr, int(m))
230
- return int(mr)
231
-
232
- def make_rank1_report(cleaned: pd.DataFrame, capacities: Dict[str, int]) -> str:
233
- wards = [w for w in cleaned.columns if w not in ("NAME", "ID")]
234
- total_students = len(cleaned)
235
- total_capacity = sum(int(capacities.get(w, 0)) for w in wards)
236
- lines = []
237
- lines.append("## Rank 1 Results (การแสดงผลอันดับที่ 1)")
238
- lines.append("")
239
- lines.append(f"- **Total Students (จำนวนนักศึกษาทั้งหมด):** {total_students} students (คน)")
240
- lines.append(f"- **Total Capacity (ความจุรวม):** {total_capacity} people (คน)")
241
- lines.append("")
242
- header = "| Ward (วอร์ด) | Capacity (ความจุ) | Rank 1 Count (จำนวนเลือกอันดับ 1) | Students (รายชื่อนักศึกษา) |"
243
- sep = "|---|---:|---:|---|"
244
- lines += [header, sep]
245
- over = []
246
- under = []
247
- for w in wards:
248
- cap = int(capacities.get(w, 0))
249
- rank1_students = cleaned.loc[cleaned[w] == 1, "NAME"].astype(str).tolist()
250
- r1_count = len(rank1_students)
251
- display_students = ", ".join(rank1_students[:3]) + ("..." if r1_count > 3 else "")
252
- lines.append(f"| {ward_display(w)} | {cap} | {r1_count} | {display_students} |")
253
- if r1_count > cap:
254
- over.append((w, r1_count, cap))
255
- elif r1_count < cap:
256
- under.append((w, r1_count, cap))
257
- lines.append("")
258
- lines.append("### Additional Statistics (สถิติเพิ่มเติม)")
259
- lines.append("")
260
- if over:
261
- lines.append("**Wards where Rank 1 count exceeds capacity (วอร์ดที่มีคนเลือกอันดับ 1 เกินความจุ):**")
262
- for w, c, cap in over:
263
- lines.append(f"- {ward_display(w)}: {c} selected (capacity {cap})")
264
- else:
265
- lines.append("- No wards exceed capacity at Rank 1. (ไม่มีวอร์ดใดเกินความจุในอันดับ 1)")
266
- if under:
267
- lines.append("")
268
- lines.append("**Wards where Rank 1 count below capacity (วอร์ดที่มีคนเลือกอันดับ 1 น้อยกว่าความจุ):**")
269
- for w, c, cap in under:
270
- lines.append(f"- {ward_display(w)}: {c} selected (capacity {cap})")
271
- return "\n".join(lines)
272
-
273
- def make_rank_report(cleaned: pd.DataFrame, capacities: Dict[str, int], rank: int) -> str:
274
- wards = [w for w in cleaned.columns if w not in ("NAME", "ID")]
275
- lines = []
276
- lines.append(f"## Rank {rank} Results (การแสดงผลอันดับที่ {rank})")
277
- total_students = len(cleaned)
278
- total_capacity = sum(int(capacities.get(w, 0)) for w in wards)
279
- lines.append(f"- **Total Students (จำนวนนักศึกษาทั้งหมด):** {total_students} students (คน)")
280
- lines.append(f"- **Total Capacity (ความจุรวม):** {total_capacity} people (คน)")
281
- lines.append("")
282
- header = "| Ward (วอร์ด) | Capacity (ความจุ) | Rank {rank} Count (จำนวนเลือกอันดับ {rank}) | Students (รายชื่อนักศึกษา) |".format(rank=rank)
283
- sep = "|---|---:|---:|---|"
284
- lines += [header, sep]
285
- over, under = [], []
286
- for w in wards:
287
- cap = int(capacities.get(w, 0))
288
- names = cleaned.loc[cleaned[w] == rank, "NAME"].astype(str).tolist()
289
- cnt = len(names)
290
- sample = ", ".join(names[:3]) + ("..." if cnt > 3 else "")
291
- lines.append(f"| {ward_display(w)} | {cap} | {cnt} | {sample} |")
292
- if cnt > cap:
293
- over.append((w, cnt, cap))
294
- elif cnt < cap:
295
- under.append((w, cnt, cap))
296
- lines.append("")
297
- lines.append("**Additional Statistics (สถิติเพิ่มเติม):**")
298
- if over:
299
- lines.append("- Wards where count exceeds capacity (เกินความจุ):")
300
- for w, c, cap in over:
301
- lines.append(f" - {ward_display(w)}: {c} selected (capacity {cap})")
302
- else:
303
- lines.append("- No wards exceed capacity at this rank. (ไม่มีวอร์ดเกินความจุในอันดับนี้)")
304
- if under:
305
- lines.append("- Wards where count below capacity (ต่ำกว่าความจุ):")
306
- for w, c, cap in under:
307
- lines.append(f" - {ward_display(w)}: {c} selected (capacity {cap})")
308
- return "\n".join(lines)
309
-
310
- def make_all_ranks_report(cleaned: pd.DataFrame, capacities: Dict[str, int]) -> str:
311
- mr = max_rank_in(cleaned)
312
- if mr == 0:
313
- return "No ranking numbers found. (ไม่พบข้อมูลอันดับเป็นตัวเลข)"
314
- parts = []
315
- for r in range(1, mr + 1):
316
- parts.append(make_rank_report(cleaned, capacities, r))
317
- parts.append("\n---\n")
318
- return "\n".join(parts)
319
-
320
  # ===== Helpers for temp file paths =====
321
  def _tmp(name: str) -> str:
322
  os.makedirs("/tmp", exist_ok=True)
@@ -424,29 +393,9 @@ def _capacities_from_df(cleaned: pd.DataFrame, capacity_df: Optional[pd.DataFram
424
  capacities[str(row["Ward"])] = 0
425
  return capacities
426
 
427
- def on_rank1_report(file, selected_wards, capacity_df, name_num, id_num,
428
- med_num, med1_num, med2_num, surg_num, ped_num, comm_num, psy_num, obs_num):
429
- df, msg = read_table(file)
430
- if df is None:
431
- return "Please upload a valid file."
432
- n_cols = len(df.columns)
433
- ward_nums = {
434
- "Medical": med_num, "Medical_1": med1_num, "Medical_2": med2_num,
435
- "Surgical": surg_num, "Pediatric": ped_num, "Community": comm_num,
436
- "Psychiatric": psy_num, "Obstetrics": obs_num
437
- }
438
- errors, mapping_idx = collect_mapping_numbers(name_num, id_num, ward_nums, selected_wards, n_cols)
439
- if errors:
440
- return "❌ Mapping invalid:\n" + "\n".join(errors)
441
- try:
442
- cleaned = build_cleaned_from_indices(df, mapping_idx)
443
- except Exception as e:
444
- return f"❌ Error building cleaned data: {e}"
445
- capacities = _capacities_from_df(cleaned, capacity_df)
446
- return make_rank1_report(cleaned, capacities)
447
-
448
- def on_all_ranks_report(file, selected_wards, capacity_df, name_num, id_num,
449
- med_num, med1_num, med2_num, surg_num, ped_num, comm_num, psy_num, obs_num):
450
  df, msg = read_table(file)
451
  if df is None:
452
  return "Please upload a valid file."
@@ -464,7 +413,7 @@ def on_all_ranks_report(file, selected_wards, capacity_df, name_num, id_num,
464
  except Exception as e:
465
  return f"❌ Error building cleaned data: {e}"
466
  capacities = _capacities_from_df(cleaned, capacity_df)
467
- return make_all_ranks_report(cleaned, capacities)
468
 
469
  def on_assign(file, selected_wards, capacity_df, name_num, id_num,
470
  med_num, med1_num, med2_num, surg_num, ped_num, comm_num, psy_num, obs_num):
@@ -482,7 +431,6 @@ def on_assign(file, selected_wards, capacity_df, name_num, id_num,
482
  }
483
  _errors, mapping_idx = collect_mapping_numbers(name_num, id_num, ward_nums, selected_wards, n_cols)
484
  cleaned = build_cleaned_from_indices(df, mapping_idx)
485
-
486
  capacities = _capacities_from_df(cleaned, capacity_df)
487
 
488
  total_capacity = sum(capacities.values())
@@ -500,9 +448,9 @@ def on_assign(file, selected_wards, capacity_df, name_num, id_num,
500
  not_assigned.to_csv(not_assigned_path, index=False, encoding="utf-8-sig")
501
 
502
  leftover_text = "Remaining capacity (จำนวนรับที่เหลือ):\n" + "\n".join([f"- {ward_display(k)}: {v}" for k, v in leftover.items()])
503
- allocation = make_all_ranks_report(cleaned, capacities) + "\n\n---\n\n" + "## Allocation Summary (สรุปการจัดสรร)\n" # Keep the all-ranks context too
504
 
505
- return status, assigned.head(30), assigned_path, not_assigned_path, leftover_text, allocation
506
 
507
  with gr.Blocks(title=APP_TITLE) as demo:
508
  gr.Markdown(f"# {APP_TITLE}")
@@ -553,28 +501,20 @@ with gr.Blocks(title=APP_TITLE) as demo:
553
  outputs=[status, available, name_num, id_num,
554
  med_num, med1_num, med2_num, surg_num, ped_num, comm_num, psy_num, obs_num])
555
 
556
- # Reports (pre-assignment)
557
- rank1_btn = gr.Button("Show Rank 1 Report (ดูสรุปอันดับ 1)")
558
- rank1_report = gr.Markdown(label="Rank 1 Results (การแสดงผลอันดับที่ 1)")
559
- rank1_btn.click(
560
- fn=on_rank1_report,
561
- inputs=[file, selected_wards, capacity_df, name_num, id_num,
562
- med_num, med1_num, med2_num, surg_num, ped_num, comm_num, psy_num, obs_num],
563
- outputs=rank1_report
564
- )
565
 
566
- all_ranks_btn = gr.Button("Show All Ranks Report (ดูสรุปทุกอันดับ)")
567
- all_ranks_report = gr.Markdown(label="All Ranks Report (การแสดงผลทุกอันดับ)")
568
- all_ranks_btn.click(
569
- fn=on_all_ranks_report,
570
  inputs=[file, selected_wards, capacity_df, name_num, id_num,
571
  med_num, med1_num, med2_num, surg_num, ped_num, comm_num, psy_num, obs_num],
572
- outputs=all_ranks_report
573
  )
574
 
575
- with gr.Row():
576
- clean_btn = gr.Button("Clean data (ดูพรีวิว)", variant="primary")
577
-
578
  preview = gr.Dataframe(label="Cleaned preview (first 30 rows)", visible=True)
579
  cleaned_file = gr.File(label="Download cleaned.csv")
580
 
@@ -590,7 +530,7 @@ with gr.Blocks(title=APP_TITLE) as demo:
590
  assigned_file = gr.File(label="Download assigned.csv")
591
  not_assigned_file = gr.File(label="Download not_assigned.csv")
592
  leftover_text = gr.Textbox(label="Remaining capacity summary", interactive=False)
593
- allocation_report = gr.Markdown(label="All Ranks & Allocation Report")
594
 
595
  assign_btn.click(
596
  fn=on_assign,
 
12
  except Exception:
13
  HAS_FUZZ = False
14
 
15
+ APP_TITLE = "Ward Ranking Cleaner & Random Assigner (Auto-map + Stepwise Reports)"
16
  DESCRIPTION = """
17
  **Flow**
18
  1) Upload .csv/.xlsx
19
  2) Choose wards + set capacity
20
  3) Check Available columns
21
  4) Map by Auto-detect (Thai/English + fuzzy) or by numbers (1-based)
22
+ 5) **Clean** → keep NAME/ID + selected wards; convert ranks to integers
23
+ 6) **Show Stepwise Report** Rank 1 results → random assign rank 1 → Rank 2 results → random assign until done
24
+ 7) (Optional) Assign button runs the full allocation too and provides CSVs to download
25
  """
26
 
27
  WARD_CHOICES = [
 
56
  "NAME": ["ชื่อ-สกุล", "ชื่อ - สกุล", "fullname", "full name", "name", "student name"],
57
  "ID": ["รหัสนักศึกษา", "รหัส", "student id", "id", "studentid"],
58
  "Medical": ["อายุรศาสตร์", "medical"],
59
+ "Medical_1": ["อายุรศาสตร์_1", "medical_1", "med_1"],
60
+ "Medical_2": ["อายุรศาสตร์_2", "medical_2", "med_2"],
61
+ "Surgical": ["ศัลยศาสตร์", "surgical", "surgery"],
62
  "Pediatric": ["เด็ก", "pediatric", "pediatrics"],
63
  "Community": ["ชุมชน", "community"],
64
  "Psychiatric": ["จิตเวช", "psychiatric"],
 
176
  cleaned = cleaned[ordered]
177
  return cleaned
178
 
179
+ def max_rank_in(cleaned: pd.DataFrame) -> int:
 
180
  wards = [w for w in cleaned.columns if w not in ("NAME", "ID")]
 
 
 
 
 
181
  mr = 0
182
  for w in wards:
183
  m = cleaned[w].max(skipna=True)
184
  if pd.notna(m):
185
  mr = max(mr, int(m))
186
+ return int(mr)
187
+
188
+ # ===== Stepwise simulation & reports (random each round) =====
189
+ def simulate_stepwise_report(cleaned: pd.DataFrame, capacities: Dict[str, int]) -> str:
190
+ """Round-by-round report: show rank r results, then randomly assign rank r, update remaining capacity, continue."""
191
+ wards = [w for w in cleaned.columns if w not in ("NAME", "ID")]
192
+ total_students = len(cleaned)
193
+ cap = {w: int(capacities.get(w, 0)) for w in wards}
194
+ assigned = pd.Series(index=cleaned.index, data=False) # True if assigned already
195
+ assigned_ward = pd.Series(index=cleaned.index, data="", dtype="object")
196
+
197
+ mr = max_rank_in(cleaned)
198
+ lines = []
199
+ lines.append(f"### Total Students (จำนวนนักศึกษาทั้งหมด): {total_students} students (คน)")
200
+ lines.append(f"### Total Capacity (ความจุรวม): {sum(cap.values())} people (คน)")
201
+ lines.append("")
202
+
203
+ for r in range(1, mr + 1):
204
+ lines.append("\n---\n")
205
+ lines.append(f"## Rank {r} Results (การแสดงผลอันดับที่ {r})\n")
206
+ header = "| Ward (วอร์ด) | Remaining Capacity (ความจุคงเหลือ) | Rank {r} Count (จำนวนเลือกอันดับ {r}) | Students (รายชื่อนักศึกษา) |".format(r=r)
207
+ sep = "|---|---:|---:|---|"
208
+ lines += [header, sep]
209
+
210
+ # Show BEFORE assignment
211
+ for w in wards:
212
+ names = cleaned.loc[(~assigned) & (cleaned[w] == r), "NAME"].astype(str).tolist()
213
+ cnt = len(names)
214
+ sample = ", ".join(names[:3]) + ("..." if cnt > 3 else "")
215
+ lines.append(f"| {ward_display(w)} | {cap[w]} | {cnt} | {sample} |")
216
+
217
+ # Now perform random assignment at this rank
218
+ lines.append("")
219
+ lines.append(f"### Allocation at Rank {r} (การสุ่มจัดสรรในอันดับที่ {r})")
220
+ for w in wards:
221
+ candidates_idx = cleaned.index[(~assigned) & (cleaned[w] == r)].tolist()
222
+ if not candidates_idx or cap[w] <= 0:
223
+ lines.append(f"- {ward_display(w)}: No allocation (ไม่มีการจัดสรร)")
224
+ continue
225
+ if len(candidates_idx) <= cap[w]:
226
+ chosen = candidates_idx
227
+ else:
228
+ chosen = list(np.random.choice(candidates_idx, size=cap[w], replace=False))
229
+ assigned.loc[chosen] = True
230
+ assigned_ward.loc[chosen] = w
231
+ cap[w] -= len(chosen)
232
 
233
+ chosen_names = cleaned.loc[chosen, "NAME"].astype(str).tolist()
234
+ sample = ", ".join(chosen_names[:10]) + ("..." if len(chosen_names) > 10 else "")
235
+ lines.append(f"- {ward_display(w)} → Selected (คัดเลือก): {len(chosen_names)} | {sample}")
236
+
237
+ # After assignment at this rank
238
+ lines.append("")
239
+ lines.append("**Remaining capacity (จำนวนรับที่เหลือหลังรอบนี้):**")
240
+ for w in wards:
241
+ lines.append(f"- {ward_display(w)}: {cap[w]}")
242
+
243
+ # Final summary
244
+ lines.append("\n---\n")
245
+ lines.append("## Final Summary (สรุปสุดท้าย)")
246
+ for w in wards:
247
+ sel_names = assigned_ward[assigned_ward == w]
248
+ lines.append(f"- {ward_display(w)}: {len(sel_names)} assigned (จัดสรรแล้ว)")
249
+ unassigned = cleaned.loc[~assigned, "NAME"].astype(str).tolist()
250
+ lines.append(f"- Not assigned (ยังไม่ได้รับการจัดสรร): {len(unassigned)}")
251
+ if unassigned:
252
+ sample_un = ", ".join(unassigned[:15]) + ("..." if len(unassigned) > 15 else "")
253
+ lines.append(f" - {sample_un}")
254
+ return "\n".join(lines)
255
+
256
+ def random_assign(cleaned: pd.DataFrame,
257
+ capacities: Dict[str, int]) -> Tuple[pd.DataFrame, pd.DataFrame, Dict[str, int]]:
258
+ """Full allocation using the same stepwise logic (for CSV outputs)."""
259
+ wards = [w for w in cleaned.columns if w not in ("NAME", "ID")]
260
+ cap = {w: int(capacities.get(w, 0)) for w in wards}
261
+ assigned = pd.Series(index=cleaned.index, data=pd.NA, dtype="object")
262
+ choice_no = pd.Series(index=cleaned.index, data=pd.NA, dtype="Int64")
263
+
264
+ mr = max_rank_in(cleaned)
265
  for r in range(1, mr + 1):
266
  if all(c <= 0 for c in cap.values()):
267
  break
 
283
  result = cleaned.copy()
284
  result["AssignedWard"] = assigned
285
  result["ChoiceNumber"] = choice_no
 
286
  not_assigned = result[result["AssignedWard"].isna()].copy()
287
  return result.fillna(""), not_assigned.fillna(""), cap
288
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  # ===== Helpers for temp file paths =====
290
  def _tmp(name: str) -> str:
291
  os.makedirs("/tmp", exist_ok=True)
 
393
  capacities[str(row["Ward"])] = 0
394
  return capacities
395
 
396
+ def on_stepwise_report(file, selected_wards, capacity_df, name_num, id_num,
397
+ med_num, med1_num, med2_num, surg_num, ped_num, comm_num, psy_num, obs_num):
398
+ # Requires cleaned mapping to be valid
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  df, msg = read_table(file)
400
  if df is None:
401
  return "Please upload a valid file."
 
413
  except Exception as e:
414
  return f"❌ Error building cleaned data: {e}"
415
  capacities = _capacities_from_df(cleaned, capacity_df)
416
+ return simulate_stepwise_report(cleaned, capacities)
417
 
418
  def on_assign(file, selected_wards, capacity_df, name_num, id_num,
419
  med_num, med1_num, med2_num, surg_num, ped_num, comm_num, psy_num, obs_num):
 
431
  }
432
  _errors, mapping_idx = collect_mapping_numbers(name_num, id_num, ward_nums, selected_wards, n_cols)
433
  cleaned = build_cleaned_from_indices(df, mapping_idx)
 
434
  capacities = _capacities_from_df(cleaned, capacity_df)
435
 
436
  total_capacity = sum(capacities.values())
 
448
  not_assigned.to_csv(not_assigned_path, index=False, encoding="utf-8-sig")
449
 
450
  leftover_text = "Remaining capacity (จำนวนรับที่เหลือ):\n" + "\n".join([f"- {ward_display(k)}: {v}" for k, v in leftover.items()])
451
+ stepwise_md = simulate_stepwise_report(cleaned, capacities)
452
 
453
+ return status, assigned.head(30), assigned_path, not_assigned_path, leftover_text, stepwise_md
454
 
455
  with gr.Blocks(title=APP_TITLE) as demo:
456
  gr.Markdown(f"# {APP_TITLE}")
 
501
  outputs=[status, available, name_num, id_num,
502
  med_num, med1_num, med2_num, surg_num, ped_num, comm_num, psy_num, obs_num])
503
 
504
+ # >>> Moved CLEAN button here (before reports) <<<
505
+ clean_btn = gr.Button("Clean data (ดูพรีวิว)", variant="primary")
506
+
507
+ # Reports (stepwise randomized)
508
+ step_btn = gr.Button("Show Stepwise Report (ดูรายงานทีละอันดับแบบสุ่ม)")
509
+ step_report = gr.Markdown(label="Stepwise Report (รายงานรอบต่อรอบ)")
 
 
 
510
 
511
+ step_btn.click(
512
+ fn=on_stepwise_report,
 
 
513
  inputs=[file, selected_wards, capacity_df, name_num, id_num,
514
  med_num, med1_num, med2_num, surg_num, ped_num, comm_num, psy_num, obs_num],
515
+ outputs=step_report
516
  )
517
 
 
 
 
518
  preview = gr.Dataframe(label="Cleaned preview (first 30 rows)", visible=True)
519
  cleaned_file = gr.File(label="Download cleaned.csv")
520
 
 
530
  assigned_file = gr.File(label="Download assigned.csv")
531
  not_assigned_file = gr.File(label="Download not_assigned.csv")
532
  leftover_text = gr.Textbox(label="Remaining capacity summary", interactive=False)
533
+ allocation_report = gr.Markdown(label="Stepwise Report (ผลการสุ่มรอบต่อรอบ)")
534
 
535
  assign_btn.click(
536
  fn=on_assign,