1005 lines
38 KiB
C#
1005 lines
38 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Threading.Tasks;
|
||
using Mapster;
|
||
using NPP.SmartSchedue.Api.Contracts.Domain.Time;
|
||
using NPP.SmartSchedue.Api.Contracts.Services.Time;
|
||
using NPP.SmartSchedue.Api.Contracts.Services.Time.Input;
|
||
using NPP.SmartSchedue.Api.Contracts.Services.Time.Output;
|
||
using NPP.SmartSchedue.Api.Contracts.Core.Enums;
|
||
using NPP.SmartSchedue.Api.Contracts.Core.Extensions;
|
||
using ZhonTai.Admin.Core.Dto;
|
||
using ZhonTai.Admin.Services;
|
||
using ZhonTai.DynamicApi;
|
||
using ZhonTai.DynamicApi.Attributes;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using NPP.SmartSchedue.Api.Contracts;
|
||
|
||
namespace NPP.SmartSchedue.Api.Services.Time;
|
||
|
||
/// <summary>
|
||
/// 班次不可用标记服务实现
|
||
/// 提供班次不可用数据的完整业务逻辑处理
|
||
/// </summary>
|
||
[DynamicApi(Area = "app")]
|
||
public class ShiftUnavailabilityService : BaseService, IShiftUnavailabilityService, IDynamicApi
|
||
{
|
||
private readonly IShiftUnavailabilityRepository _repository;
|
||
private readonly IShiftRepository _shiftRepository;
|
||
|
||
public ShiftUnavailabilityService(
|
||
IShiftUnavailabilityRepository repository,
|
||
IShiftRepository shiftRepository)
|
||
{
|
||
_repository = repository;
|
||
_shiftRepository = shiftRepository;
|
||
}
|
||
|
||
#region 基础CRUD操作
|
||
|
||
/// <summary>
|
||
/// 添加班次不可用标记
|
||
/// </summary>
|
||
public async Task<long> AddAsync(ShiftUnavailabilityAddInput input)
|
||
{
|
||
// 检查是否已存在相同的记录
|
||
var exists = await _repository.ExistsAsync(input.PersonnelId, input.Date, input.ShiftId);
|
||
if (exists)
|
||
{
|
||
throw new Exception($"员工在 {input.Date:yyyy-MM-dd} 的该班次已有不可用标记");
|
||
}
|
||
|
||
var entity = input.Adapt<ShiftUnavailabilityEntity>();
|
||
entity.ReasonType = (int)input.ReasonType;
|
||
entity.CreatedTime = DateTime.Now;
|
||
|
||
await _repository.InsertAsync(entity);
|
||
return entity.Id;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新班次不可用标记
|
||
/// </summary>
|
||
public async Task<bool> UpdateAsync(ShiftUnavailabilityUpdateInput input)
|
||
{
|
||
var entity = await _repository.GetAsync(input.Id);
|
||
if (entity == null)
|
||
{
|
||
throw new Exception("记录不存在");
|
||
}
|
||
|
||
// 更新可修改的字段
|
||
entity.ReasonType = (int)input.ReasonType;
|
||
entity.Remark = input.Remark;
|
||
entity.Priority = input.Priority;
|
||
entity.EffectiveStartTime = input.EffectiveStartTime;
|
||
entity.EffectiveEndTime = input.EffectiveEndTime;
|
||
entity.ModifiedTime = DateTime.Now;
|
||
|
||
return await _repository.UpdateAsync(entity) > 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除班次不可用标记
|
||
/// </summary>
|
||
public async Task<bool> DeleteAsync(long id)
|
||
{
|
||
return await _repository.DeleteAsync(id) > 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取班次不可用标记详情
|
||
/// </summary>
|
||
public async Task<ShiftUnavailabilityGetOutput> GetAsync(long id)
|
||
{
|
||
var entity = await _repository.GetAsync(id);
|
||
if (entity == null) return null;
|
||
|
||
var output = entity.Adapt<ShiftUnavailabilityGetOutput>();
|
||
|
||
// 设置枚举和扩展属性
|
||
output.ReasonType = ((UnavailabilityReasonType)entity.ReasonType);
|
||
output.ReasonTypeName = entity.ReasonType.GetDisplayName();
|
||
output.Category = entity.ReasonType.GetCategory();
|
||
output.CategoryName = output.Category.ToString();
|
||
output.GridSymbol = entity.ReasonType.GetGridSymbol();
|
||
output.ColorClass = entity.ReasonType.GetColorClass();
|
||
|
||
return output;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分页查询班次不可用标记
|
||
/// </summary>
|
||
public async Task<PageOutput<ShiftUnavailabilityGetPageOutput>> GetPageAsync(PageInput<ShiftUnavailabilityGetPageInput> input)
|
||
{
|
||
var query = _repository.Select;
|
||
|
||
// 条件过滤
|
||
if (input.Filter.PersonnelId.HasValue)
|
||
query = query.Where(x => x.PersonnelId == input.Filter.PersonnelId.Value);
|
||
|
||
if (input.Filter.ShiftId.HasValue)
|
||
query = query.Where(x => x.ShiftId == input.Filter.ShiftId.Value);
|
||
|
||
if (input.Filter.StartDate.HasValue)
|
||
query = query.Where(x => x.Date >= input.Filter.StartDate.Value.Date);
|
||
|
||
if (input.Filter.EndDate.HasValue)
|
||
query = query.Where(x => x.Date <= input.Filter.EndDate.Value.Date);
|
||
|
||
if (input.Filter.ReasonType.HasValue)
|
||
query = query.Where(x => x.ReasonType == (int)input.Filter.ReasonType.Value);
|
||
|
||
if (input.Filter.Category.HasValue)
|
||
{
|
||
// 根据分组筛选对应的原因类型
|
||
var reasonTypes = GetReasonTypesByCategory(input.Filter.Category.Value);
|
||
query = query.Where(x => reasonTypes.Contains(x.ReasonType));
|
||
}
|
||
|
||
if (input.Filter.IsFromTemplate.HasValue)
|
||
query = query.Where(x => x.IsFromTemplate == input.Filter.IsFromTemplate.Value);
|
||
|
||
if (!string.IsNullOrWhiteSpace(input.Filter.Keyword))
|
||
query = query.Where(x => x.Remark.Contains(input.Filter.Keyword));
|
||
|
||
// 使用标准的FreeSql分页方式
|
||
var entities = await query
|
||
.Count(out var total)
|
||
.OrderByDescending(x => x.CreatedTime)
|
||
.Page(input.CurrentPage, input.PageSize)
|
||
.ToListAsync();
|
||
|
||
// 手动进行数据转换
|
||
var list = entities.Select(entity =>
|
||
{
|
||
var output = entity.Adapt<ShiftUnavailabilityGetPageOutput>();
|
||
|
||
// 设置扩展属性
|
||
output.ReasonType = (UnavailabilityReasonType)entity.ReasonType;
|
||
output.ReasonTypeName = entity.ReasonType.GetDisplayName();
|
||
output.Category = entity.ReasonType.GetCategory();
|
||
output.GridSymbol = entity.ReasonType.GetGridSymbol();
|
||
output.ColorClass = entity.ReasonType.GetColorClass();
|
||
output.DateDisplay = entity.Date.ToString("yyyy-MM-dd");
|
||
output.Weekday = GetWeekdayName(entity.Date);
|
||
|
||
if (entity.SourceTemplateDate.HasValue)
|
||
output.SourceTemplateDateDisplay = entity.SourceTemplateDate.Value.ToString("yyyy-MM-dd");
|
||
|
||
return output;
|
||
}).ToList();
|
||
|
||
var data = new PageOutput<ShiftUnavailabilityGetPageOutput>()
|
||
{
|
||
List = list,
|
||
Total = total
|
||
};
|
||
|
||
return data;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 网格视图专用方法
|
||
|
||
/// <summary>
|
||
/// 获取指定员工指定月份的网格数据
|
||
/// </summary>
|
||
public async Task<GridDataOutput> GetMonthlyGridDataAsync(long personnelId, DateTime month)
|
||
{
|
||
var monthStart = new DateTime(month.Year, month.Month, 1);
|
||
var monthEnd = monthStart.AddMonths(1).AddDays(-1);
|
||
|
||
// 一次性查询整月数据
|
||
var entities = await _repository.GetByPersonnelAndDateRangeAsync(personnelId, monthStart, monthEnd);
|
||
|
||
// 获取所有启用的班次
|
||
var shifts = await _shiftRepository.Select
|
||
.Where(x => x.IsEnabled)
|
||
.ToListAsync();
|
||
|
||
// 构建网格数据结构
|
||
var grid = new Dictionary<DateTime, Dictionary<long, CellData>>();
|
||
var dates = new List<DateTime>();
|
||
|
||
// 初始化所有日期和班次的空单元格
|
||
for (var date = monthStart; date <= monthEnd; date = date.AddDays(1))
|
||
{
|
||
dates.Add(date);
|
||
grid[date] = new Dictionary<long, CellData>();
|
||
|
||
foreach (var shift in shifts)
|
||
{
|
||
grid[date][shift.Id] = new CellData
|
||
{
|
||
IsUnavailable = false,
|
||
DisplaySymbol = "",
|
||
CssClass = "",
|
||
TooltipText = $"{shift.Name} - 可用"
|
||
};
|
||
}
|
||
}
|
||
|
||
// 填充不可用数据
|
||
foreach (var entity in entities)
|
||
{
|
||
if (grid.ContainsKey(entity.Date.Date) && grid[entity.Date.Date].ContainsKey(entity.ShiftId))
|
||
{
|
||
var reasonType = (UnavailabilityReasonType)entity.ReasonType;
|
||
grid[entity.Date.Date][entity.ShiftId] = new CellData
|
||
{
|
||
IsUnavailable = true,
|
||
ReasonType = reasonType,
|
||
DisplaySymbol = entity.ReasonType.GetGridSymbol(),
|
||
CssClass = entity.ReasonType.GetColorClass(),
|
||
Remark = entity.Remark,
|
||
Priority = entity.Priority,
|
||
IsFromTemplate = entity.IsFromTemplate,
|
||
RecordId = entity.Id,
|
||
TooltipText = $"{entity.ReasonType.GetDisplayName()}: {entity.Remark}"
|
||
};
|
||
}
|
||
}
|
||
|
||
// 构建统计信息
|
||
var statistics = BuildGridStatistics(entities, shifts);
|
||
|
||
return new GridDataOutput
|
||
{
|
||
Grid = grid,
|
||
Dates = dates,
|
||
Shifts = shifts.Select(s => new ShiftInfo
|
||
{
|
||
Id = s.Id,
|
||
Name = s.Name,
|
||
ShiftNumber = s.ShiftNumber,
|
||
StartTime = s.StartTime,
|
||
EndTime = s.EndTime,
|
||
TimeRange = $"{s.StartTime:hh\\:mm}-{s.EndTime:hh\\:mm}",
|
||
IsEnabled = s.IsEnabled
|
||
}).ToList(),
|
||
Statistics = statistics,
|
||
MonthDisplay = month.ToString("yyyy年MM月")
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 切换单元格状态
|
||
/// </summary>
|
||
public async Task<bool> ToggleCellAsync(CellToggleInput input)
|
||
{
|
||
var exists = await _repository.ExistsAsync(input.PersonnelId, input.Date, input.ShiftId);
|
||
|
||
if (exists)
|
||
{
|
||
// 删除现有记录
|
||
return await _repository.DeleteByConditionAsync(input.PersonnelId, input.Date, input.ShiftId) > 0;
|
||
}
|
||
else
|
||
{
|
||
// 创建新记录,使用默认的个人意愿原因
|
||
var entity = new ShiftUnavailabilityEntity
|
||
{
|
||
PersonnelId = input.PersonnelId,
|
||
Date = input.Date,
|
||
ShiftId = input.ShiftId,
|
||
ReasonType = (int)UnavailabilityReasonType.PersonalPreference,
|
||
Priority = 1,
|
||
CreatedTime = DateTime.Now
|
||
};
|
||
|
||
await _repository.InsertAsync(entity);
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置单元格状态
|
||
/// </summary>
|
||
public async Task<bool> SetCellAsync(CellSetInput input)
|
||
{
|
||
// 先删除现有记录
|
||
await _repository.DeleteByConditionAsync(input.PersonnelId, input.Date, input.ShiftId);
|
||
|
||
// 如果指定了原因类型,则创建新记录
|
||
if (input.ReasonType.HasValue)
|
||
{
|
||
var entity = new ShiftUnavailabilityEntity
|
||
{
|
||
PersonnelId = input.PersonnelId,
|
||
Date = input.Date,
|
||
ShiftId = input.ShiftId,
|
||
ReasonType = (int)input.ReasonType.Value,
|
||
Remark = input.Remark,
|
||
Priority = input.Priority,
|
||
CreatedTime = DateTime.Now
|
||
};
|
||
|
||
await _repository.InsertAsync(entity);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 智能排班集成方法
|
||
|
||
/// <summary>
|
||
/// 获取指定日期和班次的不可用员工ID列表
|
||
/// </summary>
|
||
public async Task<List<long>> GetUnavailablePersonnelAsync(DateTime date, long shiftId)
|
||
{
|
||
return await _repository.GetUnavailablePersonnelIdsAsync(date, shiftId);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查人员班次的意愿
|
||
/// </summary>
|
||
/// <param name="date"></param>
|
||
/// <param name="shiftId"></param>
|
||
/// <param name="personnelId"></param>
|
||
/// <returns></returns>
|
||
public async Task<bool> CheckUnavailablePersonnelByShift(DateTime date, long shiftId, long personnelId)
|
||
{
|
||
return await _repository.CheckUnavailablePersonnelByShiftAsync(date, shiftId, personnelId);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取指定日期所有班次的不可用员工分布
|
||
/// </summary>
|
||
public async Task<Dictionary<long, List<long>>> GetDailyUnavailablePersonnelAsync(DateTime date)
|
||
{
|
||
return await _repository.GetDailyUnavailablePersonnelAsync(date);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 批量获取日期范围内的不可用员工分布
|
||
/// </summary>
|
||
public async Task<Dictionary<DateTime, Dictionary<long, List<long>>>> GetRangeUnavailablePersonnelAsync(DateTime startDate, DateTime endDate)
|
||
{
|
||
return await _repository.GetRangeUnavailablePersonnelAsync(startDate, endDate);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 私有辅助方法
|
||
|
||
/// <summary>
|
||
/// 根据分组获取对应的原因类型列表
|
||
/// </summary>
|
||
private static List<int> GetReasonTypesByCategory(UnavailabilityCategory category)
|
||
{
|
||
var allReasonTypes = Enum.GetValues<UnavailabilityReasonType>();
|
||
return allReasonTypes
|
||
.Where(rt => rt.GetCategory() == category)
|
||
.Select(rt => (int)rt)
|
||
.ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取星期几的中文名称
|
||
/// </summary>
|
||
private static string GetWeekdayName(DateTime date)
|
||
{
|
||
return date.DayOfWeek switch
|
||
{
|
||
DayOfWeek.Sunday => "周日",
|
||
DayOfWeek.Monday => "周一",
|
||
DayOfWeek.Tuesday => "周二",
|
||
DayOfWeek.Wednesday => "周三",
|
||
DayOfWeek.Thursday => "周四",
|
||
DayOfWeek.Friday => "周五",
|
||
DayOfWeek.Saturday => "周六",
|
||
_ => ""
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构建网格统计信息
|
||
/// </summary>
|
||
private static GridStatistics BuildGridStatistics(List<ShiftUnavailabilityEntity> entities, List<ShiftEntity> shifts)
|
||
{
|
||
var stats = new GridStatistics
|
||
{
|
||
TotalCount = entities.Count,
|
||
TemplateGeneratedCount = entities.Count(e => e.IsFromTemplate),
|
||
ManualCreatedCount = entities.Count(e => !e.IsFromTemplate)
|
||
};
|
||
|
||
// 按班次统计
|
||
stats.ShiftCounts = entities
|
||
.GroupBy(e => e.ShiftId)
|
||
.ToDictionary(g => g.Key, g => g.Count());
|
||
|
||
// 按原因类型统计
|
||
stats.ReasonTypeCounts = entities
|
||
.GroupBy(e => (UnavailabilityReasonType)e.ReasonType)
|
||
.ToDictionary(g => g.Key, g => g.Count());
|
||
|
||
// 按分组统计
|
||
stats.CategoryCounts = entities
|
||
.GroupBy(e => ((UnavailabilityReasonType)e.ReasonType).GetCategory())
|
||
.ToDictionary(g => g.Key, g => g.Count());
|
||
|
||
return stats;
|
||
}
|
||
|
||
#endregion
|
||
|
||
// 注意:批量操作方法和统计方法由于篇幅限制,将在后续实现
|
||
// 这里先提供基础CRUD和核心功能的完整实现
|
||
|
||
#region 待实现的方法(Phase 2 将完成)
|
||
|
||
/// <summary>
|
||
/// 复制模板到指定日期范围
|
||
/// 支持覆盖模式和保留模式
|
||
/// </summary>
|
||
public async Task<BatchOperationResult> CopyTemplateAsync(TemplateCopyInput input)
|
||
{
|
||
var result = new BatchOperationResult
|
||
{
|
||
TotalCount = 0,
|
||
SuccessCount = 0,
|
||
FailedCount = 0,
|
||
FailedItems = new List<BatchOperationFailedItem>()
|
||
};
|
||
|
||
try
|
||
{
|
||
// 获取模板日期的所有记录
|
||
var templateRecords = await _repository.GetByPersonnelAndDateRangeAsync(
|
||
input.PersonnelId, input.SourceDate.Date, input.SourceDate.Date);
|
||
|
||
if (!templateRecords.Any())
|
||
{
|
||
result.Message = "模板日期没有找到任何不可用记录";
|
||
return result;
|
||
}
|
||
|
||
// 准备目标日期列表
|
||
var targetDates = input.TargetDates.Select(d => d.Date).ToList();
|
||
|
||
result.TotalCount = targetDates.Count * templateRecords.Count;
|
||
|
||
// 使用事务确保数据一致性
|
||
using var uow = _repository.Orm.CreateUnitOfWork();
|
||
try
|
||
{
|
||
foreach (var targetDate in targetDates)
|
||
{
|
||
// 如果是覆盖模式,先删除目标日期的现有记录
|
||
if (input.OverwriteExisting)
|
||
{
|
||
await _repository.DeleteByConditionAsync(input.PersonnelId, targetDate, null);
|
||
}
|
||
|
||
// 复制每条模板记录到目标日期
|
||
foreach (var templateRecord in templateRecords)
|
||
{
|
||
try
|
||
{
|
||
// 检查是否已存在相同记录
|
||
var exists = await _repository.ExistsAsync(input.PersonnelId, targetDate, templateRecord.ShiftId);
|
||
|
||
if (!exists || input.OverwriteExisting)
|
||
{
|
||
// 创建新记录
|
||
var newRecord = new ShiftUnavailabilityEntity
|
||
{
|
||
PersonnelId = input.PersonnelId,
|
||
Date = targetDate,
|
||
ShiftId = templateRecord.ShiftId,
|
||
ReasonType = templateRecord.ReasonType,
|
||
Remark = $"{templateRecord.Remark} (从{input.SourceDate:yyyy-MM-dd}复制)",
|
||
Priority = templateRecord.Priority,
|
||
EffectiveStartTime = templateRecord.EffectiveStartTime,
|
||
EffectiveEndTime = templateRecord.EffectiveEndTime,
|
||
IsFromTemplate = true,
|
||
SourceTemplateDate = input.SourceDate.Date,
|
||
CreatedTime = DateTime.Now
|
||
};
|
||
|
||
if (exists && input.OverwriteExisting)
|
||
{
|
||
// 更新现有记录
|
||
await _repository.DeleteByConditionAsync(input.PersonnelId, targetDate, templateRecord.ShiftId);
|
||
}
|
||
|
||
await _repository.InsertAsync(newRecord);
|
||
result.SuccessCount++;
|
||
}
|
||
else
|
||
{
|
||
// 记录已存在且不是覆盖模式
|
||
result.FailedCount++;
|
||
result.FailedItems.Add(new BatchOperationFailedItem
|
||
{
|
||
Date = targetDate,
|
||
ShiftId = templateRecord.ShiftId,
|
||
Reason = "记录已存在"
|
||
});
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.FailedCount++;
|
||
result.FailedItems.Add(new BatchOperationFailedItem
|
||
{
|
||
Date = targetDate,
|
||
ShiftId = templateRecord.ShiftId,
|
||
Reason = $"操作失败: {ex.Message}"
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
uow.Commit();
|
||
result.IsSuccess = result.FailedCount == 0;
|
||
result.Message = result.IsSuccess ?
|
||
$"模板复制成功,共处理 {result.SuccessCount} 条记录" :
|
||
$"模板复制完成,成功 {result.SuccessCount} 条,失败 {result.FailedCount} 条";
|
||
}
|
||
catch (Exception)
|
||
{
|
||
uow.Rollback();
|
||
throw;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.IsSuccess = false;
|
||
result.Message = $"模板复制操作失败: {ex.Message}";
|
||
result.FailedCount = result.TotalCount;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置周期性模式
|
||
/// 根据周模式在指定日期范围内批量生成记录
|
||
/// </summary>
|
||
public async Task<BatchOperationResult> SetWeeklyPatternAsync(WeeklyPatternInput input)
|
||
{
|
||
var result = new BatchOperationResult
|
||
{
|
||
TotalCount = 0,
|
||
SuccessCount = 0,
|
||
FailedCount = 0,
|
||
FailedItems = new List<BatchOperationFailedItem>()
|
||
};
|
||
|
||
try
|
||
{
|
||
// 验证输入参数
|
||
if (input.WeeklyPattern?.Any() != true)
|
||
{
|
||
result.Message = "周模式配置不能为空";
|
||
return result;
|
||
}
|
||
|
||
if (input.StartDate > input.EndDate)
|
||
{
|
||
result.Message = "开始日期不能大于结束日期";
|
||
return result;
|
||
}
|
||
|
||
var processedDates = new List<DateTime>();
|
||
|
||
// 遍历日期范围内的所有日期
|
||
for (var date = input.StartDate.Date;
|
||
date <= input.EndDate.Date;
|
||
date = date.AddDays(1))
|
||
{
|
||
var dayOfWeek = (int)date.DayOfWeek;
|
||
|
||
// 检查当前星期几是否在周模式配置中
|
||
if (input.WeeklyPattern.ContainsKey(dayOfWeek) && input.WeeklyPattern[dayOfWeek]?.Any() == true)
|
||
{
|
||
processedDates.Add(date);
|
||
result.TotalCount += input.WeeklyPattern[dayOfWeek].Count;
|
||
}
|
||
}
|
||
|
||
if (result.TotalCount == 0)
|
||
{
|
||
result.Message = "根据周模式配置,没有需要处理的日期";
|
||
return result;
|
||
}
|
||
|
||
// 使用事务确保数据一致性
|
||
using var uow = _repository.Orm.CreateUnitOfWork();
|
||
try
|
||
{
|
||
foreach (var date in processedDates)
|
||
{
|
||
var dayOfWeek = (int)date.DayOfWeek;
|
||
var shiftIds = input.WeeklyPattern[dayOfWeek];
|
||
|
||
// 如果是覆盖模式,先删除该日期的现有记录
|
||
if (input.OverwriteExisting)
|
||
{
|
||
await _repository.DeleteByConditionAsync(input.PersonnelId, date, null);
|
||
}
|
||
|
||
// 为每个班次设置创建记录
|
||
foreach (var shiftId in shiftIds)
|
||
{
|
||
try
|
||
{
|
||
// 检查是否已存在记录
|
||
var exists = await _repository.ExistsAsync(input.PersonnelId, date, shiftId);
|
||
|
||
if (!exists || input.OverwriteExisting)
|
||
{
|
||
var newRecord = new ShiftUnavailabilityEntity
|
||
{
|
||
PersonnelId = input.PersonnelId,
|
||
Date = date,
|
||
ShiftId = shiftId,
|
||
ReasonType = (int)input.ReasonType,
|
||
Remark = $"{input.Remark ?? "周期性设置"} ({GetWeekdayName(date)})",
|
||
Priority = input.Priority,
|
||
IsFromTemplate = true,
|
||
SourceTemplateDate = input.StartDate,
|
||
CreatedTime = DateTime.Now
|
||
};
|
||
|
||
if (exists && input.OverwriteExisting)
|
||
{
|
||
await _repository.DeleteByConditionAsync(input.PersonnelId, date, shiftId);
|
||
}
|
||
|
||
await _repository.InsertAsync(newRecord);
|
||
result.SuccessCount++;
|
||
}
|
||
else
|
||
{
|
||
// 记录已存在且不是覆盖模式
|
||
result.FailedCount++;
|
||
result.FailedItems.Add(new BatchOperationFailedItem
|
||
{
|
||
Date = date,
|
||
ShiftId = shiftId,
|
||
Reason = "记录已存在"
|
||
});
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.FailedCount++;
|
||
result.FailedItems.Add(new BatchOperationFailedItem
|
||
{
|
||
Date = date,
|
||
ShiftId = shiftId,
|
||
Reason = $"操作失败: {ex.Message}"
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
uow.Commit();
|
||
result.IsSuccess = result.FailedCount == 0;
|
||
result.Message = result.IsSuccess ?
|
||
$"周期性设置成功,共处理 {result.SuccessCount} 条记录,涉及 {processedDates.Count} 个日期" :
|
||
$"周期性设置完成,成功 {result.SuccessCount} 条,失败 {result.FailedCount} 条";
|
||
}
|
||
catch (Exception)
|
||
{
|
||
uow.Rollback();
|
||
throw;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.IsSuccess = false;
|
||
result.Message = $"周期性设置操作失败: {ex.Message}";
|
||
result.FailedCount = result.TotalCount;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 批量设置单元格状态
|
||
/// 支持多选单元格的批量操作
|
||
/// </summary>
|
||
public async Task<BatchOperationResult> BatchSetCellsAsync(BatchCellEditInput input)
|
||
{
|
||
var result = new BatchOperationResult
|
||
{
|
||
TotalCount = 0,
|
||
SuccessCount = 0,
|
||
FailedCount = 0,
|
||
FailedItems = new List<BatchOperationFailedItem>()
|
||
};
|
||
|
||
try
|
||
{
|
||
// 验证输入参数
|
||
if (input.CellOperations?.Any() != true)
|
||
{
|
||
result.Message = "单元格操作列表不能为空";
|
||
return result;
|
||
}
|
||
|
||
result.TotalCount = input.CellOperations.Count;
|
||
|
||
// 使用事务确保数据一致性
|
||
using var uow = _repository.Orm.CreateUnitOfWork();
|
||
|
||
try
|
||
{
|
||
foreach (var operation in input.CellOperations)
|
||
{
|
||
try
|
||
{
|
||
switch (operation.Operation)
|
||
{
|
||
case BatchCellOperationType.Set:
|
||
await HandleSetOperationAsync(operation, input.MarkAsTemplate, input.Remark);
|
||
break;
|
||
|
||
case BatchCellOperationType.Clear:
|
||
await HandleClearOperationAsync(operation);
|
||
break;
|
||
|
||
case BatchCellOperationType.Toggle:
|
||
await HandleToggleOperationAsync(operation);
|
||
break;
|
||
|
||
default:
|
||
throw new ArgumentException($"不支持的操作类型: {operation.Operation}");
|
||
}
|
||
|
||
result.SuccessCount++;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.FailedCount++;
|
||
result.FailedItems.Add(new BatchOperationFailedItem
|
||
{
|
||
Date = operation.Date,
|
||
ShiftId = operation.ShiftId,
|
||
Reason = $"操作失败: {ex.Message}"
|
||
});
|
||
}
|
||
}
|
||
|
||
uow.Commit();
|
||
result.IsSuccess = result.FailedCount == 0;
|
||
result.Message = result.IsSuccess ?
|
||
$"批量设置成功,共处理 {result.SuccessCount} 个单元格" :
|
||
$"批量设置完成,成功 {result.SuccessCount} 个,失败 {result.FailedCount} 个";
|
||
}
|
||
catch (Exception)
|
||
{
|
||
uow.Rollback();
|
||
throw;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.IsSuccess = false;
|
||
result.Message = $"批量单元格设置操作失败: {ex.Message}";
|
||
result.FailedCount = result.TotalCount;
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理设置操作
|
||
/// </summary>
|
||
private async Task HandleSetOperationAsync(BatchCellOperation operation, bool markAsTemplate, string batchRemark)
|
||
{
|
||
if (!operation.ReasonType.HasValue)
|
||
{
|
||
throw new ArgumentException("Set操作必须指定原因类型");
|
||
}
|
||
|
||
// 先删除现有记录
|
||
await _repository.DeleteByConditionAsync(operation.PersonnelId, operation.Date, operation.ShiftId);
|
||
|
||
// 创建新记录
|
||
var newRecord = new ShiftUnavailabilityEntity
|
||
{
|
||
PersonnelId = operation.PersonnelId,
|
||
Date = operation.Date,
|
||
ShiftId = operation.ShiftId,
|
||
ReasonType = (int)operation.ReasonType.Value,
|
||
Remark = operation.Remark ?? batchRemark ?? "批量设置",
|
||
Priority = operation.Priority ?? 1,
|
||
EffectiveStartTime = operation.EffectiveStartTime.HasValue ? TimeSpan.FromTicks(operation.EffectiveStartTime.Value.Ticks) : null,
|
||
EffectiveEndTime = operation.EffectiveEndTime.HasValue ? TimeSpan.FromTicks(operation.EffectiveEndTime.Value.Ticks) : null,
|
||
IsFromTemplate = markAsTemplate,
|
||
SourceTemplateDate = markAsTemplate ? DateTime.Today : null,
|
||
CreatedTime = DateTime.Now
|
||
};
|
||
|
||
await _repository.InsertAsync(newRecord);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理清除操作
|
||
/// </summary>
|
||
private async Task HandleClearOperationAsync(BatchCellOperation operation)
|
||
{
|
||
await _repository.DeleteByConditionAsync(operation.PersonnelId, operation.Date, operation.ShiftId);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理切换操作
|
||
/// </summary>
|
||
private async Task HandleToggleOperationAsync(BatchCellOperation operation)
|
||
{
|
||
var exists = await _repository.ExistsAsync(operation.PersonnelId, operation.Date, operation.ShiftId);
|
||
|
||
if (exists)
|
||
{
|
||
// 删除现有记录
|
||
await _repository.DeleteByConditionAsync(operation.PersonnelId, operation.Date, operation.ShiftId);
|
||
}
|
||
else
|
||
{
|
||
// 创建新记录,使用个人意愿作为默认原因
|
||
var newRecord = new ShiftUnavailabilityEntity
|
||
{
|
||
PersonnelId = operation.PersonnelId,
|
||
Date = operation.Date,
|
||
ShiftId = operation.ShiftId,
|
||
ReasonType = (int)UnavailabilityReasonType.PersonalPreference,
|
||
Remark = operation.Remark ?? "切换操作",
|
||
Priority = operation.Priority ?? 1,
|
||
EffectiveStartTime = operation.EffectiveStartTime.HasValue ? TimeSpan.FromTicks(operation.EffectiveStartTime.Value.Ticks) : null,
|
||
EffectiveEndTime = operation.EffectiveEndTime.HasValue ? TimeSpan.FromTicks(operation.EffectiveEndTime.Value.Ticks) : null,
|
||
CreatedTime = DateTime.Now
|
||
};
|
||
|
||
await _repository.InsertAsync(newRecord);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取指定员工的不可用统计数据
|
||
/// </summary>
|
||
public async Task<UnavailabilityStatistics> GetStatisticsAsync(long personnelId, DateTime startDate, DateTime endDate)
|
||
{
|
||
try
|
||
{
|
||
// 获取日期范围内的所有记录
|
||
var records = await _repository.GetByPersonnelAndDateRangeAsync(personnelId, startDate, endDate);
|
||
|
||
// 获取所有班次信息
|
||
var shifts = await _shiftRepository.Select
|
||
.Where(x => x.IsEnabled)
|
||
.ToListAsync();
|
||
|
||
var totalDays = (endDate.Date - startDate.Date).Days + 1;
|
||
var totalPossibleSlots = totalDays * shifts.Count;
|
||
|
||
var statistics = new UnavailabilityStatistics
|
||
{
|
||
PersonnelId = personnelId,
|
||
StartDate = startDate,
|
||
EndDate = endDate,
|
||
TotalDays = totalDays,
|
||
TotalRecords = records.Count,
|
||
TotalPossibleSlots = totalPossibleSlots,
|
||
UnavailableRate = totalPossibleSlots > 0 ? (double)records.Count / totalPossibleSlots * 100 : 0
|
||
};
|
||
|
||
// 按班次统计
|
||
statistics.ShiftStatistics = shifts.Select(shift => new ShiftStatistics
|
||
{
|
||
ShiftId = shift.Id,
|
||
ShiftName = shift.Name,
|
||
Count = records.Count(r => r.ShiftId == shift.Id),
|
||
Percentage = totalDays > 0 ? (decimal)((double)records.Count(r => r.ShiftId == shift.Id) / totalDays * 100) : 0
|
||
}).ToList();
|
||
|
||
// 按原因类型统计
|
||
statistics.ReasonTypeStatistics = records
|
||
.GroupBy(r => (UnavailabilityReasonType)r.ReasonType)
|
||
.Select(g => new ReasonTypeStatistics
|
||
{
|
||
ReasonType = g.Key,
|
||
Count = g.Count(),
|
||
Percentage = records.Count > 0 ? (decimal)((double)g.Count() / records.Count * 100) : 0,
|
||
AffectedDays = g.Select(r => r.Date.Date).Distinct().Count()
|
||
}).OrderByDescending(s => s.Count).ToList();
|
||
|
||
// 按分类统计
|
||
statistics.CategoryStatistics = records
|
||
.GroupBy(r => ((UnavailabilityReasonType)r.ReasonType).GetCategory())
|
||
.Select(g => new CategoryStatistics
|
||
{
|
||
Category = g.Key,
|
||
Count = g.Count(),
|
||
Percentage = records.Count > 0 ? (decimal)((double)g.Count() / records.Count * 100) : 0,
|
||
AffectedDays = g.Select(r => r.Date.Date).Distinct().Count(),
|
||
ReasonTypes = g.Select(r => (UnavailabilityReasonType)r.ReasonType).Distinct().ToList()
|
||
}).OrderByDescending(s => s.Count).ToList();
|
||
|
||
// 按星期几统计
|
||
statistics.WeekdayStatistics = records
|
||
.GroupBy(r => r.Date.DayOfWeek)
|
||
.Select(g => new WeekdayStatistics
|
||
{
|
||
DayOfWeek = g.Key,
|
||
DayName = GetWeekdayName(DateTime.Today.AddDays((int)g.Key - (int)DateTime.Today.DayOfWeek)),
|
||
Count = g.Count(),
|
||
AffectedDates = g.Select(r => r.Date.Date).Distinct().Count()
|
||
}).OrderBy(s => s.DayOfWeek).ToList();
|
||
|
||
// 模板生成统计
|
||
var templateRecords = records.Where(r => r.IsFromTemplate).ToList();
|
||
statistics.TemplateStatistics = new TemplateStatistics
|
||
{
|
||
TotalTemplateRecords = templateRecords.Count,
|
||
TemplateGeneratedRate = records.Count > 0 ? (decimal)((double)templateRecords.Count / records.Count * 100) : 0,
|
||
ManualCreatedRecords = records.Count - templateRecords.Count,
|
||
ManualCreatedRate = records.Count > 0 ? (decimal)((double)(records.Count - templateRecords.Count) / records.Count * 100) : 0
|
||
};
|
||
|
||
// 月度趋势(如果时间范围超过一个月)
|
||
if (totalDays > 31)
|
||
{
|
||
statistics.MonthlyTrend = records
|
||
.GroupBy(r => new { Year = r.Date.Year, Month = r.Date.Month })
|
||
.Select(g => new MonthlyTrendItem
|
||
{
|
||
Year = g.Key.Year,
|
||
Month = g.Key.Month,
|
||
Count = g.Count(),
|
||
UnavailableDays = g.Select(r => r.Date.Date).Distinct().Count(),
|
||
MonthDisplay = $"{g.Key.Year}年{g.Key.Month:00}月"
|
||
}).OrderBy(t => t.Year).ThenBy(t => t.Month).ToList();
|
||
}
|
||
|
||
return statistics;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
throw new Exception($"获取统计数据失败: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取团队成员的不可用报告
|
||
/// </summary>
|
||
public async Task<List<UnavailabilityStatistics>> GetTeamReportAsync(List<long> personnelIds, DateTime month)
|
||
{
|
||
try
|
||
{
|
||
var monthStart = new DateTime(month.Year, month.Month, 1);
|
||
var monthEnd = monthStart.AddMonths(1).AddDays(-1);
|
||
|
||
var teamReports = new List<UnavailabilityStatistics>();
|
||
|
||
// 并发获取每个成员的统计数据
|
||
var tasks = personnelIds.Select(async personnelId =>
|
||
{
|
||
var statistics = await GetStatisticsAsync(personnelId, monthStart, monthEnd);
|
||
return statistics;
|
||
});
|
||
|
||
var results = await Task.WhenAll(tasks);
|
||
teamReports.AddRange(results);
|
||
|
||
// 对结果按不可用率排序
|
||
return teamReports.OrderByDescending(r => r.UnavailableRate).ToList();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
throw new Exception($"获取团队报告失败: {ex.Message}", ex);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
} |