using System; using System.Collections.Generic; using System.Linq; using NCC.Extend.Entitys.Dto.LqAttendanceRecord; using NCC.Extend.Entitys.Enum; using NCC.Extend.Entitys.lq_attendance_record; using Newtonsoft.Json.Linq; namespace NCC.Extend { /// /// 考勤汇总列表用:按自然月、每日一条有效记录口径统计应休/到岗/请假/迟到早退/旷工及可自快照计算的固定扣款。 /// public static class AttendanceSummaryMonthMetricsHelper { private const string PersonalLeaveType = "事假"; private const string SickLeaveType = "病假"; /// /// 单月扩展指标 /// public sealed class MonthMetrics { /// 应休天数(考勤组月应休额度,优先取当月打卡规则快照中的分组配置) public decimal EntitledRestDays { get; set; } /// 到岗天数(正常+迟到) public decimal OnDutyDays { get; set; } /// 工作天数(应休+到岗) public decimal CompositeWorkDays { get; set; } /// 带薪请假天数(请假状态中假种非事假、非病假;不含病假状态) public decimal PaidLeaveDays { get; set; } /// 请假天数(请假+病假状态合计) public decimal LeaveDaysTotal { get; set; } /// 迟到或早退分钟大于 0 的天数 public decimal LateEarlyDays { get; set; } /// 旷工天数 public decimal AbsenteeismDays { get; set; } /// 迟到/早退规则为「固定金额」时,自 F_RuleSnapshotJson 汇总 public decimal LateDeductionAmount { get; set; } /// 事假/病假扣款依赖日薪×倍率;未传日薪时为 0 public decimal LeaveDeductionAmount { get; set; } /// 旷工规则为「固定金额」时,自 F_AllRuleSnapshotJson 的旷工规则表取首条匹配单日区间 public decimal AbsenteeismDeductionAmount { get; set; } } /// /// 按用户当月有效打卡记录计算扩展指标(须已按日折叠为每日一条,与汇总同步口径一致)。 /// /// 该用户该月「每日一条」记录 /// 无快照时的月应休兜底(主档分组) /// 日薪;为空则请假扣款金额为 0 public static MonthMetrics Compute( IReadOnlyList perDayRecords, int fallbackMonthlyRestDays, decimal? dailySalaryForLeaveDeduct = null) { var metrics = new MonthMetrics(); if (perDayRecords == null || perDayRecords.Count == 0) { metrics.EntitledRestDays = fallbackMonthlyRestDays; metrics.CompositeWorkDays = metrics.EntitledRestDays + metrics.OnDutyDays; return metrics; } var entitled = TryResolveMonthlyRestDays(perDayRecords, fallbackMonthlyRestDays); metrics.EntitledRestDays = entitled; decimal? leaveRate = null; decimal? sickRate = null; foreach (var r in perDayRecords) { TryReadBaseLeaveRates(r.AllRuleSnapshotJson, ref leaveRate, ref sickRate); } foreach (var r in perDayRecords) { var st = r.Status; if (st == (int)AttendanceRecordStatusEnum.正常 || st == (int)AttendanceRecordStatusEnum.迟到) { metrics.OnDutyDays += 1; } if (st == (int)AttendanceRecordStatusEnum.请假) { metrics.LeaveDaysTotal += 1; var lt = ResolveLeaveType(r); if (IsPaidLeaveCategory(lt)) { metrics.PaidLeaveDays += 1; } if (dailySalaryForLeaveDeduct.HasValue && leaveRate.HasValue && string.Equals(lt, PersonalLeaveType, StringComparison.Ordinal)) { metrics.LeaveDeductionAmount += dailySalaryForLeaveDeduct.Value * leaveRate.Value; } } else if (st == (int)AttendanceRecordStatusEnum.病假) { metrics.LeaveDaysTotal += 1; if (dailySalaryForLeaveDeduct.HasValue && sickRate.HasValue) { metrics.LeaveDeductionAmount += dailySalaryForLeaveDeduct.Value * sickRate.Value; } } if (r.LateMinutes > 0 || r.EarlyLeaveMinutes > 0) { metrics.LateEarlyDays += 1; } if (st == (int)AttendanceRecordStatusEnum.旷工) { metrics.AbsenteeismDays += 1; } metrics.LateDeductionAmount += TrySumFixedLateEarlyDeduction(r); if (st == (int)AttendanceRecordStatusEnum.旷工) { metrics.AbsenteeismDeductionAmount += TryPickFixedAbsenteeismDeduction(r.AllRuleSnapshotJson); } } metrics.CompositeWorkDays = metrics.EntitledRestDays + metrics.OnDutyDays; return metrics; } /// /// 将原始当月记录按日取最新一条(UpdateTime 优先)。 /// public static List CollapseToLatestPerDay( IEnumerable monthRecords) { var list = monthRecords?.Where(x => x != null).ToList() ?? new List(); return list .GroupBy(x => x.AttendanceDate.Date) .Select(g => g.OrderByDescending(x => x.UpdateTime ?? x.CreateTime ?? DateTime.MinValue).First()) .OrderBy(x => x.AttendanceDate) .ToList(); } private static int TryResolveMonthlyRestDays( IReadOnlyList perDayRecords, int fallback) { foreach (var r in perDayRecords.OrderByDescending(x => x.AttendanceDate)) { var v = TryGetMonthlyRestFromRuleSnapshot(r.RuleSnapshotJson); if (v.HasValue) { return Math.Max(0, v.Value); } } return Math.Max(0, fallback); } private static int? TryGetMonthlyRestFromRuleSnapshot(string json) { if (string.IsNullOrWhiteSpace(json)) { return null; } try { var o = JObject.Parse(json); var rg = o["restGroup"]; var fromRest = rg?["monthlyRestDays"]?.Value(); if (fromRest.HasValue) { return fromRest; } var ag = o["attendanceGroup"]; return ag?["monthlyRestDays"]?.Value(); } catch { return null; } } private static void TryReadBaseLeaveRates( string allJson, ref decimal? leaveRate, ref decimal? sickRate) { if (string.IsNullOrWhiteSpace(allJson) || (leaveRate.HasValue && sickRate.HasValue)) { return; } try { var o = JObject.Parse(allJson); var bs = o["baseSetting"]; if (bs == null) { return; } if (!leaveRate.HasValue) { leaveRate = bs["leaveDeductDailySalaryRate"]?.Value(); } if (!sickRate.HasValue) { sickRate = bs["sickLeaveDeductDailySalaryRate"]?.Value(); } } catch { // ignore } } private static decimal TrySumFixedLateEarlyDeduction(LqAttendanceRecordEntity r) { if (string.IsNullOrWhiteSpace(r?.RuleSnapshotJson)) { return 0m; } try { var o = JObject.Parse(r.RuleSnapshotJson); decimal sum = 0; if (r.LateMinutes > 0) { sum += ReadFixedDeductAmount(o["lateRule"]); } if (r.EarlyLeaveMinutes > 0) { sum += ReadFixedDeductAmount(o["earlyLeaveRule"]); } return sum; } catch { return 0m; } } private static decimal ReadFixedDeductAmount(JToken rule) { if (rule == null || rule.Type == JTokenType.Null) { return 0m; } var mode = rule["deductMode"]?.Value(); if (mode != (int)AttendanceDeductModeEnum.FixedAmount) { return 0m; } return rule["deductValue"]?.Value() ?? 0m; } private static decimal TryPickFixedAbsenteeismDeduction(string allJson) { if (string.IsNullOrWhiteSpace(allJson)) { return 0m; } try { var o = JObject.Parse(allJson); var arr = o["allRules"]?["absenteeismRules"] as JArray; if (arr == null) { return 0m; } foreach (var token in arr) { var minDays = token["minDays"]?.Value() ?? 1m; var maxDays = token["maxDays"]?.Value(); if (minDays > 1m) { continue; } if (maxDays.HasValue && maxDays.Value <= 1m) { continue; } var mode = token["deductMode"]?.Value(); if (mode != (int)AttendanceDeductModeEnum.FixedAmount) { continue; } return token["deductValue"]?.Value() ?? 0m; } } catch { // ignore } return 0m; } private static string ResolveLeaveType(LqAttendanceRecordEntity r) { var workflows = ParseRelatedWorkflows(r.RelatedWorkflowsJson); var fromWf = workflows .FirstOrDefault(x => x != null && x.type == "请假" && !string.IsNullOrWhiteSpace(x.leaveType)); if (fromWf != null) { return fromWf.leaveType.Trim(); } return TryGetLeaveTypeLabelFromSyncRemark(r.Remark); } private static List ParseRelatedWorkflows(string json) { if (string.IsNullOrWhiteSpace(json)) { return new List(); } try { return Newtonsoft.Json.JsonConvert.DeserializeObject>(json) ?? new List(); } catch { return new List(); } } private static string TryGetLeaveTypeLabelFromSyncRemark(string remark) { if (string.IsNullOrWhiteSpace(remark)) { return null; } const string prefix = "请假审批通过:"; var idx = remark.IndexOf(prefix, StringComparison.Ordinal); if (idx < 0) { return null; } var tail = remark.Substring(idx + prefix.Length); var paren = tail.IndexOf('('); if (paren <= 0) { return null; } var label = tail.Substring(0, paren).Trim(); return string.IsNullOrEmpty(label) ? null : label; } private static bool IsPaidLeaveCategory(string leaveType) { if (string.IsNullOrWhiteSpace(leaveType)) { return false; } if (string.Equals(leaveType, PersonalLeaveType, StringComparison.Ordinal) || string.Equals(leaveType, SickLeaveType, StringComparison.Ordinal)) { return false; } return true; } } }