If late/early/OT > 30min → mark as abnormal status (override status_code+text, zero out minutes)
This commit is contained in:
+27
-13
@@ -299,7 +299,7 @@ async def upload_excel(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_user: User = Depends(get_current_user)
|
current_user: User = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""Upload Excel file — saves file AND imports rows into SQL table."""
|
"""Upload Excel file - saves file AND imports rows into SQL table."""
|
||||||
if section not in ["attendance", "accident"]:
|
if section not in ["attendance", "accident"]:
|
||||||
raise HTTPException(status_code=400, detail="Invalid section")
|
raise HTTPException(status_code=400, detail="Invalid section")
|
||||||
|
|
||||||
@@ -742,6 +742,20 @@ def calculate_attendance_status(check_in, check_out, shift_start, shift_end):
|
|||||||
result["status_code"] = "normal"
|
result["status_code"] = "normal"
|
||||||
|
|
||||||
result["status_text"] = " / ".join(status_parts) if status_parts else "正常"
|
result["status_text"] = " / ".join(status_parts) if status_parts else "正常"
|
||||||
|
|
||||||
|
# If late / early / OT > 30 min → mark as abnormal (override after status_text is set)
|
||||||
|
if result["late_minutes"] > 30 or result["early_minutes"] > 30 or result["ot_minutes"] > 30:
|
||||||
|
result["status_code"] = "abnormal"
|
||||||
|
if result["late_minutes"] > 30:
|
||||||
|
result["status_text"] = f"異常-遲到{result['late_minutes']}分"
|
||||||
|
elif result["early_minutes"] > 30:
|
||||||
|
result["status_text"] = f"異常-早退{result['early_minutes']}分"
|
||||||
|
else:
|
||||||
|
result["status_text"] = f"異常-OT{result['ot_minutes']}分"
|
||||||
|
result["late_minutes"] = 0
|
||||||
|
result["early_minutes"] = 0
|
||||||
|
result["ot_minutes"] = 0
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -2121,7 +2135,7 @@ async def _import_accident_to_db(db: Session, headers, rows, user_id: int) -> di
|
|||||||
Col 8 涉及的患者或相關人員 -> patient_or_staff
|
Col 8 涉及的患者或相關人員 -> patient_or_staff
|
||||||
Col 9 事件描述 -> long_description
|
Col 9 事件描述 -> long_description
|
||||||
Col 10 如有事件相關照片可提供-> incident_photos
|
Col 10 如有事件相關照片可提供-> incident_photos
|
||||||
Col 11 處理情況/已採取的行動-> action_taken
|
Col 11 處理情況/已採取的行動-> action_taken
|
||||||
Col 12 需要管理層介入的事項 -> needs_escalation
|
Col 12 需要管理層介入的事項 -> needs_escalation
|
||||||
Col 13 第 12 欄 -> (skipped)
|
Col 13 第 12 欄 -> (skipped)
|
||||||
Col 14 分數 -> incident_score
|
Col 14 分數 -> incident_score
|
||||||
@@ -2839,7 +2853,7 @@ async def accident_export_excel(
|
|||||||
# User types case_no in C5 of 個案查詢 sheet.
|
# User types case_no in C5 of 個案查詢 sheet.
|
||||||
# xlsxwriter requires single quotes around sheet names containing non-ASCII.
|
# xlsxwriter requires single quotes around sheet names containing non-ASCII.
|
||||||
# Use proper xlsxwriter hyperlink element (write_url with internal:Sheet!Cell).
|
# Use proper xlsxwriter hyperlink element (write_url with internal:Sheet!Cell).
|
||||||
# HYPERLINK formula would cache value 0 — write_url sets display text directly.
|
# HYPERLINK formula would cache value 0 - write_url sets display text directly.
|
||||||
overview.write_url(row_n, 6,
|
overview.write_url(row_n, 6,
|
||||||
f"internal:個案查詢!A1",
|
f"internal:個案查詢!A1",
|
||||||
link_fmt,
|
link_fmt,
|
||||||
@@ -2847,7 +2861,7 @@ async def accident_export_excel(
|
|||||||
tip=f"跳到「個案查詢」輸入個案 ID {r.id}")
|
tip=f"跳到「個案查詢」輸入個案 ID {r.id}")
|
||||||
overview.set_row(row_n, 18)
|
overview.set_row(row_n, 18)
|
||||||
|
|
||||||
# ============ Charts block — 統計圖表 (Sheet 1 總覽 右側 columns I-N) ============
|
# ============ Charts block - 統計圖表 (Sheet 1 總覽 右側 columns I-N) ============
|
||||||
# 4 charts: Severity Pie, Severity Column, Department Bar TOP 10, Monthly Trend Line
|
# 4 charts: Severity Pie, Severity Column, Department Bar TOP 10, Monthly Trend Line
|
||||||
# Position: I column (right of KPI + 個案索引 blocks), so no overlap with case index.
|
# Position: I column (right of KPI + 個案索引 blocks), so no overlap with case index.
|
||||||
|
|
||||||
@@ -2957,7 +2971,7 @@ async def accident_export_excel(
|
|||||||
# ============ Sheet 3 (built FIRST so detail-VLOOKUP can reference it): 明細 ============
|
# ============ Sheet 3 (built FIRST so detail-VLOOKUP can reference it): 明細 ============
|
||||||
# Build it now so that 個案查詢 formulas can resolve its range properly.
|
# Build it now so that 個案查詢 formulas can resolve its range properly.
|
||||||
# However, xlsxwriter's defined_name allows forward reference; formula text is
|
# However, xlsxwriter's defined_name allows forward reference; formula text is
|
||||||
# what matters at runtime — even if the sheet is added later, the formula string
|
# what matters at runtime - even if the sheet is added later, the formula string
|
||||||
# '明細!A:O' will be valid when Excel opens the file.
|
# '明細!A:O' will be valid when Excel opens the file.
|
||||||
# We define 明細 here so that the formula range 明細!A2:O{max_row} resolves.
|
# We define 明細 here so that the formula range 明細!A2:O{max_row} resolves.
|
||||||
# But xlsxwriter requires sheets to be added in the order they're defined.
|
# But xlsxwriter requires sheets to be added in the order they're defined.
|
||||||
@@ -3017,12 +3031,12 @@ async def accident_export_excel(
|
|||||||
"align": "center", "num_format": "@"})
|
"align": "center", "num_format": "@"})
|
||||||
# B5 = label only (NOT merged with C5). Otherwise user-typed text goes to
|
# B5 = label only (NOT merged with C5). Otherwise user-typed text goes to
|
||||||
# B5 (top-left of merge) instead of C5 where the formula references.
|
# B5 (top-left of merge) instead of C5 where the formula references.
|
||||||
detail.write("B5", "輸入個案 ID:", input_label_fmt)
|
detail.write("B5", "輸入個案 ID:", input_label_fmt)
|
||||||
# C5 = case_no input cell (NOT merged)
|
# C5 = case_no input cell (NOT merged)
|
||||||
detail.write_string("C5", "", text_input_fmt)
|
detail.write_string("C5", "", text_input_fmt)
|
||||||
# Hint
|
# Hint
|
||||||
detail.merge_range("B6:C6",
|
detail.merge_range("B6:C6",
|
||||||
"提示:個案 ID 喺「總覽」個案索引或「明細」表嘅 id 欄。輸入後下面自動顯示。",
|
"提示:個案 ID 喺「總覽」個案索引或「明細」表嘅 id 欄。輸入後下面自動顯示。",
|
||||||
subtitle_fmt)
|
subtitle_fmt)
|
||||||
|
|
||||||
# Detail rows: each pulls from 明細!<col><row> via VLOOKUP keyed on id (col A)
|
# Detail rows: each pulls from 明細!<col><row> via VLOOKUP keyed on id (col A)
|
||||||
@@ -3061,7 +3075,7 @@ async def accident_export_excel(
|
|||||||
col_sev = col_index_lookup.get("severity", 6)
|
col_sev = col_index_lookup.get("severity", 6)
|
||||||
col_emp = col_index_lookup.get("employee_name", 3)
|
col_emp = col_index_lookup.get("employee_name", 3)
|
||||||
col_desc = col_index_lookup.get("description", 5)
|
col_desc = col_index_lookup.get("description", 5)
|
||||||
# Use INDIRECT or simply use cell reference for ID — but ID varies per row.
|
# Use INDIRECT or simply use cell reference for ID - but ID varies per row.
|
||||||
# Easier: each row's ID is in B<row_n>; build formula with that.
|
# Easier: each row's ID is in B<row_n>; build formula with that.
|
||||||
# VLOOKUP needs lookup_value, table, col_index, FALSE.
|
# VLOOKUP needs lookup_value, table, col_index, FALSE.
|
||||||
# table 明細!$A:$O.
|
# table 明細!$A:$O.
|
||||||
@@ -3108,13 +3122,13 @@ async def accident_export_excel(
|
|||||||
compare_start_row = list_header_row + n_list_rows + 3
|
compare_start_row = list_header_row + n_list_rows + 3
|
||||||
detail.write(f"B{compare_start_row}", "兩個個案 並列比較 Side-by-Side", section_fmt)
|
detail.write(f"B{compare_start_row}", "兩個個案 並列比較 Side-by-Side", section_fmt)
|
||||||
detail.write(f"B{compare_start_row + 1}",
|
detail.write(f"B{compare_start_row + 1}",
|
||||||
"輸入兩個個案 ID (左個案 A, 右個案 B),對比每個欄位。空白表示無資料。",
|
"輸入兩個個案 ID (左個案 A, 右個案 B),對比每個欄位。空白表示無資料。",
|
||||||
subtitle_fmt)
|
subtitle_fmt)
|
||||||
|
|
||||||
cmp_label_row = compare_start_row + 2
|
cmp_label_row = compare_start_row + 2
|
||||||
detail.write(cmp_label_row, 1, "個案 A ID:", input_label_fmt)
|
detail.write(cmp_label_row, 1, "個案 A ID:", input_label_fmt)
|
||||||
detail.write_string(cmp_label_row, 2, "", text_input_fmt)
|
detail.write_string(cmp_label_row, 2, "", text_input_fmt)
|
||||||
detail.write(cmp_label_row, 4, "個案 B ID:", input_label_fmt)
|
detail.write(cmp_label_row, 4, "個案 B ID:", input_label_fmt)
|
||||||
detail.write_string(cmp_label_row, 5, "", text_input_fmt)
|
detail.write_string(cmp_label_row, 5, "", text_input_fmt)
|
||||||
detail.set_column("F:F", 50)
|
detail.set_column("F:F", 50)
|
||||||
|
|
||||||
@@ -3146,7 +3160,7 @@ async def accident_export_excel(
|
|||||||
|
|
||||||
# Hint at top
|
# Hint at top
|
||||||
detail.merge_range(f"B{cmp_data_start + len(detail_fields) + 2}:C{cmp_data_start + len(detail_fields) + 2}",
|
detail.merge_range(f"B{cmp_data_start + len(detail_fields) + 2}:C{cmp_data_start + len(detail_fields) + 2}",
|
||||||
"💡 全部公式 (VLOOKUP),無需啟用巨集。Excel/Numbers 都正常運作。",
|
"💡 全部公式 (VLOOKUP),無需啟用巨集。Excel/Numbers 都正常運作。",
|
||||||
subtitle_fmt)
|
subtitle_fmt)
|
||||||
|
|
||||||
# ============ Sheet 3: 明細 ============
|
# ============ Sheet 3: 明細 ============
|
||||||
@@ -3411,7 +3425,7 @@ async def export_excel(
|
|||||||
earliest = min((r.date for r in rows if r.date), default=None)
|
earliest = min((r.date for r in rows if r.date), default=None)
|
||||||
overview.merge_range("F7:G7", earliest.isoformat() if earliest else "-", kpi_value_fmt)
|
overview.merge_range("F7:G7", earliest.isoformat() if earliest else "-", kpi_value_fmt)
|
||||||
|
|
||||||
# Status distribution table — simplified 6-group (B10:D)
|
# Status distribution table - simplified 6-group (B10:D)
|
||||||
overview.write("B10", "出勤狀態分佈", section_fmt)
|
overview.write("B10", "出勤狀態分佈", section_fmt)
|
||||||
overview.write("B11", "Status", header_fmt_ov)
|
overview.write("B11", "Status", header_fmt_ov)
|
||||||
overview.write("C11", "數量", header_fmt_ov)
|
overview.write("C11", "數量", header_fmt_ov)
|
||||||
|
|||||||
Reference in New Issue
Block a user