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;
}
}
}