using CRMBLL.Task;
using CRMDAL.DTO.Task;
using ECPBLL.Meeting.Interfaces;
using ECPDAL.Meeting.DTOs;
using ECPDAL.Meeting.Interfaces;
using GB5Shared.DaprCache;
using GB5Shared.DTO.Framework.Login;
using GB5Shared.EventLogPublish;
using GB5Shared.GB5Constant;
using GB5Shared.Telemetry;
using Newtonsoft.Json;
namespace ECPBLL.Meeting.Implementations;
///
/// The Meeting "Action Items" tab — a thin translation layer over the existing shared TTASK
/// entity (CRMBLL.Task.ITaskBLL), per the ECP preview's own framing: "the same underlying task
/// record used across GB5, not an ECP-only checklist." Every CRM-only field on TaskDTO
/// (BizTransactionType, PartyBranch, Asset, Machine, Allocation, WBS codes, plan/expire dates,
/// ...) is defaulted here and never exposed on IMeetingActionItemBLL's own DTOs.
///
/// List/SelectList go through IMeetingActionItemDAL (ECP-owned, direct TTASK query) rather than
/// ITaskBLL, since ITaskBLL's own list methods (GetSelectListTask/GetTaskReport) don't support
/// filtering by ObjectTypeId+ObjectId — see MeetingActionItemQB's doc-comment for the confirmed
/// column provenance.
///
public class MeetingActionItemBLL(
ITaskBLL _taskBll,
IMeetingActionItemDAL _dal,
KeyInvalidate _keyInvalidate,
EventLogPublish _eventLog
) : IMeetingActionItemBLL
{
// Confirmed against MBIZTRANSACTIONTYPE: BIZTRANSACTIONTYPEID = -1499999993, Code/Name
// "Task" — the generic CRM Task BizTransactionType, reused here rather than seeding a
// separate "ECP Meeting Action Item" row since it carries no ECP-specific routing/workflow.
private const int BizTransactionTypeId_EcpMeetingActionItem = -1499999993;
// TaskAssignedToType: 3 = user, per CRM's TaskQB ASSIGNEDTOTYPE join convention
// (2=usergroup, 3=user, 4=employee, 5=worktype, 6=contact).
private const byte AssignedToType_User = 3;
public async Task GetMeetingActionItemAsync(int taskId, LoginDTO login, CancellationToken ct)
{
GB5Trace.Step("get-meeting-action-item", new { taskId });
var json = await _taskBll.GetTask(taskId, login).ConfigureAwait(false);
var task = JsonConvert.DeserializeObject(json)
?? throw new InvalidOperationException($"Action item {taskId} not found.");
return JsonConvert.SerializeObject(ToActionItemDTO(task));
}
public async Task GetMeetingActionItemListAsync(int meetingId, LoginDTO login, CancellationToken ct)
{
GB5Trace.Step("get-meeting-action-item-list", new { meetingId });
var list = await _dal.GetActionItemsForMeetingAsync(Constant.EntityConstant.OBJECTMEETING, meetingId, login, ct)
.ConfigureAwait(false);
return JsonConvert.SerializeObject(list);
}
public async Task GetSelectListMeetingActionItemAsync(int meetingId, LoginDTO login, CancellationToken ct)
{
GB5Trace.Step("get-selectlist-meeting-action-item", new { meetingId });
var list = await _dal.GetSelectListActionItemsForMeetingAsync(Constant.EntityConstant.OBJECTMEETING, meetingId, login, ct)
.ConfigureAwait(false);
return JsonConvert.SerializeObject(list);
}
public async Task SaveMeetingActionItemAsync(SaveMeetingActionItemDTO dto, LoginDTO login, CancellationToken ct)
{
GB5Trace.Step("save-meeting-action-item", new { dto.TaskId, dto.MeetingId });
if (string.IsNullOrWhiteSpace(dto.Title))
throw new InvalidOperationException("Title is required.");
TaskDTO task;
bool isNew = dto.TaskId == 0;
if (isNew)
{
task = new TaskDTO
{
TaskId = 0,
ObjectTypeId = Constant.EntityConstant.OBJECTMEETING,
ObjectId = dto.MeetingId,
TaskTaskName = dto.Title,
TaskDescription = dto.Title, // TaskDescription is required by ValidateTask; the
// ECP preview's task modal has no separate description field.
TaskAssignedToType = AssignedToType_User,
AssignedToUserId = dto.AssignedTo,
ParentTaskId = dto.ParentTaskId,
TaskPlanStartOn = DateOnly.FromDateTime(DateTime.UtcNow),
TaskPlanEndOn = dto.DueDate,
TaskDueOn = dto.DueDate,
TaskExpireOn = dto.DueDate,
BizTransactionTypeId = BizTransactionTypeId_EcpMeetingActionItem,
};
}
else
{
var existingJson = await _taskBll.GetTask(dto.TaskId, login).ConfigureAwait(false);
task = JsonConvert.DeserializeObject(existingJson)
?? throw new InvalidOperationException($"Action item {dto.TaskId} not found.");
task.TaskTaskName = dto.Title;
task.TaskDescription = dto.Title;
task.AssignedToUserId = dto.AssignedTo;
task.ParentTaskId = dto.ParentTaskId;
task.TaskPlanEndOn = dto.DueDate;
task.TaskDueOn = dto.DueDate;
task.TaskExpireOn = dto.DueDate;
}
var result = await _taskBll.SaveTask([task], login).ConfigureAwait(false);
var eventTypeId = isNew
? Constant.EventTypeConstant.MEETINGACTIONITEMCREATEDEVENTTYPEID
: Constant.EventTypeConstant.MEETINGACTIONITEMUPDATEDEVENTTYPEID;
GB5Trace.Step("event-publish", new { eventTypeId });
await _eventLog.PublishEventLogAsync(
isNew ? "Meeting Action Item Created" : "Meeting Action Item Updated", task,
eventTypeId, dto.MeetingId, login, ct: ct)
.ConfigureAwait(false);
await InvalidateActionItemListCacheAsync(dto.MeetingId, login).ConfigureAwait(false);
if (!isNew)
await InvalidateActionItemDetailCacheAsync(dto.TaskId, login).ConfigureAwait(false);
return result;
}
public async Task DeleteMeetingActionItemAsync(int taskId, LoginDTO login, CancellationToken ct)
{
GB5Trace.Step("delete-meeting-action-item", new { taskId });
// Fetched before delete purely to recover MeetingId for cache invalidation — TTASK's
// ObjectId (the meeting) isn't passed by the caller, only the TaskId.
var existingJson = await _taskBll.GetTask(taskId, login).ConfigureAwait(false);
var existing = JsonConvert.DeserializeObject(existingJson);
var result = await _taskBll.DeleteTask(taskId, login).ConfigureAwait(false);
GB5Trace.Step("event-publish", new { Constant.EventTypeConstant.MEETINGACTIONITEMDELETEDEVENTTYPEID });
await _eventLog.PublishEventLogAsync(
"Meeting Action Item Deleted", new { taskId },
Constant.EventTypeConstant.MEETINGACTIONITEMDELETEDEVENTTYPEID, taskId, login, ct: ct)
.ConfigureAwait(false);
await InvalidateActionItemDetailCacheAsync(taskId, login).ConfigureAwait(false);
if (existing is not null)
await InvalidateActionItemListCacheAsync(existing.ObjectId, login).ConfigureAwait(false);
return result;
}
public async Task RescheduleMeetingActionItemAsync(RescheduleMeetingActionItemDTO dto, LoginDTO login, CancellationToken ct)
{
GB5Trace.Step("reschedule-meeting-action-item", new { dto.TaskId, dto.NewDate, dto.Reason });
var json = await _taskBll.GetTask(dto.TaskId, login).ConfigureAwait(false);
var task = JsonConvert.DeserializeObject(json)
?? throw new InvalidOperationException($"Action item {dto.TaskId} not found.");
var previousDueDate = task.TaskDueOn;
task.TaskDueOn = dto.NewDate;
task.TaskPlanEndOn = dto.NewDate;
task.TaskExpireOn = dto.NewDate;
var result = await _taskBll.SaveTask([task], login).ConfigureAwait(false);
// TTASK has no revision-history column — the reason is preserved only in the event log,
// not on the task row itself (see this class's / RescheduleMeetingActionItemDTO's doc-comments).
GB5Trace.Step("event-publish", new { dto.TaskId, previousDueDate, dto.NewDate });
await _eventLog.PublishEventLogAsync(
"Meeting Action Item Rescheduled",
new { dto.TaskId, From = previousDueDate, To = dto.NewDate, dto.Reason },
Constant.EventTypeConstant.MEETINGACTIONITEMRESCHEDULEDEVENTTYPEID, dto.TaskId, login, ct: ct)
.ConfigureAwait(false);
await InvalidateActionItemDetailCacheAsync(dto.TaskId, login).ConfigureAwait(false);
await InvalidateActionItemListCacheAsync(task.ObjectId, login).ConfigureAwait(false);
return result;
}
// Clears GetMeetingActionItemList + GetSelectListMeetingActionItem (both keyed by MeetingId).
private async Task InvalidateActionItemListCacheAsync(int meetingId, LoginDTO login)
{
var keyGen = new CacheKeyGeneration();
var listKey = keyGen.KeyGeneration(
meetingId.ToString(), Constant.EntityConstant.OBJECTTASKLIST, Constant.CacheKeyLevel.CLIENT_LEVEL, login);
await _keyInvalidate.InvalidateCache(listKey).ConfigureAwait(false);
var selectListKey = keyGen.KeyGeneration(
meetingId.ToString(), Constant.EntityConstant.OBJECTTASKSELECTLIST, Constant.CacheKeyLevel.CLIENT_LEVEL, login);
await _keyInvalidate.InvalidateCache(selectListKey).ConfigureAwait(false);
}
// Clears GetMeetingActionItem's own per-id cache — needed by any mutation on an existing task
// (update/delete/reschedule; never the create path, which has no prior cache to clear).
private async Task InvalidateActionItemDetailCacheAsync(int taskId, LoginDTO login)
{
var cacheKey = new CacheKeyGeneration().KeyGeneration(
taskId, Constant.EntityConstant.OBJECTTASK, Constant.CacheKeyLevel.CLIENT_LEVEL, login);
await _keyInvalidate.InvalidateCache(cacheKey).ConfigureAwait(false);
}
private static MeetingActionItemDTO ToActionItemDTO(TaskDTO task) => new()
{
TaskId = task.TaskId,
MeetingId = task.ObjectId,
Title = task.TaskTaskName,
AssignedTo = task.AssignedToUserId,
AssignedToName = task.AssignedToUserName,
DueDate = task.TaskDueOn,
ParentTaskId = task.ParentTaskId,
WbsLevel = task.TaskWbcLevel,
Status = task.TaskStatus,
};
}