using Newtonsoft.Json; using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Windows.Forms; using 報到系統客戶端.Data; using 報到系統客戶端.Data.Models; using 報到系統客戶端.Data.Repositories; using 報到系統客戶端.Models; using 報到系統客戶端.Services; namespace 報到系統客戶端 { public partial class MainForm : Form { private string systemName = "新譽管理課程報到系統"; /// /// 是否作業中 /// private bool checkinOperating = false; private Color enlabedColor = Color.White; private Color disabledColor = Color.LightGray; private Color focusedColor = Color.FromArgb(255, 255, 200); private ProcessingForm processingForm = new ProcessingForm(); public MainForm() { InitializeComponent(); Text = string.Format("{0} ({1})", systemName, ClientSession.Version); cbCheckMode.SelectedIndex = 0; } private void SetupTextBoxFocusEffects() { // 定義輸入框列表 Control[] controls = { cbCurrentClass, cbCheckMode, txtQRCode, txtName, txtMobile, txtApiBaseUrl, txtIDNumber, txtEMail, cbIDType, cbMealType, cbGender, txtLossJobTimes }; Control[] menualCheckInControls = { txtName, txtMobile, txtIDNumber, txtEMail, cbIDType, cbMealType, cbGender, txtLossJobTimes }; foreach (Control control in controls) { control.Enter += (sender, e) => Control_Enter(sender, e); control.Leave += (sender, e) => Control_Leave(sender, e); if (control is System.Windows.Forms.ComboBox) { (control as System.Windows.Forms.ComboBox).DrawItem += comboBox_DrawItem; (control as System.Windows.Forms.ComboBox).MouseDown += comboBox_MouseDown; } } foreach (Control control in menualCheckInControls) { control.KeyPress += (sender, e) => { if (e.KeyChar == '\r' && checkinOperating) { ProcessManualCheckIn(); } }; } } private void Control_Enter(object sender, EventArgs e) { Control control = sender as Control; if (control != null) { // Focus 時:淺黃色背景 control.BackColor = focusedColor; control.ForeColor = Color.Black; // 展開 ComboBox if (control is System.Windows.Forms.ComboBox) { (control as System.Windows.Forms.ComboBox).DroppedDown = true; } } } private void Control_Leave(object sender, EventArgs e) { Control control = sender as Control; if (control != null) { // 失焦時:白色背景 control.BackColor = enlabedColor; control.ForeColor = Color.Black; } } private void comboBox_MouseDown(object sender, EventArgs e) { System.Windows.Forms.ComboBox cb = (System.Windows.Forms.ComboBox)sender; cb.DroppedDown = true; } private void comboBox_DrawItem(object sender, DrawItemEventArgs e) { if (e.Index < 0) return; var combobox = (sender as System.Windows.Forms.ComboBox); bool selected = (e.State & DrawItemState.Selected) == DrawItemState.Selected; bool disabled = (e.State & DrawItemState.Disabled) == DrawItemState.Disabled; Color backColor; Color foreColor; if (disabled) { backColor = disabledColor; foreColor = SystemColors.GrayText; } else if (selected) { backColor = SystemColors.Highlight; foreColor = SystemColors.HighlightText; } else { backColor = focusedColor; foreColor = SystemColors.ControlText; } using (SolidBrush b = new SolidBrush(backColor)) e.Graphics.FillRectangle(b, e.Bounds); string text = combobox.Items[e.Index].ToString(); if (combobox.DisplayMember != null) { var displayMemberProp = combobox.Items[e.Index].GetType().GetProperty(combobox.DisplayMember); if (displayMemberProp != null) { text = displayMemberProp.GetValue(combobox.Items[e.Index]).ToString(); } } TextRenderer.DrawText( e.Graphics, text, e.Font, e.Bounds, foreColor, TextFormatFlags.Left | TextFormatFlags.VerticalCenter); e.DrawFocusRectangle(); } private void UpdateSessionDisplay() { lbLoginUser.Text = string.Format("{0} {1}", ClientSession.UserID, ClientSession.UserName); lbOnlineMode1.Text = ClientSession.OnlineMode ? "連線" : "離線"; lbOnlineMode2.Text = ClientSession.OnlineMode ? "連線" : "離線"; lbLocalDB.Text = DatabaseService.Instance.DatabasePath; string apiUrl = ConfigService.Instance.GetApiBaseUrl(); txtApiBaseUrl.Text = apiUrl; lbApiBaseUrl.Text = apiUrl; btnDownload.Enabled = ClientSession.OnlineMode; btnUpload.Enabled = ClientSession.OnlineMode; btnRefreshClasses.Enabled = ClientSession.OnlineMode; } private void ClientSession_OnModeChanged(object sender, ModeChangeEventArgs e) { // 更新畫面 UpdateSessionDisplay(); // 連線模式時,定時上傳數據 if (e.OnlineMode) { // 啟動同步 Timer Processing = true; Application.DoEvents(); downloadWorker.RunWorkerAsync(); uploadTimer.Enabled = true; } else { LoadClassesData(); uploadTimer.Enabled = false; } } private void MainForm_Load(object sender, EventArgs e) { ClientSession.OnModeChange += ClientSession_OnModeChanged; ClientSession.ShowLoginForm(); // 偵測是否需要修復資料庫 if (!CheckLocalDB(false)) { if (MessageBox.Show("檢查到本地資料庫需要修復,是否現在修復?", "警告", MessageBoxButtons.YesNo, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button1) == DialogResult.Yes) { var repairResult = DatabaseService.Instance.AutoRepairDatabase(); if (repairResult.IsSuccessful) { MessageBox.Show("資料庫修復完成"); } else { MessageBox.Show("資料庫修復失敗,請至「系統設定」頁籤執行重建資料庫"); } } } // 設定 tabControl1 的樣式 tabControl1.DrawMode = TabDrawMode.OwnerDrawFixed; tabControl1.DrawItem += TabControl1_DrawItem; // 綁定 cbClasses 的選擇變更事件 cbClasses.SelectedIndexChanged += CbClasses_SelectedIndexChanged; // 綁定下載和上傳按鈕事件 btnDownload.Click += BtnDownload_Click; btnUpload.Click += BtnUpload_Click; // 綁定 txtQRCode KeyDown 事件(預約報名簽到簽退) txtQRCode.KeyDown += TxtQRCode_KeyDown; // 設定輸入框 focus 效果 SetupTextBoxFocusEffects(); // 清空結果顯示 ClearCheckResultDisplay(); } private void TabControl1_DrawItem(object sender, DrawItemEventArgs e) { TabPage page = tabControl1.TabPages[e.Index]; // 設定背景色 (根據分頁索引選擇相應的顏色) Color tabBackColor; if (e.Index == tabControl1.SelectedIndex) { // 選中時使用比分頁底色深一點的顏色 switch (e.Index) { case 0: // tabPage1 - 淺橘色 -> 深橘色 tabBackColor = Color.FromArgb(255, 200, 124); break; case 1: // tabPage2 - 淺青色 -> 深青色 tabBackColor = Color.FromArgb(150, 220, 220); break; case 2: // tabPage3 - 淺灰色 -> 深灰色 tabBackColor = Color.FromArgb(190, 190, 190); break; default: tabBackColor = Color.FromArgb(255, 220, 150); break; } } else { tabBackColor = SystemColors.Control; } e.Graphics.FillRectangle(new SolidBrush(tabBackColor), e.Bounds); // 設定字體 (粗體) Font tabFont; if (e.Index == tabControl1.SelectedIndex) { tabFont = new Font(tabControl1.Font, FontStyle.Bold); } else { tabFont = new Font(tabControl1.Font, FontStyle.Regular); } // 繪製文本 TextRenderer.DrawText( e.Graphics, page.Text, tabFont, e.Bounds, Color.Black, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPrefix ); } private void btnSaveApiBaseUrl_Click(object sender, EventArgs e) { bool success = ConfigService.Instance.SetApiBaseUrl(txtApiBaseUrl.Text.Trim()); if (success) { Console.WriteLine("API URL 已更新"); // 更新 ApiService 的 BaseUrl ApiService.Instance.BaseUrl = txtApiBaseUrl.Text.Trim(); // 更新畫面顯示 UpdateSessionDisplay(); } else { Console.WriteLine("API URL 更新失敗"); } } private void btnTestAPIConnection_Click(object sender, EventArgs e) { if (ClientSession.Ping()) { MessageBox.Show("連線測試成功"); } else { MessageBox.Show("連線測試失敗"); } } /// /// 載入活動清單資料 /// private void LoadClassesData() { try { ClassesRepository classes = new ClassesRepository(); var classList1 = classes.GetAll(); var classList2 = classes.GetAll(); if (classList1 != null && classList1.Count > 0) { // 綁定到 ComboBox cbClasses.DisplayMember = "DisplayText"; // 顯示課程名稱 cbClasses.ValueMember = "ID"; // 值為課程 ID cbClasses.DataSource = classList1; CbClasses_SelectedIndexChanged(cbClasses, new EventArgs()); cbCurrentClass.DisplayMember = "DisplayText"; // 顯示課程名稱 cbCurrentClass.ValueMember = "ID"; // 值為課程 ID cbCurrentClass.DataSource = classList2; } else { cbClasses.DataSource = null; cbClasses.Items.Clear(); cbCurrentClass.DataSource = null; cbCurrentClass.Items.Clear(); } } catch (Exception ex) { MessageBox.Show("載入課程清單時發生錯誤: " + ex.Message); cbClasses.DataSource = null; cbClasses.Items.Clear(); } } /// /// 當選擇活動時,取得相應統計資料 /// private void CbClasses_SelectedIndexChanged(object sender, EventArgs e) { if (cbClasses.SelectedValue == null) { // 清空統計資訊 lbServerCount.Text = "0"; lbLocalCount.Text = "0"; lbWaitingUploadCount.Text = "0"; lbCheckInCount.Text = "0"; lbCheckOutCount.Text = "0"; return; } int courseID = (int)cbClasses.SelectedValue; try { // 1. 取得後端報名人數 if (ClientSession.OnlineMode) { int serverCount = GetServerSignUpCount(courseID); lbServerCount.Text = serverCount.ToString(); } else { lbServerCount.Text = "(離線)"; } // 2. 取得本地已下載人數 int localCount = GetLocalSignUpCount(courseID); lbLocalCount.Text = localCount.ToString(); // 3. 取得資料未上傳筆數(本地有簽到簽退記錄但後端沒有更新的筆數) int waitingUploadCount = GetWaitingUploadCount(courseID); lbWaitingUploadCount.Text = waitingUploadCount.ToString(); // 4. 取得本地資料顯示出來 SignUpsRepository signUps = new SignUpsRepository(); var data = signUps.GetByCourseID(courseID); dgSignUp.DataSource = null; bsSignUp.DataSource = null; bsSignUp.DataSource = data; dgSignUp.DataSource = bsSignUp; // 5.統計人數 lbCheckInCount.Text = string.Format("{0}", data.Count(p => !string.IsNullOrWhiteSpace(p.CheckInTime))); lbCheckOutCount.Text = string.Format("{0}", data.Count(p => !string.IsNullOrWhiteSpace(p.CheckOutTime))); } catch (Exception ex) { MessageBox.Show("取得統計資料時發生錯誤: " + ex.Message); } } /// /// 取得後端報名人數 /// private int GetServerSignUpCount(int courseID) { try { // 呼叫後端 API 取得該活動的報名人數 BaseResponse response = SignUpService.SearchSignUps( pageIndex: 1, pageSize: 1, courseID: courseID, name: "", mobile: "" ); if (response.Success) { return response.TotalCount; } else { return 0; } } catch (Exception) { return 0; } } /// /// 取得本地已下載人數 /// private int GetLocalSignUpCount(int courseID) { try { // 從本地資料庫取得該活動的參加人員數 SignUpsRepository repo = new SignUpsRepository(); return repo.GetCount(courseID: courseID); } catch (Exception) { return 0; } } /// /// 取得資料未上傳筆數 /// private int GetWaitingUploadCount(int courseID) { try { CheckInRepository repo = new CheckInRepository(); return repo.GetByCourseID(courseID).Where(p => !p.UploadTime.HasValue).Count(); } catch (Exception) { return 0; } } /// /// 下載活動參加人員資料 /// private void BtnDownload_Click(object sender, EventArgs e) { if (cbClasses.SelectedValue == null) { MessageBox.Show("請先選擇課程", "提示"); return; } int courseID = (int)cbClasses.SelectedValue; try { CheckInRepository checkinRepo = new CheckInRepository(); SignUpsRepository signupRepo = new SignUpsRepository(); // 檢查是否有未上傳報到紀錄 if (checkinRepo.GetByCourseID(courseID).Where(p => !p.UploadTime.HasValue).Count() > 0) { MessageBox.Show("有未上傳報到紀錄,請先上傳", "提示"); return; } // 禁用按鈕防止重複點擊 btnDownload.Enabled = false; btnDownload.Text = "下載中..."; Processing = true; Application.DoEvents(); // 清除本地該活動所有參加人員以及報到紀錄 signupRepo.DeleteByCourseID(courseID); checkinRepo.DeleteByCourseID(courseID); // 呼叫 API 取得該活動的所有參加人員(分頁取得) int downloadedCount = 0; int pageIndex = 1; const int pageSize = 100; while (true) { BaseResponse response = SignUpService.SearchSignUps( pageIndex: pageIndex, pageSize: pageSize, courseID: courseID, name: "", mobile: "" ); if (!response.Success) { MessageBox.Show("下載失敗: " + response.Message, "錯誤"); return; } if (response.Data == null || response.Data.ToString() == "[]") { break; } // 轉換資料並保存到本地資料庫 SignUpList signUpList = ApiService.DeserializeObject(response.Data.ToString()); foreach (var item in signUpList) { // 轉換為本地模型 LocalSignUpItem localItem = LocalSignUpItem.ConvertFromApiModel(item); // 檢查是否已存在,存在則更新,不存在則新增 var existingItem = signupRepo.GetByCourseNameMobile(courseID, item.Name, item.Mobile); if (existingItem != null) { localItem.ID = existingItem.ID; signupRepo.Update(localItem); } else { signupRepo.Insert(localItem); } downloadedCount++; } // 如果取回的筆數小於 pageSize,表示已取完所有資料 if (signUpList.Count < pageSize) { break; } pageIndex++; } // 重新更新統計資訊 CbClasses_SelectedIndexChanged(null, null); MessageBox.Show($"下載成功,共下載 {downloadedCount} 筆資料", "成功"); } catch (Exception ex) { MessageBox.Show("下載時發生錯誤: " + ex.Message, "錯誤"); } finally { Processing = false; btnDownload.Enabled = true; btnDownload.Text = "下載課程人員"; } } /// /// 上傳本地簽到簽退資料 /// private void BtnUpload_Click(object sender, EventArgs e) { if (cbClasses.SelectedValue == null) { MessageBox.Show("請先選擇課程", "提示"); return; } int courseID = (int)cbClasses.SelectedValue; try { // 禁用按鈕防止重複點擊 btnUpload.Enabled = false; btnUpload.Text = "上傳中..."; Processing = true; Application.DoEvents(); // 呼叫上傳 ProcessUpload(courseID, true); } catch (Exception ex) { MessageBox.Show("上傳時發生錯誤: " + ex.Message, "錯誤"); } finally { Processing = false; btnUpload.Enabled = true; btnUpload.Text = "上傳報到資料"; } } /// /// 重新下載員工資料表 /// private void DownloadBPSNData() { try { // 呼叫 BPSNService 下載員工資料 BaseResponse response = BPSNService.Download(); if (response.Success && response.Data != null) { // 轉換資料為 BPSNList BPSNList bpsnList = ApiService.DeserializeObject(response.Data.ToString()); if (bpsnList != null && bpsnList.Count > 0) { // 建立本地資料庫 Repository BPSNRepository repo = new BPSNRepository(); // 清除現有資料 repo.DeleteAll(); // 新增下載的員工資料 int addedCount = 0; foreach (var item in bpsnList) { repo.Insert(new LocalBPSNItem() { BPENO = item.BPENO, BPNME = item.BPNME, BPAWD = item.BPAWD, Is系統管理群組 = item.Is系統管理群組 == 1, Is客戶管理群組 = item.Is客戶管理群組 == 1, Is現場工作群組 = item.Is現場工作群組 == 1, Is查詢群組 = item.Is查詢群組 == 1 }); addedCount++; } Console.WriteLine($"員工資料重新載入成功,共 {addedCount} 筆"); } else { MessageBox.Show("未取得任何員工資料", "提示"); } } else { MessageBox.Show("重新載入員工資料失敗: " + (response.Message ?? "未知錯誤"), "錯誤"); } } catch (Exception ex) { MessageBox.Show("重新載入員工資料時發生錯誤: " + ex.Message, "錯誤"); } } private void btnBeginEnd_Click(object sender, EventArgs e) { if (cbCurrentClass.Enabled) { checkinOperating = true; btnBeginEnd.Text = "停止作業"; } else { checkinOperating = false; btnBeginEnd.Text = "開始作業"; } SetInputControlEnabled(); txtQRCode.Focus(); } private void SetInputControlEnabled() { string checkMode = cbCheckMode.SelectedItem?.ToString() ?? "簽到"; bool isCheckIn = checkMode == "簽到"; cbCurrentClass.Enabled = !checkinOperating; cbCheckMode.Enabled = !checkinOperating; txtQRCode.Enabled = checkinOperating; btnMenualCheckIn.Enabled = checkinOperating; txtName.Enabled = checkinOperating; txtMobile.Enabled = checkinOperating; txtIDNumber.Enabled = checkinOperating && isCheckIn; txtEMail.Enabled = checkinOperating && isCheckIn; cbIDType.Enabled = checkinOperating && isCheckIn; cbMealType.Enabled = checkinOperating && isCheckIn; cbGender.Enabled = checkinOperating && isCheckIn; txtLossJobTimes.Enabled = checkinOperating && isCheckIn; cbCurrentClass.BackColor = cbCurrentClass.Enabled ? enlabedColor : disabledColor; cbCheckMode.BackColor = cbCheckMode.Enabled ? enlabedColor : disabledColor; txtQRCode.BackColor = txtQRCode.Enabled ? enlabedColor : disabledColor; txtName.BackColor = txtName.Enabled ? enlabedColor : disabledColor; txtMobile.BackColor = txtMobile.Enabled ? enlabedColor : disabledColor; txtIDNumber.BackColor = txtIDNumber.Enabled ? enlabedColor : disabledColor; txtEMail.BackColor = txtEMail.Enabled ? enlabedColor : disabledColor; cbIDType.BackColor = cbIDType.Enabled ? enlabedColor : disabledColor; cbMealType.BackColor = cbMealType.Enabled ? enlabedColor : disabledColor; cbGender.BackColor = cbGender.Enabled ? enlabedColor : disabledColor; txtLossJobTimes.BackColor = txtLossJobTimes.Enabled ? enlabedColor : disabledColor; } /// /// 預約報名 - QRCode 簽到簽退事件處理器 /// 當掃描器掃入 QRCode 時觸發 /// private void TxtQRCode_KeyDown(object sender, KeyEventArgs e) { // Enter 鍵觸發簽到簽退 if (e.KeyCode == Keys.Return) { e.Handled = true; ProcessScanCheckIn(); } } /// /// 處理 QRCode 簽到簽退邏輯 /// private void ProcessScanCheckIn() { try { // 先清空結果顯示 ClearCheckResultDisplay(); string qrCode = txtQRCode.Text.Trim(); // 驗證 QRCode 不為空 if (string.IsNullOrWhiteSpace(qrCode)) { DisplayScanCheckResult(false, "請掃碼"); return; } // 取得當前課程 ID if (cbCurrentClass.SelectedValue == null) { DisplayScanCheckResult(false, "請先選擇課程"); return; } int courseID = (int)cbCurrentClass.SelectedValue; string courseAgreedTerms = (cbCurrentClass.SelectedItem as LocalClassesItem)?.AgreedTerms; // 取得簽到簽退模式 string checkMode = cbCheckMode.SelectedItem?.ToString() ?? "簽到"; bool isCheckIn = checkMode == "簽到"; // 查詢本地資料庫 SignUpsRepository repo = new SignUpsRepository(); LocalSignUpItem signUpItem = repo.GetByQRCode(qrCode); if (signUpItem == null) { // 未查到 DisplayScanCheckResult(false, "無此筆資料"); txtQRCode.Text = string.Empty; return; } // 查到記錄,核對是否為本場次 if (signUpItem.CourseID != courseID) { DisplayScanCheckResult(false, "非本課程報到碼"); txtQRCode.Text = string.Empty; return; } var now = DateTime.Now; var nowStr = now.ToString("yyyy-MM-dd HH:mm:ss"); bool needUpdate = false; // 根據簽到簽退模式更新資料 if (isCheckIn) { // 簽到:只在 CheckInTime 為 NULL 時才寫入 if (string.IsNullOrWhiteSpace(signUpItem.CheckInTime)) { // TODO:此處檢查是否有同意確認事項,若有則要跳出確認視窗 if (!string.IsNullOrWhiteSpace(courseAgreedTerms)) { signUpItem.AgreementStatus = ShowAgreementConfirmDialog(courseAgreedTerms); } signUpItem.CheckInTime = nowStr; needUpdate = true; } } else { // 檢查是否沒有簽到而要簽退 if (string.IsNullOrWhiteSpace(signUpItem.CheckInTime)) { using (var checkoutOnlyForm = new CheckoutOnlyConfirmForm()) { checkoutOnlyForm.姓名 = signUpItem.Name; checkoutOnlyForm.手機號 = signUpItem.Mobile; checkoutOnlyForm.Top = 0; checkoutOnlyForm.Left = (this.Width - checkoutOnlyForm.Width) / 2; if (checkoutOnlyForm.ShowDialog() == DialogResult.No) { ClearInputsAndFocus(); return; } } } // 簽退:只在 CheckOutTime 為 NULL 時才寫入 if (string.IsNullOrWhiteSpace(signUpItem.CheckOutTime)) { signUpItem.CheckOutTime = nowStr; needUpdate = true; } } // 更新 SignUps 表 // 新增 CheckIn 紀錄 if (needUpdate) { repo.Update(signUpItem); CheckInRepository checkInRepo = new CheckInRepository(); LocalCheckInItem checkInItem = new LocalCheckInItem { CourseID = signUpItem.CourseID, Name = signUpItem.Name, Mobile = signUpItem.Mobile, SignUpType = signUpItem.SignUpType, AgreementStatus = signUpItem.AgreementStatus, CheckInType = isCheckIn ? "CHECKIN" : "CHECKOUT", CheckInTime = nowStr, CreatedAt = now, UpdatedAt = now }; checkInRepo.Insert(checkInItem); } // 顯示成功結果 DisplayScanCheckResult(true, needUpdate ? $"{cbCheckMode.Text}成功" : $"已{cbCheckMode.Text}"); DisplaySignUpInfo(signUpItem); // 播放音效 if (isCheckIn) Services.NotificationSoundService.CheckinOK(); else Services.NotificationSoundService.CheckoutOK(); // 清空 QRCode 輸入框,準備下一次掃碼 txtQRCode.Clear(); txtQRCode.Focus(); } catch (Exception ex) { // 異常處理:顯示錯誤訊息,不清空 UI DisplayScanCheckResult(false, $"處理簽到簽退時發生錯誤:{ex.Message}"); Console.WriteLine($"QRCode 簽到簽退異常:{ex.Message}"); } } /// /// 顯示簽到簽退結果(成功/失敗) /// private void DisplayScanCheckResult(bool success, string message) { if (success) { // 綠底白字 - 成功 lbCheckResult.BackColor = Color.Green; lbCheckResult.ForeColor = Color.White; } else { // 紅底白字 - 失敗 lbCheckResult.BackColor = Color.Red; lbCheckResult.ForeColor = Color.White; } lbCheckResult.Text = message; } /// /// 顯示報名人員資訊 /// private void DisplaySignUpInfo(LocalSignUpItem item) { if (item == null) { ClearCheckResultDisplay(); return; } lbSeqNo.Text = item.SeqNo.ToString(); lbName.Text = item.Name ?? string.Empty; lbMobile.Text = Utils.HideMobile(item.Mobile ?? string.Empty); lbCheckInTime.Text = item.CheckInTime; lbCheckOutTime.Text = item.CheckOutTime; } /// /// 清空簽到簽退顯示資訊 /// 這裡不處理輸入控制項的清空 /// private void ClearCheckResultDisplay() { lbCheckResult.Text = string.Empty; lbSeqNo.Text = string.Empty; lbName.Text = string.Empty; lbMobile.Text = string.Empty; lbCheckInTime.Text = string.Empty; lbCheckOutTime.Text = string.Empty; lbName2.Text = string.Empty; lbMobile2.Text = string.Empty; lbMenualCheckResult.Text = string.Empty; lbMenualCheckInTime.Text = string.Empty; lbMenualCheckOutTime.Text = string.Empty; } private void downloadWorker_DoWork(object sender, DoWorkEventArgs e) { downloadWorker.ReportProgress(0, "下載員工資料..."); DownloadBPSNData(); downloadWorker.ReportProgress(0, "下載課程資料..."); DownloadClassesData(); } private void downloadWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) { lbSyncJob.Text = e.UserState.ToString(); Application.DoEvents(); } private void downloadWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { lbSyncJob.Text = "(無)"; LoadClassesData(); Processing = false; } private void btnRefreshSignUp_Click(object sender, EventArgs e) { CbClasses_SelectedIndexChanged(sender, e); } private void btnMenualCheckIn_Click(object sender, EventArgs e) { ProcessManualCheckIn(); } /// /// 顯示簽到簽退結果(成功/失敗) /// private void DisplayMenualCheckResult(bool success, string message, LocalSignUpItem signUpItem) { if (success) { // 綠底白字 - 成功 lbMenualCheckResult.BackColor = Color.Green; lbMenualCheckResult.ForeColor = Color.White; } else { // 紅底白字 - 失敗 lbMenualCheckResult.BackColor = Color.Red; lbMenualCheckResult.ForeColor = Color.White; } lbMenualCheckResult.Text = message; lbName2.Text = signUpItem?.Name; lbMobile2.Text = Utils.HideMobile(signUpItem?.Mobile); txtMobile.Text = lbMobile2.Text; lbMenualCheckInTime.Text = !string.IsNullOrWhiteSpace(signUpItem?.CheckInTime) ? ExtractTimeFromDateTime(signUpItem?.CheckInTime) : string.Empty; lbMenualCheckOutTime.Text = !string.IsNullOrWhiteSpace(signUpItem?.CheckOutTime) ? ExtractTimeFromDateTime(signUpItem?.CheckOutTime) : string.Empty; } /// /// 處理現場報到邏輯 /// 支持簽到和簽退 /// private void ProcessManualCheckIn() { try { // 先清空結果顯示 ClearCheckResultDisplay(); // 驗證課程選擇 if (cbCurrentClass.SelectedValue == null) { DisplayMenualCheckResult(false, "請先選擇課程", null); cbCurrentClass.Focus(); return; } int courseID = (int)cbCurrentClass.SelectedValue; string courseAgreedTerms = (cbCurrentClass.SelectedItem as LocalClassesItem)?.AgreedTerms; // 驗證輸入資料 if (!ValidateInputs(out string name, out string mobile, out string idNumber, out string email, out int lossJobTimes)) { return; } // 取得簽到簽退模式 bool isCheckIn = cbCheckMode.SelectedItem?.ToString() == "簽到"; DateTime now = DateTime.Now; var nowStr = now.ToString("yyyy-MM-dd HH:mm:ss"); // 查詢或建立簽到記錄 SignUpsRepository repo = new SignUpsRepository(); LocalSignUpItem signUpItem = repo.GetByCourseNameMobile(courseID, name, mobile); bool isNewRecord = signUpItem == null; bool foundByNameOrMobile = false; // 簽到作業時,若姓名 + 手機號查不到,用姓名查再用手機號查 if (isNewRecord) { signUpItem = repo.GetByCourseNameMobile(courseID, name, null); foundByNameOrMobile = signUpItem != null; if (!foundByNameOrMobile) { signUpItem = repo.GetByCourseNameMobile(courseID, null, mobile); foundByNameOrMobile = signUpItem != null; } } if (foundByNameOrMobile && signUpItem.SignUpType == "預約") { using (var confirmForm = new ExistConfirmForm()) { confirmForm.姓名 = signUpItem.Name; confirmForm.手機號 = signUpItem.Mobile; confirmForm.Top = 0; confirmForm.Left = (this.Width - confirmForm.Width) / 2; switch (confirmForm.ShowDialog()) { case DialogResult.Yes: isNewRecord = false; break; case DialogResult.No: isNewRecord = true; break; case DialogResult.Cancel: return; } } } // 此處檢查是否有同意確認事項,若有則要跳出確認視窗 string agreementStatus = string.Empty; if (!string.IsNullOrWhiteSpace(courseAgreedTerms)) { agreementStatus = ShowAgreementConfirmDialog(courseAgreedTerms); } if (isNewRecord) { // 檢查是否沒有簽到而要簽退 if (!isCheckIn && signUpItem == null) { using (var checkoutOnlyForm = new CheckoutOnlyConfirmForm()) { checkoutOnlyForm.姓名 = name; checkoutOnlyForm.手機號 = mobile; checkoutOnlyForm.Top = 0; checkoutOnlyForm.Left = (this.Width - checkoutOnlyForm.Width) / 2; if (checkoutOnlyForm.ShowDialog() == DialogResult.No) { ClearInputsAndFocus(); return; } } } signUpItem = new LocalSignUpItem { CourseID = courseID, Name = name, Mobile = mobile, SignUpType = "現場", IDNumber = idNumber, Email = email, IDType = cbIDType.Text, MealType = cbMealType.Text, LossJobTimes = lossJobTimes, Gender = cbGender.Text, AgreementStatus = agreementStatus, CheckInTime = isCheckIn ? nowStr : string.Empty, CheckOutTime = isCheckIn ? string.Empty : nowStr, CreatedAt = now, UpdatedAt = now }; repo.Insert(signUpItem); AddCheckInRecord(signUpItem, isCheckIn, now); } else { // 更新現有記錄 signUpItem.AgreementStatus = agreementStatus; bool updated = UpdateCheckInTime(signUpItem, isCheckIn, now); if (updated) { repo.Update(signUpItem); AddCheckInRecord(signUpItem, isCheckIn, now); } } // 更新時間顯示 // DisplayCheckInTimes(signUpItem); // 顯示結果 string resultMessage = isNewRecord || !IsAlreadyChecked(signUpItem, isCheckIn) ? $"{cbCheckMode.Text}成功" : $"已{cbCheckMode.Text}"; DisplayMenualCheckResult(true, resultMessage, signUpItem); // 播放音效 if (isCheckIn) Services.NotificationSoundService.CheckinOK(); else Services.NotificationSoundService.CheckoutOK(); // 清空輸入框並設定焦點 ClearInputsAndFocus(); } catch (Exception ex) { MessageBox.Show($"現場報到時發生錯誤:{ex.Message}", "錯誤"); Console.WriteLine($"現場報到異常:{ex}"); } } /// /// 驗證輸入資料 /// private bool ValidateInputs(out string name, out string mobile, out string idNumber, out string email, out int lossJobTimes) { name = txtName.Text.Trim(); mobile = txtMobile.Text.Trim(); idNumber = txtIDNumber.Text.Trim().ToUpper(); email = txtEMail.Text.Trim(); lossJobTimes = 0; var str_lossJobTimes = txtLossJobTimes.Text.Trim(); if (string.IsNullOrWhiteSpace(name)) { DisplayMenualCheckResult(false, "請輸入姓名", null); txtName.Focus(); return false; } if (string.IsNullOrWhiteSpace(mobile)) { DisplayMenualCheckResult(false, "請輸入手機號", null); txtMobile.Focus(); return false; } if (!System.Text.RegularExpressions.Regex.IsMatch(mobile, @"^09\d{8}$")) { DisplayMenualCheckResult(false, "手機號格式錯誤", null); txtMobile.Focus(); return false; } // 驗證身分證字號(非必填,但如果輸入則驗證格式) if (!string.IsNullOrWhiteSpace(idNumber)) { if (!ValidateROCIDNumber(idNumber)) { DisplayMenualCheckResult(false, "身分證字號格式錯誤", null); txtIDNumber.Focus(); return false; } } // 驗證電子郵件(非必填,但如果輸入則驗證格式) if (!string.IsNullOrWhiteSpace(email)) { if (!ValidateEmail(email)) { DisplayMenualCheckResult(false, "電子郵件格式錯誤", null); txtEMail.Focus(); return false; } } // 驗證失業給付認定次數(非必填,但如果輸入則驗證) if (!string.IsNullOrWhiteSpace(str_lossJobTimes)) { if (!int.TryParse(str_lossJobTimes, out lossJobTimes)) { DisplayMenualCheckResult(false, "失業給付認定次數必須輸入數字", null); txtLossJobTimes.Focus(); return false; } else if (lossJobTimes < 0) { DisplayMenualCheckResult(false, "失業給付認定次數必須大於或等於 0", null); txtLossJobTimes.Focus(); return false; } } return true; } /// /// 驗證中華民國身分證字號 /// 規則:第一碼為英文字母(A-Z),後接九位數字 /// 使用檢查碼算法驗證 /// private bool ValidateROCIDNumber(string idNumber) { if (string.IsNullOrWhiteSpace(idNumber)) return false; idNumber = idNumber.Trim().ToUpper(); // 檢查長度 if (idNumber.Length != 10) return false; // 第一碼必須是英文字母 if (idNumber[0] < 'A' || idNumber[0] > 'Z') return false; // 後九碼必須是數字 for (int i = 1; i < 10; i++) { if (idNumber[i] < '0' || idNumber[i] > '9') return false; } // 驗證檢查碼 // 字母對應的數字 int[] letterValues = { 10, 11, 12, 13, 14, 15, 16, 17, 34, 18, 19, 20, 21, 22, 35, 23, 24, 25, 26, 27, 28, 29, 32, 30, 31, 33 }; int code = letterValues[idNumber[0] - 'A']; int sum = code / 10 + (code % 10) * 9; int[] weights = { 8, 7, 6, 5, 4, 3, 2, 1 }; for (int i = 1; i <= 8; i++) { sum += (idNumber[i] - '0') * weights[i - 1]; } sum += idNumber[9] - '0'; return sum % 10 == 0; } /// /// 驗證電子郵件格式 /// private bool ValidateEmail(string email) { if (string.IsNullOrWhiteSpace(email)) return false; try { var addr = new System.Net.Mail.MailAddress(email); return addr.Address == email; } catch { return false; } } /// /// 更新簽到簽退時間 /// private bool UpdateCheckInTime(LocalSignUpItem signUpItem, bool isCheckIn, DateTime now) { if (isCheckIn && string.IsNullOrWhiteSpace(signUpItem.CheckInTime)) { signUpItem.CheckInTime = now.ToString("yyyy-MM-dd HH:mm:ss"); return true; } else if (!isCheckIn && string.IsNullOrWhiteSpace(signUpItem.CheckOutTime)) { signUpItem.CheckOutTime = now.ToString("yyyy-MM-dd HH:mm:ss"); return true; } return false; } /// /// 檢查是否已經簽到或簽退 /// private bool IsAlreadyChecked(LocalSignUpItem signUpItem, bool isCheckIn) { return isCheckIn ? !string.IsNullOrWhiteSpace(signUpItem.CheckInTime) : !string.IsNullOrWhiteSpace(signUpItem.CheckOutTime); } /// /// 新增簽到檢查紀錄 /// private void AddCheckInRecord(LocalSignUpItem signUpItem, bool isCheckIn, DateTime now) { CheckInRepository checkInRepo = new CheckInRepository(); LocalCheckInItem checkInItem = new LocalCheckInItem { CourseID = signUpItem.CourseID, Name = signUpItem.Name, Mobile = signUpItem.Mobile, SignUpType = signUpItem.SignUpType, IDNumber = signUpItem.IDNumber, IDType = signUpItem.IDType, MealType = signUpItem.MealType, Email = signUpItem.Email, LossJobTimes = signUpItem.LossJobTimes, Gender = signUpItem.Gender, AgreementStatus = signUpItem.AgreementStatus, CheckInType = isCheckIn ? "CHECKIN" : "CHECKOUT", CheckInTime = now.ToString("yyyy-MM-dd HH:mm:ss"), CreatedAt = now, UpdatedAt = now }; checkInRepo.Insert(checkInItem); } /// /// 顯示簽到簽退時間 /// //private void DisplayCheckInTimes(LocalSignUpItem signUpItem) //{ // lbMenualCheckInTime.Text = !string.IsNullOrWhiteSpace(signUpItem.CheckInTime) // ? ExtractTimeFromDateTime(signUpItem.CheckInTime) // : string.Empty; // lbMenualCheckOutTime.Text = !string.IsNullOrWhiteSpace(signUpItem.CheckOutTime) // ? ExtractTimeFromDateTime(signUpItem.CheckOutTime) // : string.Empty; //} /// /// 從日期時間字符串中提取時間部分(HH:mm:ss) /// private string ExtractTimeFromDateTime(string dateTimeStr) { if (string.IsNullOrWhiteSpace(dateTimeStr)) { return string.Empty; } // 日期時間格式為 "yyyy-MM-dd HH:mm:ss",時間部分從索引 11 開始 if (dateTimeStr.Length >= 19) { return dateTimeStr.Substring(11, 8); // 提取 "HH:mm:ss" } return dateTimeStr; } /// /// 清空輸入框並設定焦點 /// private void ClearInputsAndFocus() { txtQRCode.Clear(); txtName.Clear(); txtMobile.Clear(); txtIDNumber.Clear(); txtEMail.Clear(); txtLossJobTimes.Clear(); cbIDType.SelectedIndex = -1; cbMealType.SelectedIndex = -1; cbGender.SelectedIndex = -1; // 根據作業狀態設定焦點 if (checkinOperating) { txtQRCode.Focus(); } } /// /// 從後端服務下載 Classes 資料並儲存到本地 /// private void DownloadClassesData() { try { // 呼叫 API 取得所有課程資料(分頁取得) int downloadedCount = 0; ClassesRepository classesRepo = new ClassesRepository(); // 清除本地現有課程資料 classesRepo.DeleteAll(); BaseResponse response = ClassesService.SearchClasses(); if (!response.Success) { return; } // 轉換資料並保存到本地資料庫 ClassesList classesList = ApiService.DeserializeObject(response.Data.ToString()); foreach (var item in classesList) { // 轉換為本地模型 LocalClassesItem localItem = LocalClassesItem.ConvertFromApiModel(item); classesRepo.Insert(localItem); downloadedCount++; } } catch { } } private void cbCheckMode_SelectedIndexChanged(object sender, EventArgs e) { btnMenualCheckIn.Text = $"現場{cbCheckMode.Text}"; } /// /// 執行簽到紀錄上傳 /// /// 課程ID /// 是否要顯示訊息 private void ProcessUpload(int courseID, bool showMessage) { // 取得本地有簽到簽退記錄的資料 CheckInRepository checkinRepo = new CheckInRepository(); var localItems = checkinRepo.GetByCourseID(courseID).Where(p => !p.UploadTime.HasValue).OrderBy(p => p.ID); if (localItems.Count() == 0) { if (showMessage) { MessageBox.Show("沒有需要上傳的資料", "提示"); btnUpload.Enabled = true; btnUpload.Text = "上傳"; } return; } // 篩選出有簽到或簽退記錄的項目 var itemsToUpload = new List(); foreach (var item in localItems) { itemsToUpload.Add( new CheckInItem() { ID = item.ID, CourseID = item.CourseID, Name = item.Name, Mobile = item.Mobile, SignUpType = item.SignUpType, IDNumber = item.IDNumber, IDType = item.IDType, MealType = item.MealType, Email = item.Email, AgreementStatus = item.AgreementStatus, CheckInType = item.CheckInType, CheckInTime = item.CheckInTime, }); } // 呼叫 API 上傳資料 BaseResponse response = SignUpService.UploadCheckIns(courseID, itemsToUpload); if (response.Success) { var createdCount = response.Data.CreatedCount; var updatedCount = response.Data.UpdatedCount; var failedCount = response.Data.FailedCount; var SkippedCount = response.Data.SkippedCount; var successIDs = response.Data.SuccessIDs.ToString().Split(','); // 更新上傳時間 checkinRepo.UpdateUploadTime(successIDs); // 重新更新統計資訊 if (showMessage) { CbClasses_SelectedIndexChanged(null, null); MessageBox.Show($"上傳成功,共上傳 {itemsToUpload.Count} 筆資料", "成功"); } } else { if (showMessage) { MessageBox.Show("上傳失敗: " + response.Message, "錯誤"); } } } private void uploadWorker_DoWork(object sender, DoWorkEventArgs e) { var courseID = Convert.ToInt32(e.Argument); uploadWorker.ReportProgress(0, $"{courseID} 上傳報到資料..."); ProcessUpload(courseID, false); } private void uploadWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) { lbSyncJob.Text = e.UserState.ToString(); Application.DoEvents(); } private void uploadWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { lbSyncJob.Text = "(無)"; } private void uploadTimer_Tick(object sender, EventArgs e) { if (cbCurrentClass.SelectedValue == null) { return; } if (!uploadWorker.IsBusy) { int courseID = (int)cbCurrentClass.SelectedValue; uploadWorker.RunWorkerAsync(courseID); } } private void MainForm_Activated(object sender, EventArgs e) { WindowState = FormWindowState.Maximized; SetInputControlEnabled(); } private bool CheckLocalDB(bool showLog = true) { if (showLog) txtLocalDBStatus.AppendText("檢查資料庫...\r\n"); var healthCheck = DatabaseService.Instance.PerformHealthCheck(); if (healthCheck.IsHealthy) { if (showLog) txtLocalDBStatus.AppendText(" >> 資料庫健康狀態良好\r\n\r\n"); } else { if (showLog) txtLocalDBStatus.AppendText(" >> 資料庫需要修復\r\n\r\n"); } return healthCheck.IsHealthy; } private void btnCheckLocalDB_Click(object sender, EventArgs e) { CheckLocalDB(); } private void btnRepareLocalDB_Click(object sender, EventArgs e) { txtLocalDBStatus.AppendText("修復資料庫...\r\n"); var repairResult = DatabaseService.Instance.AutoRepairDatabase(); foreach (var tableRepair in repairResult.TableRepairResults) { txtLocalDBStatus.AppendText($" >> {tableRepair.TableName}: {tableRepair.Message}\r\n"); } if (CheckLocalDB() && ClientSession.OnlineMode) { txtLocalDBStatus.AppendText("下載員工資料...\r\n"); DownloadBPSNData(); txtLocalDBStatus.AppendText("下載課程資料...\r\n"); DownloadClassesData(); txtLocalDBStatus.AppendText(" >> 課程資料下載完畢,請依需要下載課程人員資料\r\n\r\n"); } } private void btnRebuildLocalDB_Click(object sender, EventArgs e) { txtLocalDBStatus.AppendText("重建資料庫...\r\n"); DatabaseService.Instance.DropAndCreateAllTables(); if (CheckLocalDB() && ClientSession.OnlineMode) { txtLocalDBStatus.AppendText("下載員工資料...\r\n"); DownloadBPSNData(); txtLocalDBStatus.AppendText("下載課程資料...\r\n"); DownloadClassesData(); txtLocalDBStatus.AppendText(" >> 課程資料下載完畢,請依需要下載課程人員資料\r\n\r\n"); } } private void btnFilter_Click(object sender, EventArgs e) { txtFilterName.Text = txtFilterName.Text.Trim(); txtFilterMobile.Text = txtFilterMobile.Text.Trim(); if (string.IsNullOrWhiteSpace(txtFilterName.Text) && string.IsNullOrWhiteSpace(txtFilterMobile.Text)) { MessageBox.Show("請輸入姓名或手機號來篩選"); return; } if (cbClasses.SelectedValue == null) { MessageBox.Show("請先選擇課程"); return; } int courseID = (int)cbClasses.SelectedValue; SignUpsRepository signUps = new SignUpsRepository(); var data = signUps.GetByCourseID(courseID); if (!string.IsNullOrWhiteSpace(txtFilterName.Text)) data = data.Where(x => x.Name.Contains(txtFilterName.Text)).ToList(); if (!string.IsNullOrWhiteSpace(txtFilterMobile.Text)) data = data.Where(x => x.Mobile.Contains(txtFilterMobile.Text)).ToList(); dgSignUp.DataSource = null; bsSignUp.DataSource = null; bsSignUp.DataSource = data; dgSignUp.DataSource = bsSignUp; } private void btnClearFilter_Click(object sender, EventArgs e) { txtFilterName.Text = string.Empty; txtFilterMobile.Text = string.Empty; int courseID = (int)cbClasses.SelectedValue; SignUpsRepository signUps = new SignUpsRepository(); dgSignUp.DataSource = null; bsSignUp.DataSource = null; bsSignUp.DataSource = signUps.GetByCourseID(courseID); dgSignUp.DataSource = bsSignUp; } private void btnRefreshClasses_Click(object sender, EventArgs e) { // 連線模式時,定時上傳數據 if (ClientSession.OnlineMode) { try { btnRefreshClasses.Text = "下載中"; btnRefreshClasses.Enabled = false; Processing = true; Application.DoEvents(); // 下載課程資料 DownloadClassesData(); } finally { btnRefreshClasses.Text = "重新下載課程資料"; btnRefreshClasses.Enabled = true; Processing = false; } } } private bool Processing { set { if (value) { processingForm.TopMost = true; processingForm.Show(); } else { processingForm.Hide(); } } } private string ShowAgreementConfirmDialog(string agreementText) { using (var confirmForm = new AgreementConfirmForm()) { confirmForm.同意事項 = agreementText; confirmForm.Top = 0; confirmForm.Left = (this.Width - confirmForm.Width) / 2; return confirmForm.ShowDialog() == DialogResult.Yes ? "同意" : "不同意"; } } } }