1718 lines
62 KiB
C#
1718 lines
62 KiB
C#
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 = "新譽管理課程報到系統";
|
|
|
|
/// <summary>
|
|
/// 是否作業中
|
|
/// </summary>
|
|
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("連線測試失敗");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 載入活動清單資料
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 當選擇活動時,取得相應統計資料
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 取得後端報名人數
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 取得本地已下載人數
|
|
/// </summary>
|
|
private int GetLocalSignUpCount(int courseID)
|
|
{
|
|
try
|
|
{
|
|
// 從本地資料庫取得該活動的參加人員數
|
|
SignUpsRepository repo = new SignUpsRepository();
|
|
return repo.GetCount(courseID: courseID);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 取得資料未上傳筆數
|
|
/// </summary>
|
|
private int GetWaitingUploadCount(int courseID)
|
|
{
|
|
try
|
|
{
|
|
CheckInRepository repo = new CheckInRepository();
|
|
return repo.GetByCourseID(courseID).Where(p => !p.UploadTime.HasValue).Count();
|
|
}
|
|
catch (Exception)
|
|
{
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 下載活動參加人員資料
|
|
/// </summary>
|
|
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<SignUpList>(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 = "下載課程人員";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 上傳本地簽到簽退資料
|
|
/// </summary>
|
|
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 = "上傳報到資料";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 重新下載員工資料表
|
|
/// </summary>
|
|
private void DownloadBPSNData()
|
|
{
|
|
try
|
|
{
|
|
// 呼叫 BPSNService 下載員工資料
|
|
BaseResponse response = BPSNService.Download();
|
|
|
|
if (response.Success && response.Data != null)
|
|
{
|
|
// 轉換資料為 BPSNList
|
|
BPSNList bpsnList = ApiService.DeserializeObject<BPSNList>(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;
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// 預約報名 - QRCode 簽到簽退事件處理器
|
|
/// 當掃描器掃入 QRCode 時觸發
|
|
/// </summary>
|
|
private void TxtQRCode_KeyDown(object sender, KeyEventArgs e)
|
|
{
|
|
// Enter 鍵觸發簽到簽退
|
|
if (e.KeyCode == Keys.Return)
|
|
{
|
|
e.Handled = true;
|
|
ProcessScanCheckIn();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 處理 QRCode 簽到簽退邏輯
|
|
/// </summary>
|
|
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}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 顯示簽到簽退結果(成功/失敗)
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 顯示報名人員資訊
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 清空簽到簽退顯示資訊
|
|
/// 這裡不處理輸入控制項的清空
|
|
/// </summary>
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 顯示簽到簽退結果(成功/失敗)
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// 處理現場報到邏輯
|
|
/// 支持簽到和簽退
|
|
/// </summary>
|
|
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}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 驗證輸入資料
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 驗證中華民國身分證字號
|
|
/// 規則:第一碼為英文字母(A-Z),後接九位數字
|
|
/// 使用檢查碼算法驗證
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 驗證電子郵件格式
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 更新簽到簽退時間
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 檢查是否已經簽到或簽退
|
|
/// </summary>
|
|
private bool IsAlreadyChecked(LocalSignUpItem signUpItem, bool isCheckIn)
|
|
{
|
|
return isCheckIn ? !string.IsNullOrWhiteSpace(signUpItem.CheckInTime) : !string.IsNullOrWhiteSpace(signUpItem.CheckOutTime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 新增簽到檢查紀錄
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 顯示簽到簽退時間
|
|
/// </summary>
|
|
//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;
|
|
//}
|
|
|
|
/// <summary>
|
|
/// 從日期時間字符串中提取時間部分(HH:mm:ss)
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 清空輸入框並設定焦點
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 從後端服務下載 Classes 資料並儲存到本地
|
|
/// </summary>
|
|
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<ClassesList>(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}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// 執行簽到紀錄上傳
|
|
/// </summary>
|
|
/// <param name="courseID">課程ID</param>
|
|
/// <param name="showMessage">是否要顯示訊息</param>
|
|
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<CheckInItem>();
|
|
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 ? "同意" : "不同意";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|