diff --git a/Data/DbModels.cs b/Data/DbModels.cs index 3ab38a3..3befcaf 100644 --- a/Data/DbModels.cs +++ b/Data/DbModels.cs @@ -31,4 +31,6 @@ internal sealed class AttendanceMachineFaceTemplateRow public byte[] Template { get; set; } = Array.Empty(); public DateTime CreatedDate { get; set; } public bool IsActive { get; set; } = true; + public string SourceMachineId { get; set; } = ""; + public string SourceMachineIp { get; set; } = ""; } diff --git a/Data/Interfaces.cs b/Data/Interfaces.cs index e4f0380..97ea465 100644 --- a/Data/Interfaces.cs +++ b/Data/Interfaces.cs @@ -19,14 +19,20 @@ internal interface IAttendanceMachineRepository internal interface IAttendanceLogRepository { - bool UpsertAttendance( + /// + /// Inserts one attendance_log row. Does not use REPLACE until mapping is confirmed. + /// must be a non-empty numeric employee string (never ""). + /// + bool InsertAttendance( string acNo, + int acNoInt, DateTime checkTime, int processed, string machineId, int inOutTypeId, string machineIp, DateTime dateOnly, + Action? log, out string error); } @@ -40,6 +46,6 @@ internal interface IAttendanceMachineUserRepository internal interface IAttendanceMachineFaceTemplateRepository { - bool UpsertFaceTemplate(string serialNo, byte[] template, DateTime createdDate, bool isActive, out string error); + bool UpsertFaceTemplate(string serialNo, byte[] template, DateTime createdDate, bool isActive, string? sourceMachineId, string? sourceMachineIp, out bool createdNew, out string error); bool TryGetActiveFaceTemplateBySerial(string serialNo, out AttendanceMachineFaceTemplateRow? row, out string error); } diff --git a/Data/MySqlRepositories.cs b/Data/MySqlRepositories.cs index 53414a4..1bebc4e 100644 --- a/Data/MySqlRepositories.cs +++ b/Data/MySqlRepositories.cs @@ -22,7 +22,7 @@ internal sealed class MySqlAttendanceMachineRepository : IAttendanceMachineRepos string sql = "SELECT machine_id,machine_ip,port_number,machine_name,site_id,machine_status,machine_type,status,last_sync_date,total_users " + "FROM attendance_machine WHERE (machine_status='active' OR machine_status='1' OR machine_status=1)"; if (!string.IsNullOrWhiteSpace(machineTypeFilter)) - sql += " AND machine_type=@machineType"; + sql += " AND UPPER(TRIM(machine_type))=UPPER(TRIM(@machineType))"; using var cmd = new MySqlCommand(sql, conn); if (!string.IsNullOrWhiteSpace(machineTypeFilter)) cmd.Parameters.AddWithValue("@machineType", machineTypeFilter); @@ -126,30 +126,94 @@ internal sealed class MySqlAttendanceLogRepository : IAttendanceLogRepository private readonly IHrmsDbConnectionFactory _factory; public MySqlAttendanceLogRepository(IHrmsDbConnectionFactory factory) { _factory = factory; } - public bool UpsertAttendance(string acNo, DateTime checkTime, int processed, string machineId, int inOutTypeId, string machineIp, DateTime dateOnly, out string error) + // Confirmed via SHOW CREATE TABLE / DESCRIBE hrms.attendance_log + private static readonly (string Param, string Column, string MySqlType)[] AttendanceLogParamMeta = + { + ("@ac_no", "ac_no", "varchar(24) NOT NULL"), + ("@checktime", "checktime", "datetime NOT NULL"), + ("@processed", "processed", "int NULL"), + ("@machine_id", "machine_id", "varchar(45) NULL"), + ("@in_out_type_id", "in_out_type_id", "int NULL"), + ("@machine_ip", "machine_ip", "varchar(50) NULL"), + ("@date", "date", "datetime NULL"), + }; + + public bool InsertAttendance( + string acNo, + int acNoInt, + DateTime checkTime, + int processed, + string machineId, + int inOutTypeId, + string machineIp, + DateTime dateOnly, + Action? log, + out string error) { error = ""; if (!_factory.TryBuildConnectionString(out var cs, out error)) return false; + + // Guard: empty ac_no breaks generated column ac_no_int = CAST(ac_no AS UNSIGNED) + // ("Truncated incorrect INTEGER value: ''"). + if (string.IsNullOrWhiteSpace(acNo)) + { + error = "refused insert: ac_no is empty (would break generated ac_no_int CAST)"; + log?.Invoke("[AttendanceInsert] REFUSED: " + error); + return false; + } + try { using var conn = new MySqlConnection(cs); conn.Open(); - using var cmd = new MySqlCommand( - "REPLACE INTO attendance_log (ac_no,checktime,processed,machine_id,in_out_type_id,machine_ip,date) " + - "VALUES (@ac_no,@checktime,@processed,@machine_id,@in_out_type_id,@machine_ip,@date)", - conn); - cmd.Parameters.AddWithValue("@ac_no", acNo ?? ""); - cmd.Parameters.AddWithValue("@checktime", checkTime); - cmd.Parameters.AddWithValue("@processed", processed); - cmd.Parameters.AddWithValue("@machine_id", machineId ?? ""); - cmd.Parameters.AddWithValue("@in_out_type_id", inOutTypeId); - cmd.Parameters.AddWithValue("@machine_ip", machineIp ?? ""); - cmd.Parameters.AddWithValue("@date", dateOnly.Date); + // Plain INSERT (not REPLACE) until field mapping is confirmed. + const string sql = + "INSERT INTO attendance_log (ac_no,checktime,processed,machine_id,in_out_type_id,machine_ip,date) " + + "VALUES (@ac_no,@checktime,@processed,@machine_id,@in_out_type_id,@machine_ip,@date)"; + + using var cmd = new MySqlCommand(sql, conn); + cmd.Parameters.Add("@ac_no", MySqlDbType.VarChar, 24).Value = acNo; + cmd.Parameters.Add("@checktime", MySqlDbType.DateTime).Value = checkTime; + cmd.Parameters.Add("@processed", MySqlDbType.Int32).Value = processed; + cmd.Parameters.Add("@machine_id", MySqlDbType.VarChar, 45).Value = + string.IsNullOrWhiteSpace(machineId) ? (object)DBNull.Value : machineId; + cmd.Parameters.Add("@in_out_type_id", MySqlDbType.Int32).Value = inOutTypeId; + cmd.Parameters.Add("@machine_ip", MySqlDbType.VarChar, 50).Value = + string.IsNullOrWhiteSpace(machineIp) ? (object)DBNull.Value : machineIp; + cmd.Parameters.Add("@date", MySqlDbType.DateTime).Value = dateOnly.Date; + + log?.Invoke("[AttendanceInsert] SQL=" + sql); + log?.Invoke("[AttendanceInsert] NOTE: for_date is GENERATED STORED from checktime; ac_no_int is GENERATED VIRTUAL CAST(ac_no AS UNSIGNED). Neither is bound. cardNo is NOT inserted."); + log?.Invoke("[AttendanceInsert] Parsed employee int (source employeeNoString) acNoInt=" + acNoInt + + " (not a bound param; drives @ac_no string value)"); + foreach (var meta in AttendanceLogParamMeta) + { + var p = cmd.Parameters[meta.Param]; + var val = p.Value; + var runtimeType = val == null || val == DBNull.Value ? "DBNull" : val.GetType().FullName; + var display = val == null || val == DBNull.Value ? "" : Convert.ToString(val, System.Globalization.CultureInfo.InvariantCulture); + log?.Invoke( + "[AttendanceInsert] param=" + meta.Param + + " value=\"" + display + "\"" + + " csharpType=" + runtimeType + + " column=" + meta.Column + + " mysqlType=" + meta.MySqlType); + } + cmd.ExecuteNonQuery(); return true; } - catch (Exception ex) { error = ex.Message; return false; } + catch (Exception ex) + { + error = ex.Message; + log?.Invoke("[AttendanceInsert] FAILED err=" + ex.Message + + " ac_no=\"" + (acNo ?? "") + "\" acNoInt=" + acNoInt + + " machine_id=\"" + (machineId ?? "") + "\" machine_ip=\"" + (machineIp ?? "") + "\"" + + " checktime=" + checkTime.ToString("yyyy-MM-dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture) + + " in_out_type_id=" + inOutTypeId); + return false; + } } } @@ -245,23 +309,65 @@ internal sealed class MySqlAttendanceMachineFaceTemplateRepository : IAttendance private readonly IHrmsDbConnectionFactory _factory; public MySqlAttendanceMachineFaceTemplateRepository(IHrmsDbConnectionFactory factory) { _factory = factory; } - public bool UpsertFaceTemplate(string serialNo, byte[] template, DateTime createdDate, bool isActive, out string error) + public bool UpsertFaceTemplate(string serialNo, byte[] template, DateTime createdDate, bool isActive, string? sourceMachineId, string? sourceMachineIp, out bool createdNew, out string error) { error = ""; + createdNew = true; if (!_factory.TryBuildConnectionString(out var cs, out error)) return false; try { using var conn = new MySqlConnection(cs); conn.Open(); - using var cmd = new MySqlCommand( - "REPLACE INTO attendance_machine_face_templates (serial_no,template,created_date,is_active) " + - "VALUES (@serial_no,@template,@created_date,@is_active)", conn); - cmd.Parameters.AddWithValue("@serial_no", serialNo ?? ""); - cmd.Parameters.Add("@template", MySqlDbType.Blob).Value = (object?)template ?? Array.Empty(); - cmd.Parameters.AddWithValue("@created_date", createdDate); - cmd.Parameters.AddWithValue("@is_active", isActive ? 1 : 0); - cmd.ExecuteNonQuery(); + + using (var existsCmd = new MySqlCommand( + "SELECT 1 FROM attendance_machine_face_templates WHERE serial_no=@serial_no LIMIT 1", conn)) + { + existsCmd.Parameters.AddWithValue("@serial_no", serialNo ?? ""); + createdNew = existsCmd.ExecuteScalar() == null; + } + + if (createdNew) + { + using var insert = new MySqlCommand( + "INSERT INTO attendance_machine_face_templates (serial_no,template,created_date,is_active) " + + "VALUES (@serial_no,@template,@created_date,@is_active)", conn); + insert.Parameters.AddWithValue("@serial_no", serialNo ?? ""); + insert.Parameters.Add("@template", MySqlDbType.Blob).Value = (object?)template ?? Array.Empty(); + insert.Parameters.AddWithValue("@created_date", createdDate); + insert.Parameters.AddWithValue("@is_active", isActive ? 1 : 0); + insert.ExecuteNonQuery(); + } + else + { + using var update = new MySqlCommand( + "UPDATE attendance_machine_face_templates SET template=@template, created_date=@created_date, is_active=@is_active " + + "WHERE serial_no=@serial_no", conn); + update.Parameters.AddWithValue("@serial_no", serialNo ?? ""); + update.Parameters.Add("@template", MySqlDbType.Blob).Value = (object?)template ?? Array.Empty(); + update.Parameters.AddWithValue("@created_date", createdDate); + update.Parameters.AddWithValue("@is_active", isActive ? 1 : 0); + update.ExecuteNonQuery(); + } + + // Best-effort source metadata update for schemas that include these columns. + if (!string.IsNullOrWhiteSpace(sourceMachineId) || !string.IsNullOrWhiteSpace(sourceMachineIp)) + { + try + { + using var metaCmd = new MySqlCommand( + "UPDATE attendance_machine_face_templates SET source_machine_id=@source_machine_id, source_machine_ip=@source_machine_ip WHERE serial_no=@serial_no", + conn); + metaCmd.Parameters.AddWithValue("@source_machine_id", (object?)sourceMachineId ?? DBNull.Value); + metaCmd.Parameters.AddWithValue("@source_machine_ip", (object?)sourceMachineIp ?? DBNull.Value); + metaCmd.Parameters.AddWithValue("@serial_no", serialNo ?? ""); + metaCmd.ExecuteNonQuery(); + } + catch + { + // Older schemas may not have source metadata columns; keep core template write successful. + } + } return true; } catch (Exception ex) { error = ex.Message; return false; }