績效考核管理系統 — AI 開發規格文件
本文件只保留開發時必須知道的規範與業務規則。 參考資料已搬至
Docs/dev/,AI 按需讀取即可。
系統概覽
- 系統名稱:績效考核管理系統(Performance Appraisal Management, P.A.M)
- 後端:C# / .NET 8 + Web API(7 個 Controller,170+ 支 API)
- 前端:React 19 + TypeScript + Vite + Tailwind CSS + TanStack Query + Zustand
- 資料庫:Microsoft SQL Server(MSSQL),EF Core
- 部署:公司內部伺服器(Intranet)
技術架構
後端
| 層級 | 說明 |
|---|---|
| Controllers | AuthController、HrController、ReviewController、NotificationController、BugReportController、AnnouncementController、TaglineController |
| Services | EmailService(SMTP,含附件)、ExcelService(ClosedXML)、AuditLogService(稽核日誌)、InterviewPdfService(面談表 PDF 套表)、OverdueNotificationService(催繳排程)、GradingService(核心公式單一來源)、ReviewValidator(提交前驗證)、SettlementService(年度結算)、ProtectionService(人員管理保護)、GradeReviewService(部門確認/退回)、ReviewerAccountService(主管帳號 lifecycle)、ReviewerTransferService(主管交接:保留記錄換配置 + Shift 遞補)、Hrms104Repository(104 HRMS 直連) |
| Helpers | Helpers/HttpContextExtensions.cs.GetRealClientIp() — 取得真實 client IP(優先 X-Forwarded-For / X-Real-IP) |
| Tests | ExamSystem.Tests/(xUnit + EF InMemory + Moq,223 筆:GradingService 36 / ReviewValidator 58 / ReviewController 19 / HrController 32 / ParticipantImportService 14 / GradeReviewService 18 / TaglineController 13 / AnnouncementController 13 / NotificationController 16 / 其他 4) |
| Models | Models/Models.cs — 所有 Entity 類別 |
| Data | Data/ExamDbContext.cs — EF Core DbContext |
| Auth | JWT Bearer Token(8 小時過期)+ BCrypt 密碼雜湊 |
前端
| 目錄 | 說明 |
|---|---|
client-app/src/pages/hr/ | HR 管理頁面(17 頁,含 AnnualDetailPage、AnnouncementsPage、TaglinesPage) |
client-app/src/pages/review/ | 主管考核頁面(7 頁,含 TodayPage 今日要事) |
client-app/src/components/AnnualDetailModal.tsx | 年度考核總覽彈窗(年度結算頁點擊員工使用) |
client-app/src/pages/auth/ | 登入、修改密碼、忘記密碼 |
client-app/src/components/ui/ | 共用 UI 元件 |
client-app/src/lib/api.ts | API 呼叫封裝(apiCall, apiUpload) |
client-app/src/stores/ | Zustand 狀態管理(authStore) |
開發工具
| 檔案 | 用途 |
|---|---|
restart-backend.bat | 關閉舊後端 → 重新編譯啟動 |
restart-frontend.bat | 關閉 port 5173 → 重啟 Vite |
restart-all.bat | 一次重啟前後端 |
gen_import.py | 從 104 HRMS 產出考核匯入名單 Excel |
gen_interview_pdf.py | 面談表 + 追蹤表 PDF 套表產出 |
gen_exam_result_pdf.py | 考核結果通知書 PDF 產出(年中/年終/年度) |
使用者角色(僅 4 種,一般員工不使用系統)
| 角色代碼 | 說明 |
|---|---|
HR | 人事管理員,全流程管理 |
Reviewer1 | 初核主管,填寫 A 分 |
Reviewer2 | 複核主管,填寫 B 分 |
Approver | 初審/複審/三審/終審主管 |
考核審核流程
初核(A) → 複核(B) → 初審 → 複審 → 三審 → 終審 → HR等第分配 → 年度結算
- Excel 名單中有填哪幾關審核主管,系統就執行哪幾關
- 沒填的關卡自動跳過,最多 6 關,最少 2 關
- 任何一關可退回上一關,退回時必須填寫退回原因
退回流程邏輯(重要)
退回時系統執行以下操作:
CurrentStep--(HR 退回已完成案件時 CurrentStep 不變)- 將
Step >= 退回步驟的舊 ExamRecord 標記為IsReturned = true - 新增一筆
IsReturned = true的退回記錄(含退回原因) - 清除
FinalGrade、FinalScore、ScoreBeforeAdjust、IsLocked - 清除
ForceFlag = "Normal"、ForceFlagReason = null
查詢 ExamRecords 的關鍵規則:
validRecords:必須過濾!r.IsReturned && r.Step <= p.CurrentStepreviewer2Record:當CurrentStep < 2時必須設為 nulllatestRecord(取 D 分):必須從validRecords中取- 前一關分數(prevA / prevRecord):必須加
r.Step < p.CurrentStep - 歷史紀錄排序:依
SubmittedAt(非 Step)
計算公式
// 當次平均分數 C
if (初核主管 == 複核主管) → C = A
else → C = (A * 0.5) + (B * 0.5)
// 當次考核成績 D
D = C + 出勤扣分(負數)
// 全年度成績
全年度 = (年中D + 年終D) ÷ 2 + 獎懲加減分
// 若只有年終:全年度 = 年終D + 獎懲加減分
核心公式單一來源(GradingService)
所有等第相關計算必須呼叫 Services/GradingService.cs,禁止各處自行實作重複邏輯。
| 方法 | 用途 |
|---|---|
CalcGrade(score) | 分數 → 等第(特優/優等/甲等/乙等/丙等) |
GradeRank(grade) | 等第 → 排序分數(比較大小用) |
ComputeScoreC(A, B, isMerged) | 當次平均分數 C(合併主管→C=A;否則(A+B)/2) |
ComputeScoreD(A, B, isMerged, attendance) | 當次成績 D = C + 出勤扣分 |
ComputeAttendanceDeduction(hours) | 事假時數 → 扣分(每小時 -0.125) |
ComputeByRate(n) / QuotaSuggestion | 人數 → 建議配額(餘額法,sum 永遠等於 n) |
GetSuggested(quota, grade) | 從 QuotaSuggestion 取對應等第建議數 |
CalcCeiling(...) | 等第天花板(懲處/假勤/年資 → 天花板等第 + 原因) |
HrController / ReviewController 的 CalculateGrade 與各處 fallback 皆已改為呼叫 GradingService。
新功能若需要等第判定或配額計算,一律在 GradingService 補方法,不在 Controller 重寫。
分數精度(重要)
所有分數欄位 DB schema 為 decimal(6,3)(原為 decimal(5,2),於 2026-04-17 遷移加寬)。允許 3 位小數精度,支援 0.125 公式(事假每小時扣 0.125 分)產生的分數如 -0.375 / 69.625 / 75.125。
- 涉及欄位:
ExamRecord.Score[A-I]、ExamRecord.AttendanceDeduction、ExamParticipant.FinalScore、ExamParticipant.ScoreBeforeAdjust、AnnualResult.*Score、GradeAdjustment.OriginalScore/AdjustedScore - 遷移:
Migrations/20260417113907_WidenScoreDecimalsTo6_3 - 歷史資料重算:
Docs/dev/recalc_scores.sql(修正舊 decimal(5,2) 累積的精度損失;亦可透過POST /api/hr/recalc-scores重算) - 前端顯示統一用
fmtScore(3 位小數,尾數 0 時降為 2 位)
列表分數顯示(即時重算,避免過時快照)
/review/my-list 的 latestScoreMap:Step 1/2 的 ScoreD 為提交當時的快照,若事假時數後續異動(104 重同步)會與考核表單的即時計算不一致。Step 1/2 記錄改用 ScoreC + 當前事假 × -0.125 即時重算;Step 3+ 才直接取 E~I(審核強制/繼承分數)。
草稿估算只套用自己:draftEstMap 只有在 isMyTurn && 我有草稿 時才覆蓋顯示,避免下游審核主管看到上游複核 WIP 的估算分數。
等第分配自動平衡(TryAutoBalanceCounts)
HR 已核定的等第分配人數(OutstandingCount/ExcellentCount/…)遇到 TotalCount 異動(部門主管 toggle、人員排除/恢復、DesignatedReviewer 異動)時,自動將差額吸收到甲等(最大桶),保持 IsConfirmed = true。僅差額過大致甲等變負數無法平衡才重置。位於 HrController.TryAutoBalanceCounts,套用於 ApplyTotalCount / GenerateGradeDistributionsAsync / RecalculateGradeDistributionAsync / GetGradeDistributionAsync 四處。
出勤扣分:僅事假扣分,每小時 -0.125(病假不扣分,僅影響等第天花板判定)
104 假勤同步假別:事假 S0001-1 + 事假-專案 S0001-3 + 半薪病假 S0002-1 + 無薪病假 S0015-1
考核表單年中假勤顯示
年終考核表單中,所有角色(含初核)都能看到年中的假勤數據(事假/病假時數),因為假勤是參考資訊非機密。但年中考核分數(D分、C分)僅複核以上可見。
獎懲加減分:大功 +9、小功 +3、嘉獎 +1、大過 -9、小過 -3、申誡 -1
等第審核與年度結算流程
完整流程(三階段)
第一階段:考核評分(per 週期)
初核(A) → 複核(B) → 初審 → 複審 → 三審 → 終審 → 部門全員完成
第二階段:等第審核(per 虛擬部門 × 週期)
部門全員完成 → 虛擬部門主管調整等第 → 提交 → HR確認 → 考核等第已確認
(全部虛擬部門都確認 → 週期鎖定 Locked)
第三階段:年度結算(per 虛擬部門)
考核等第已確認 → 虛擬部門主管調整年度總分 → 提交 → HR確認 → 年度考核已完成
(全部虛擬部門都確認 → 執行結算 Settle → 確認鎖定 Lock)
關鍵概念
- DepartmentConfirmation.ConfirmationType:
"GradeReview"(等第審核)vs"Settlement"(年度結算) - DepartmentConfirmation.Status:
Pending→Submitted→Confirmed(可Returned退回) - 等第審核和年度結算使用同一 API 端點,透過
?type=GradeReview/?type=Settlement區分 - 年度結算不需等到週期 Locked,只需該虛擬部門的等第審核已 Confirmed 即可開始
- 年度結算鎖定:全部虛擬部門 Settlement 都 Confirmed 後才可鎖定專案(
LockProjectAsync驗證) - 年度結算解鎖(退回):需輸入 HR 管理員密碼(
SystemSettings Sync104:Password),非使用者登入密碼 - 等第審核解鎖(退回):Confirmed 狀態 HR 點鎖頭按鈕退回時也需 HR 管理員密碼(比照年度結算);週期若已 Locked 會自動解鎖為 Active
- 年度等第檢查站:
AnnualNarrativeCheckedAt/AnnualNarrativeCheckedBy(獨立於週期等第檢查站NarrativeCheckedAt) - 年度結算確認前須通過兩道檢查站:(1) 年終考核等第檢查站 (2) 年度調分等第檢查站
分數調整記錄(GradeAdjustment)
| 欄位 | 說明 |
|---|---|
Scope | "MidYear" / "YearEnd" / "Annual" — 區分調分來源 |
ParticipantId | 關聯到 ExamParticipant |
OriginalScore / AdjustedScore | 調整前後分數 |
OriginalGrade / AdjustedGrade | 調整前後等第 |
查詢規則:
- 考核表單審核歷程:排除
Scope == "Annual"(年度調分不屬於週期考核) - 年度結算頁面:優先顯示
Scope == "Annual"的調分說明 - 等第審核頁面:顯示
Scope != "Annual"的調分說明
ExamParticipant 調分欄位
| 欄位 | 說明 |
|---|---|
ScoreBeforeAdjust | 等第調整前的原始 FinalScore(首次調整時記錄,多次調整不覆蓋) |
AdjustedAnnualScore | HR 調整的年度總分 |
AdjustedAnnualGrade | HR 調整的年度等第 |
AnnualNarrativeCheckedAt | 年度等第檢查站通過時間(HR 確認年度調分後的特優/乙丙評語) |
AnnualNarrativeCheckedBy | 年度等第檢查站確認人 |
退回時清除:FinalScore、FinalGrade、ScoreBeforeAdjust、IsLocked、ForceFlag
年度結算 API
POST /api/hr/projects/{projectId}/settle
- 以年終已完成參與者為基準(僅限等第審核已確認的虛擬部門)
- 逐人查詢年中成績 + 獎懲加減分
- HR 調整值優先:
AdjustedAnnualScore> 公式計算值 - 等第判定:
AdjustedAnnualGrade> 依分數自動判定 - Upsert 至
AnnualResults,專案狀態設為Settled
年度考核總覽 API
GET /api/hr/projects/{projectId}/annual-detail/{employeeId} — 整合年中 + 年終 + 獎懲 + 調分歷程
年度等第檢查站 API
POST /api/hr/participants/{id}/annual-narrative-check— 通過年度等第檢查DELETE /api/hr/participants/{id}/annual-narrative-check— 撤銷年度等第檢查
年度考核總覽分數顯示
- 考核已完成:使用
FinalScore - 考核進行中:使用
latestScoreD(最新 ExamRecord 的 ScoreD)作為 fallback - HR 角色不分狀態都可查看年度考核總覽
列表頁分數顯示規則(重要)
- 考核已完成(Status == “Completed”):永遠使用
FinalScore和FinalGrade - 考核進行中:初核主管看自己的 ScoreA;其他主管看最新提交分數
isReviewer1WithScoreA條件必須排除Status == "Completed",否則等第調整後列表不會更新
考核表單等第分配(GradeDistPanel)
- 位置:ScoreSummary 與 AttendanceDisciplinaryPanel 之間
- 分組邏輯:初核主管 →
Reviewer1 == myName;複核主管 →Reviewer2 == myName;HR/審核主管 →VirtualDeptName - 顯示該主管負責的全部考核人員的等第分布
出勤匯入頁面(AttendancePage)
- 假勤 tab:依考核週期同步,統計區間設定於週期設定(
LeaveCalcStartDate/LeaveCalcEndDate) - 獎懲 tab:依年度專案同步(無週期選擇器),統計區間設定於專案(
DisciplinaryCalcStartDate/DisciplinaryCalcEndDate) - 獎懲匯入後顯示:匯入 N 筆、對應成功 N 筆、N 筆未對應
- 未對應的獎懲紀錄(員工不在考核名單中)以琥珀色警告區塊顯示,含員工姓名與獎懲類型
- 假勤統計區間:顯示匯入資料的起迄日期
- 獎懲統計區間:唯讀顯示專案設定值(不在此頁輸入)
獎懲同步架構(重要)
- 獎懲為年度性質,儲存在專案層級(
ExamProject.DisciplinaryCalcStartDate/DisciplinaryCalcEndDate) - 同步 API:
POST /hr/projects/{projectId}/sync-104-disciplinary(非 period 層級) - Replace 模式清除同專案所有週期的獎懲記錄(
allProjectPeriodIds) - 獎懲統計區間設定:在考核週期「設定作業時間」對話框中輸入,但儲存至專案(
PUT /hr/projects/{id}/disciplinary-dates) - PeriodCard 顯示專案的獎懲統計區間(與假勤統計並列)
考核表單對應規則
string formType = (department == "業務部", jobLevel) switch
{
(true, "管理職") => "BizManager",
(true, "一般職") => "BizGeneral",
(false, "管理職") => "GenManager",
(false, "一般職") => "GenGeneral",
};- 評分選項(固定 10 個):0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0
- 強制修正分數 / 調整分數:0.5 單位 四捨五入(前端 onBlur 自動 round,例:75.3333 → 75.5)
- 具體事蹟(NarrativeReason ≥50 字)必填:只看主管自己打的原始分數(初核=A、複核=B、合併=A)落在特優/乙等/丙等時觸發。扣分造成的 D drop 不觸發(事由明確為出勤)。
- 例 1:A=70,B=70(甲等),D=69.625(乙等)→ 不強制具體事蹟(B 在甲等)
- 例 2:A=77.5,B=68(乙等),D=72.75(甲等)→ 強制具體事蹟(B 在乙等)
- 主管評語(Comment)必填:初核甲等(70-89)時必填;複核完全選填
- 內容驗證執行時機:存草稿時即擋(與天花板相同機制,參見
SaveDraftAsync);單筆送出 / 批次送出再次驗證為 safety net
等第規則
| 等第 | 分數區間 | 比率目標 | 上限 | 下限 |
|---|---|---|---|---|
| 特優 | 90+ | 8% | 12% | 5% |
| 優等 | 80-89 | 12% | 15% | 8% |
| 甲等 | 70-79 | 75% | 80% | 70% |
| 乙等 | 60-69 | 3% | 5% | 0% |
| 丙等 | <60 | 2% | 5% | 0% |
等第天花板法則(CalculateGradeCeiling)
- 有申誡以上懲處 → 不得列特優
- 有記過以上懲處 → 不得列優等
- 任職未滿 1 年 → 不得列特優
- 部門未滿 10 人:特優至多 1 人,優等以上不超過 1/4
- 事假超過 14 天 → 天花板為甲等
- 病假超過 30 天 → 天花板為甲等
天花板計算以年度為單位:假勤天數與獎懲紀錄合計同專案所有週期(年中+年終),非單一週期。
年資參考日以「評分當下(今天)」為準:避免使用未來的 period.EndDate 讓尚未滿一年者被放行。若週期已結束才取 EndDate(歷史視角)。位於 ReviewController.GetGradeCeilingAsync。
天花板檢查範圍:年中/年終皆硬性阻擋(SaveDraftAsync / SubmitReviewAsync / BatchSubmitAsync),不再只限 YearEnd。
前端天花板即時警告:
- 考核進行中:使用即時計算的分數(
watchedScores),打分時即時顯示,不需存檔 - 已完成案件:使用伺服器端
finalScore/finalGrade - 位置:ScoreSummary(分數摘要)下方
- 後端
GetGradeCeilingAsync:取得考核表詳情時即時計算天花板(即使從未存過檔也會回傳)
超額說明歷程(GradeOverrideLog)
超額/不足/主動填寫原因累積寫入 GradeOverrideLog(退回重送不覆蓋)。SourceType 區分 ReviewerBatch / DeptSupervisor / HrReturn。
📖 完整規格:OVERRIDE_LOG.md
異常標記(ForceFlag)
欄位 ExamParticipant.ForceFlag,預設 "Normal",非 Normal 時列入 HR 儀表板異常標記區。
| Flag 值 | 觸發條件 |
|---|---|
特優需評語 | 評分 ≥ 90 且主管未填評語 |
乙丙需評語 | 評分 < 70 且主管未填評語 |
懲處不得特優 | 有申誡以上懲處,但等第為特優 |
懲處不得優等 | 有記過以上懲處,但等第為優等 |
年資不得特優 | 任職未滿 1 年,但等第為特優 |
事假超量 | 年度事假合計 ≥ 14 天(112 小時),天花板為甲等 |
病假超量 | 年度病假合計 > 30 天(240 小時),天花板為甲等 |
等第超標 | 該虛擬部門某等第人數超過上限比率 |
分數等第不符 | 最終分數與最終等第的分數區間不一致 |
審核主管調分 | 審核主管覆寫了初核/複核分數 |
處理:HR 可退回主管修正 / 強制調整等第 / 確認無誤後清除標記
考核結案報表頁(ExamReportPage)
HR 查看各部門考核成績 + 等第統計,三個 Tab:各部門結案報告 / 週期/年度報告書 / 104 回寫 PDF 存檔。
Tab 1 含「單位 × 公司別」統計表(取代原等第統計)+ 人員清單;Excel 匯出 2 sheet(統計表 + 人員清單)、5 欄簽核、頁尾製表單位。
📖 完整規格:EXAM_REPORT.md
人員管理保護機制(CheckProtectedAsync)
週期/專案鎖定或虛擬部門已確認時,HR 修改人員管理需 HR 管理員密碼;驗證通過後自動解鎖週期/作廢確認。8 個受保護端點 + 前端兩層閘門(預檢 + fallback)。
📖 完整規格:PROTECTION.md
孤立 ExamRecord 防護
當 TotalReviewSteps 減少(如移除審核主管)時,可能產生 Step > TotalReviewSteps 的孤立 ExamRecord,導致 FinalScore 錯誤。
防護機制(3 層)
- UpdateReviewersAsync:減少關卡時自動刪除孤立 ExamRecord + ExamAnswers,重算 FinalScore
- SubmitDepartmentAsync:提交前檢查並自動修復孤立記錄,記錄稽核日誌
- 查詢規則:
validRecords過濾r.Step <= p.TotalReviewSteps
FinalScore 重算保護(重要)
孤立記錄清理後重算 FinalScore 時,必須優先使用 GradeAdjustment 調分值:
- 有 GradeAdjustment → 使用最後一筆的
AdjustedScore/AdjustedGrade(保護等第調整) - 沒有 GradeAdjustment → 從 ExamRecord 重算 ScoreD(原本行為)
跨 Controller 共用方法
ReviewController.GetFinalScore(record, totalSteps, isMerged)—internal static,根據 TotalReviewSteps 取最終分數ReviewController.CalculateGrade(score)—internal static,分數→等第轉換- HrController 有自己的
CalculateGrade(private static),功能相同
強制面談流程
候選人(乙丙等)→ HR確認面談名單 → 自動產PDF → 待通知 → 發信(附PDF)→ 待面談 → 掃描上傳 → 已完成
InterviewPdfService 呼叫 gen_interview_pdf.py 產 PDF;gen_import.py 依 JOB_CODE 決定管理職/一般職 + 審核鏈,排除特定主管與到職未滿 90 天。
📖 完整規格(含 PDF 套表欄位、gen_import 匯出規則):INTERVIEW.md
帳號管理(SystemUser 池)
帳號池架構
- SystemUser 是全域帳號池,匯入過的主管永遠存在,不會自動刪除
- PeriodUserMapping 記錄「帳號 ↔ 週期」對應關係,用於追蹤通知狀態(
NotifiedAt、PasswordSentAt) - HR/Admin 帳號:以
EmployeeNo登入 - 考核主管帳號:以姓名或
EmployeeNo登入
同步帳號生命週期(SyncAccountsAsync)
全域操作 — 影響所有非 HR/Admin 帳號:
- 在本期考核名單中 → 啟用 + 重設密碼為
@2026 - 不在本期名單中 → 停用
⚠️ 同一時間只能有一個進行中的週期,否則同步帳號會互相覆蓋
帳號管理頁面
- 預設「全部帳號」模式:不帶
periodId/projectId,顯示完整帳號池 - 選擇專案/週期後:只顯示 HR/Admin + 該週期的主管
- 帳號檢查(
check-missing):偵測該週期需要但缺少/已停用的帳號
專案刪除
- **進行中(Active)**專案不可刪除(前端隱藏按鈕)
- **已鎖定(Locked)/ 已結算(Settled)**可刪除,需輸入 HR 管理員密碼
- 刪除會連帶清除:ExamAnswers → ExamRecords → GradeAdjustments → ExamAttachments → DepartmentConfirmations → InterviewRecords → GradeDistributions → GradeOverrideLogs → DisciplinaryRecords → PeriodUserMappings → NotificationLogs → ExamParticipants → AnnualResults → ExamPeriods → ExamProject
- 不刪除:SystemUsers(帳號)、Employees(員工)、AuditLogs(稽核)
批次送出驗證(BatchSubmitAsync)
初核/複核主管批次送出草稿時,系統按單位(Unit) 檢查「整包送」規則(對應紙本流程:整包考核表裝信封袋才能往後送):
單位 key 解析順序
Employee.Unit → Employee.Department → VirtualDeptName(fallback)
規則
- 初核:單位內目前在初核(step 1)且 R1=我的人 → 全部有草稿才可送
- 複核:單位內 R2=我 且未通過複核(currentStep ≤ R2Step)的人 → 全員已到複核 + 全員有草稿 才可送
- 若有人仍在初核(currentStep < R2Step)→ 擋下,等初核主管把該人送上來
- 若有人因退回回到初核 → 擋下整單位,等他回到複核 + 重新存草稿
- 審核主管(初審/複審/三審/終審):可沿用前關分數,不需草稿;全選/批次時審核主管自動視為「可送」
為什麼按單位不按虛擬部門
- 單位(如「製作工程部-製播-攝影組」)= 有 1 位單位主管負責該單位一般職的初核
- 單位內考核「一起走」符合實務流程(單位主管也是批次送)
- 不同單位獨立推進,避免初核主管彼此等待
HR 紙本流程類比
- 整包考核表要全做完、裝信封袋 → 才能送下一關
- 少一份或有一份沒填完 → 不能封袋
- 退回 1 人 → 抽出那份、該份處理完再回包 → 一起再封袋送
All-or-nothing(重要)
批次送出為全有全無交易,只要任一筆驗證失敗,整批不寫入 DB。避免過去「部分送出部分留草稿」令使用者困惑的情況。
一次批次只能同一關卡(前端限制)
MyListPage 批次送出前會檢查 draftSelected 的 myRole 去重,size > 1 直接擋下並顯示 toast,要求使用者分別選取各關卡後送出。
原因:同時送出初核 + 複核時,分母/分子跨關卡混算會造成超額判定失真;分開送出讓每批的配額檢查對齊到單一關卡。
考核記錄跨專案轉移(MigrateRecordsAsync)
一次性 API POST /api/hr/migrate-records,用於將舊專案的考核記錄轉移到新專案。
轉移規則
- 以
EmployeeId配對來源/目標參與者 - 只轉移 Step 1(初核)和 Step 2(複核)的記錄(含草稿)
- Step 3+(初審/複審)的記錄不轉移(審核鏈可能不同)
- R1/R2 必須一致才轉移,不一致則跳過
- 目標已有非退回記錄 → 跳過(防重複轉移)
- 轉移後自動更新
CurrentStep和Status - 支援
dryRun: true預覽模式
主管交接(ReviewerTransferService)
考核進行中主管離職,保留 ExamRecord.ReviewerName 歷史、只換 ExamParticipant.Reviewer* 配置欄位;NewReviewerName 為空時 Shift 往前遞補(TotalReviewSteps -= 1)。人員管理頁批次調整主管 Dialog 切換「保留記錄 / 清空重評」兩模式。
API:POST /api/hr/participants/batch-transfer-reviewers (支援 DryRun 預覽)。
📖 完整規格:REVIEWER_TRANSFER.md
命名規範
資料庫
- 資料表 / 欄位:PascalCase
- 主鍵:一律
Id(INT IDENTITY) - 外鍵:
{資料表名}Id - 布林:
Is開頭,時間:At結尾 - 分數:
DECIMAL(5,2)
API
- Base URL:
/api,kebab-case - 統一回應:
{ success, data, message }
C# 程式碼
- 命名空間:
PerformanceAppraisal.{層級} - 類別/方法:PascalCase,非同步加
Async - 變數:camelCase,常數:UPPER_SNAKE_CASE
- 所有 DB 操作用 EF Core,禁止拼接 SQL
- 每支 API 須加
[Authorize] - Controller 不得 try-catch 吞掉例外
// ❌ 禁止
var sql = "SELECT * FROM Employees WHERE Name = '" + name + "'";
try { ... } catch { return Ok(); }
// ✅ 正確
var employees = await _db.Employees.Where(e => e.Name == name).ToListAsync();Git 分支
main ← 正式環境 feature/xxx ← 功能開發
develop ← 整合測試 hotfix/xxx ← 緊急修復
公告系統(Announcements)
登入頁「最新消息」與系統公告維護(HR 可增修)。含自動公告:GradeReviewService.ConfirmDepartmentAsync 確認後寫入「【部門】X 年 Y 考核已全數完成!」。
📖 完整規格(資料表/API/session chip/HR 後台):ANNOUNCEMENT.md
標語系統(Taglines)
登入頁 Hero + 今日要事 tagline 輪播的內容源(HR 在 /hr/taglines 增刪改)。Category 分 login(三段)/ today(兩段);預種 80 筆。
📖 完整規格:TAGLINE.md
今日要事(TodayPage · /review/today)
考核主管 / 虛擬部門主管登入後的任務導向首頁(取代 /review/dashboard)。頁面結構:頁首(問候 + hero tagline + 最新消息卡)+ 單位進度面板 + 三分組(需要先看 / 今日可做 / 已完成)。
📖 完整規格(頁面結構、預設展開規則、分組判斷、UnitProgressPanel):TODAY_PAGE.md
UI 色彩設計規範
主色:年代紅(Era Red)
--color-primary-500: #C00018(年代集團品牌色,定義於 index.css @theme)。原 Google 藍已替換為紅色色階 50~900。
設計原則(避免紅色過載)
- 紅色僅作為錨點與強調色,不做大面積背景/漸層(頁面頭部 banner、儀表板卡片等要避免
from-primary-*系列深色漸層) - 中性灰打底:頁首、列表、表格底色用
bg-gray-50/bg-white+border-gray-200 - 資料視覺化避開 primary:
- 進度條:
blue-300→blue-400→blue-600(關卡遞進淡→深) - 分析/圖表 icon 圈:
bg-indigo-50+text-indigo-600(等第比率監控、截止日倒數、等第分配進度) - 完成色保留
emerald-500
- 進度條:
- 連結/姓名可點:預設
text-gray-900,hovertext-primary-600(避免整欄紅字) - 次要操作:
text-gray-500hovertext-gray-700(展開/收合等) - 保留語意紅:違規/錯誤
text-red-700bg-red-50borderred-200(與 primary 紅可區分)
這些原則已套用於 ReviewDashboardPage.tsx 與 DashboardPage.tsx 的 banner 與 icon。
尚未實作
- 🟡 104 回寫 PDF 格式待微調 — 年中 / 年終 / 年度結算三種 PDF 版型細節(印章位置 / 簽核歷程 / 等第區塊 / 欄位對齊)待 HR 提供修改清單
- 🟡 單元測試:xUnit + EF InMemory + Moq(
ExamSystem.Tests,223 筆全綠)- GradingService 36 / ReviewValidator 58 / ReviewController 19 / HrController 32 / ParticipantImportService 14 / GradeReviewService 18 / TaglineController 13 / AnnouncementController 13 / NotificationController 16 / 其他 4
- 尚未覆蓋:EmailService / ExcelService
- 🟡 Service 層重構:
- ✅ 已抽出:GradingService(等第/天花板公式)、ReviewValidator(提交驗證)、SettlementService(年度結算)、ProtectionService(人員管理保護 CheckProtectedAsync + 密碼驗證)、Hrms104Repository(104 HRMS 直連 SQL)、ReviewerAccountService(主管帳號 lifecycle)、GradeReviewService(部門確認/退回 Confirm/Return Department + 主管端 SubmitDepartment)、ParticipantImportService(Sync104Employees + ImportEmployees 共用 upsert)
- HrController 行數:9,979 → 9,520 → 11,225(後續迭代成長)→ 2026/04/25 拆 7 個 partial 檔,主檔 7,433 行
- Partial 檔清單:
HrController.{Settlement,GradeDistribution,GradeAdjustment,NarrativeCheck,Interviews,Sync,Reports}.cs(共 3,792 行移出主檔;主類別宣告加partial修飾元,行為零變化,163 tests 全綠)
- ❌ 共用 PasswordModal + sudo-mode:本週期結束後與單一帳號權限設計一併處理(使用者決議延後)
參考文件(Docs/dev/)
核心參考
| 文件 | 內容 | 何時讀取 |
|---|---|---|
| API_REFERENCE.md | 160+ API 端點完整清單 | 新增/修改 API 時 |
| DATABASE.md | 29 DbSet 資料表清單 | 查詢資料表結構時 |
| PAGES.md | 23 個前端頁面路由對照 | 修改路由/新增頁面時 |
| DEPLOY.md | IIS 部署指南與設定 | 部署時 |
| NOTIFICATION.md | 催繳通知系統、範本變數、附件寄送 | 修改通知功能時 |
| EXCEL_IMPORT.md | Excel 匯入規格 + 104 整合 + gen_import.py 規則 | 修改匯入功能時 |
| CHANGELOG.md | 已完成功能清單 | 了解歷史變更時 |
功能模組(從 CLAUDE.md 拆出)
| 文件 | 內容 | 何時讀取 |
|---|---|---|
| EXAM_REPORT.md | 考核結案報表 3 Tab + 單位×公司統計 + Excel 匯出 | 修改結案報表時 |
| REVIEWER_TRANSFER.md | 主管交接(保留記錄換配置 + Shift 遞補) | 修改交接邏輯時 |
| TODAY_PAGE.md | 今日要事頁面結構、分組、UnitProgressPanel | 修改 /review/today 時 |
| PROTECTION.md | 人員管理保護機制、8 端點、密碼解鎖 | 動人員管理 API 時 |
| OVERRIDE_LOG.md | 超額說明歷程 DTO + 寫入規則 + 權限 | 修改超額歷程時 |
| INTERVIEW.md | 強制面談流程 + PDF 套表 + gen_import 規則 | 修改面談/匯入時 |
| ANNOUNCEMENT.md | 公告系統(含自動公告) | 修改公告時 |
| TAGLINE.md | 標語系統(login/today/return 三類) | 修改 tagline / 退回原因時 |
| COORDINATOR.md | 部門行政角色(Coordinator)Schema + API + TodayPage 追蹤區塊 | 修改行政追蹤時 |
| 104_WRITEBACK_FLOW.md | 104 回寫 9 階段 lifecycle(考核完成 → INSERT HRMS_EXAM + COPY PDF → 週期結案) | 修改 104 回寫 / 週期結案時 |
| EXAM_FORM_PDF.md | 考核表單 PDF 三模式(blank/half/filled)版面、分數可見性、附件合併、職別題目、API 規格 | 修改 gen_exam_form_pdf.py 或 ExamFormPdfService 時 |
最後更新:2026/04/25(A3 Service 重構 + Controller tests + ExcelService 清理 + ExcludedCandidates 候選池 + UI 統計 chip)· 年代集團資訊科技部
近期重大變更:
- 2026/04/25 晚間 — A3 Service 重構 + B 測試補強 + ExcelService 清理 + ExcludedCandidates 候選池:
- A3:
ReviewController.SubmitDepartmentAsync委派 GradeReviewService(~370 行 → ~22 行薄殼;天花板/配額/Confirmation/稽核全保留行為) - B:3 個 Controller 測試集 +60 筆(GradeReviewService 18 / TaglineController 13 / AnnouncementController 13 / NotificationController 16);測試 163 → 223 全綠
- ExcelService 清理 -637 行:移除假勤/獎懲/虛擬部門 3 條 Excel 匯入路徑(全改用 104 同步 / 手動 CRUD),對外 API 縮減為 4 支(ParseImportFileAsync / ExportReportAsync / ExportRatingRatioReport / ExportDeptUnitRatingRatioReport)
- ExcludedCandidates 候選池(migration
AddExcludedCandidates):解決「gen_import.py 用 today() 篩 90 天,但實際考核 1-2 個月後才開始」問題- gen_import.py 必填
--period-start YYYY-MM-DD+ 寫第 2 sheet「未列入名單」(HireDate90 / Resigned 兩類) - 後端
ExcelService.ParseExcludedCandidates+ParticipantImportService.ImportExcludedCandidatesAsync+ 3 支 API(GET/POST restore/DELETE) - 前端人員管理拆 2 Tab:「在考核名單」/「未列入名單」(含 amber badge),新元件
ExcludedCandidatesPanel:篩選/列表/加入考核 Modal/清除全部;說明口語化
- gen_import.py 必填
- UI 微調:篩選筆數 chip 移入篩選卡片右下角(無篩選=灰底「共 N 筆」/有篩選=主色 chip + 灰字全部數);操作提醒 +2 條(period-start 必填 + 未列入 Tab)
- 104 PDF 存檔年度部分修正:考核結案報表「年度總結」tab 部門列表改以年終受評為基準(
GetSettlementStatsAsync加YearEndCount/YearEndCompletedCount欄;前端 filteryearEndCount === 0部門 + 應考人數改用年終值);同邏輯與「受評 230 人(已排除年中受評但年終未列入 208 人)」對齊 - 104 回寫中心部門列表 header 加人數統計 chip:3 個 chip(部門 / 應考人數·已完成 / 已回寫進度);年度 tab 從
GetAnnualWritebackStatusAsync新增Depts: [{name, totalCount, completedCount}]+TotalPeople/TotalCompleted
- A3:
- 2026/04/25 — HrController 拆檔 + 效能健檢 + UX 微調:
- HrController.cs 拆 7 partial:主檔 11,225 → 7,433 行(移出 3,792 行);按功能切 Settlement / GradeDistribution / GradeAdjustment / NarrativeCheck / Interviews / Sync / Reports;主類別加
partial修飾元,行為零變化,163 tests 全綠 - 效能索引 migration(
AddPerformanceIndexes):新增 6 個複合索引(ExamRecord×2 / GradeOverrideLog / DisciplinaryRecord / GradeAdjustment / AuditLog),條件式 DropIndex 避免歷史 schema 殘留 - AsNoTracking 收斂:4 個唯讀 Controller(Announcement / BugReport / Notification / Tagline)13 處 read-only 查詢加
.AsNoTracking(),減少 EF change-tracker 開銷 - 104 年度成績回寫改 per-dept:年度 tab 從單一按鈕改為與年中/年終 tab 一致的
DepWritebackBlock(每部門一鍵 + 進度追蹤);資料源從AnnualResults(結算前為空)改為ExamParticipants(年終 VirtualDeptName) - 104 SMB 路徑可訪問性檢查:
writeback-statusAPI 新增smbConnected+smbError;前端銀行式雙 banner(DB / SMB);成功不曝路徑、失敗提示「請 HR 聯絡資訊科技部」 - UI 用語對齊:「專案尚未鎖定 → 至專案管理頁面鎖定專案」改「考核週期尚未鎖定 → 至專案管理頁面鎖定該考核週期」(年中/年終/面談 tab);Waiting 狀態徽章「待審核」→ 「等待中」(與分組標題「複核(等待中)」一致)
- HrController.cs 拆 7 partial:主檔 11,225 → 7,433 行(移出 3,792 行);按功能切 Settlement / GradeDistribution / GradeAdjustment / NarrativeCheck / Interviews / Sync / Reports;主類別加
- 2026/04/24 深夜 — 等第分配卡片分子分母對齊(4 視角,min~max 容許區間)、5 本 PAM 操作手冊按角色顯示、104 試打資料 cleanup SQL
- 2026/04/24 晚間 — 用語對齊「年底→年終」(48 檔 379 處 + DB 補丁)、面談表 PDF 表頭加民國年份、104 回寫操作人預檢補成功 banner
- 2026/04/24 — 考核結案報表年度總結基準改「年終受評者」、Excel Sheet 2 欄位統一、狀態欄 7 種年度進度標示
- 更早變更見 CHANGELOG.md