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 DateTime CreatedDate { get; set; }
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
{
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,
int acNoInt,
DateTime checkTime,
int processed,
string machineId,
int inOutTypeId,
string machineIp,
DateTime dateOnly,
Action<string>? 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);
}

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 " +
"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<string>? 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 ? "<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();
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<byte>();
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<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;
}
catch (Exception ex) { error = ex.Message; return false; }