Fix attendance_log inserts and extend face-template/user deletion DB APIs

Replace REPLACE/empty ac_no inserts with guarded InsertAttendance (avoids generated ac_no_int cast failures); extend face-template upsert metadata and pending-deletion user queries for machine-scoped delete requests.
main
SYED MUSTUFA AHMED NAQVI 2026-07-30 13:19:28 +05:00
parent a136fe587c
commit 9ae838888a
3 changed files with 139 additions and 25 deletions

View File

@ -31,4 +31,6 @@ internal sealed class AttendanceMachineFaceTemplateRow
public byte[] Template { get; set; } = Array.Empty<byte>(); public byte[] Template { get; set; } = Array.Empty<byte>();
public DateTime CreatedDate { get; set; } public DateTime CreatedDate { get; set; }
public bool IsActive { get; set; } = true; public bool IsActive { get; set; } = true;
public string SourceMachineId { get; set; } = "";
public string SourceMachineIp { get; set; } = "";
} }

View File

@ -19,14 +19,20 @@ internal interface IAttendanceMachineRepository
internal interface IAttendanceLogRepository internal interface IAttendanceLogRepository
{ {
bool UpsertAttendance( /// <summary>
/// Inserts one attendance_log row. Does not use REPLACE until mapping is confirmed.
/// <paramref name="acNo"/> must be a non-empty numeric employee string (never "").
/// </summary>
bool InsertAttendance(
string acNo, string acNo,
int acNoInt,
DateTime checkTime, DateTime checkTime,
int processed, int processed,
string machineId, string machineId,
int inOutTypeId, int inOutTypeId,
string machineIp, string machineIp,
DateTime dateOnly, DateTime dateOnly,
Action<string>? log,
out string error); out string error);
} }
@ -40,6 +46,6 @@ internal interface IAttendanceMachineUserRepository
internal interface IAttendanceMachineFaceTemplateRepository 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); bool TryGetActiveFaceTemplateBySerial(string serialNo, out AttendanceMachineFaceTemplateRow? row, out string error);
} }

View File

@ -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 " + 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)"; "FROM attendance_machine WHERE (machine_status='active' OR machine_status='1' OR machine_status=1)";
if (!string.IsNullOrWhiteSpace(machineTypeFilter)) 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); using var cmd = new MySqlCommand(sql, conn);
if (!string.IsNullOrWhiteSpace(machineTypeFilter)) if (!string.IsNullOrWhiteSpace(machineTypeFilter))
cmd.Parameters.AddWithValue("@machineType", machineTypeFilter); cmd.Parameters.AddWithValue("@machineType", machineTypeFilter);
@ -126,30 +126,94 @@ internal sealed class MySqlAttendanceLogRepository : IAttendanceLogRepository
private readonly IHrmsDbConnectionFactory _factory; private readonly IHrmsDbConnectionFactory _factory;
public MySqlAttendanceLogRepository(IHrmsDbConnectionFactory factory) { _factory = 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<string>? log,
out string error)
{ {
error = ""; error = "";
if (!_factory.TryBuildConnectionString(out var cs, out error)) if (!_factory.TryBuildConnectionString(out var cs, out error))
return false; 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 try
{ {
using var conn = new MySqlConnection(cs); using var conn = new MySqlConnection(cs);
conn.Open(); conn.Open();
using var cmd = new MySqlCommand( // Plain INSERT (not REPLACE) until field mapping is confirmed.
"REPLACE INTO attendance_log (ac_no,checktime,processed,machine_id,in_out_type_id,machine_ip,date) " + const string sql =
"VALUES (@ac_no,@checktime,@processed,@machine_id,@in_out_type_id,@machine_ip,@date)", "INSERT INTO attendance_log (ac_no,checktime,processed,machine_id,in_out_type_id,machine_ip,date) " +
conn); "VALUES (@ac_no,@checktime,@processed,@machine_id,@in_out_type_id,@machine_ip,@date)";
cmd.Parameters.AddWithValue("@ac_no", acNo ?? "");
cmd.Parameters.AddWithValue("@checktime", checkTime); using var cmd = new MySqlCommand(sql, conn);
cmd.Parameters.AddWithValue("@processed", processed); cmd.Parameters.Add("@ac_no", MySqlDbType.VarChar, 24).Value = acNo;
cmd.Parameters.AddWithValue("@machine_id", machineId ?? ""); cmd.Parameters.Add("@checktime", MySqlDbType.DateTime).Value = checkTime;
cmd.Parameters.AddWithValue("@in_out_type_id", inOutTypeId); cmd.Parameters.Add("@processed", MySqlDbType.Int32).Value = processed;
cmd.Parameters.AddWithValue("@machine_ip", machineIp ?? ""); cmd.Parameters.Add("@machine_id", MySqlDbType.VarChar, 45).Value =
cmd.Parameters.AddWithValue("@date", dateOnly.Date); 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 ? "<DBNull>" : 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(); cmd.ExecuteNonQuery();
return true; 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; private readonly IHrmsDbConnectionFactory _factory;
public MySqlAttendanceMachineFaceTemplateRepository(IHrmsDbConnectionFactory factory) { _factory = 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 = ""; error = "";
createdNew = true;
if (!_factory.TryBuildConnectionString(out var cs, out error)) if (!_factory.TryBuildConnectionString(out var cs, out error))
return false; return false;
try try
{ {
using var conn = new MySqlConnection(cs); using var conn = new MySqlConnection(cs);
conn.Open(); conn.Open();
using var cmd = new MySqlCommand(
"REPLACE INTO attendance_machine_face_templates (serial_no,template,created_date,is_active) " + using (var existsCmd = new MySqlCommand(
"VALUES (@serial_no,@template,@created_date,@is_active)", conn); "SELECT 1 FROM attendance_machine_face_templates WHERE serial_no=@serial_no LIMIT 1", conn))
cmd.Parameters.AddWithValue("@serial_no", serialNo ?? ""); {
cmd.Parameters.Add("@template", MySqlDbType.Blob).Value = (object?)template ?? Array.Empty<byte>(); existsCmd.Parameters.AddWithValue("@serial_no", serialNo ?? "");
cmd.Parameters.AddWithValue("@created_date", createdDate); createdNew = existsCmd.ExecuteScalar() == null;
cmd.Parameters.AddWithValue("@is_active", isActive ? 1 : 0); }
cmd.ExecuteNonQuery();
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<byte>();
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<byte>();
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; return true;
} }
catch (Exception ex) { error = ex.Message; return false; } catch (Exception ex) { error = ex.Message; return false; }