Add new domain models and interfaces for project management features

This commit is contained in:
2025-12-08 14:47:03 +03:30
parent b7a7fb01d7
commit 27e8a26ed8
295 changed files with 24896 additions and 26 deletions

View File

@@ -0,0 +1,134 @@
using System.ComponentModel.DataAnnotations.Schema;
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.CheckoutAgg.Enums;
namespace GozareshgirProgramManager.Domain.CheckoutAgg.Entities;
public class Checkout : EntityBase<Guid>
{
/// <summary>
/// ایجاد فیش
/// </summary>
/// <param name="checkoutStartDate"></param>
/// <param name="checkoutEndDate"></param>
/// <param name="year"></param>
/// <param name="month"></param>
/// <param name="fullName"></param>
/// <param name="userId"></param>
/// <param name="mandatoryHours"></param>
/// <param name="totalHoursWorked"></param>
/// <param name="totalDaysWorked"></param>
/// <param name="remainingHours"></param>
/// <param name="monthlySalaryDefined"></param>
/// <param name="monthlySalaryPay"></param>
/// <param name="deductionFromSalary"></param>
public Checkout(DateTime checkoutStartDate, DateTime checkoutEndDate, int year, int month, string fullName, long userId,
int mandatoryHours, int totalHoursWorked, int totalDaysWorked, int remainingHours, double monthlySalaryDefined, double monthlySalaryPay, double deductionFromSalary)
{
CheckoutStartDate = checkoutStartDate;
CheckoutEndDate = checkoutEndDate;
Year = year;
Month = month;
FullName = fullName;
UserId = userId;
MandatoryHours = mandatoryHours;
TotalHoursWorked = totalHoursWorked;
TotalDaysWorked = totalDaysWorked;
RemainingHours = remainingHours;
MonthlySalaryDefined = monthlySalaryDefined;
MonthlySalaryPay = monthlySalaryPay;
DeductionFromSalary = deductionFromSalary;
}
/// <summary>
/// تاریخ شروع فیش حقوقی
/// </summary>
public DateTime CheckoutStartDate { get; private set; }
/// <summary>
/// تاریخ پایان فیش حقوقی
/// </summary>
public DateTime CheckoutEndDate { get; private set; }
/// <summary>
/// سال
/// </summary>
public int Year { get; private set; }
/// <summary>
/// ماه
/// </summary>
public int Month { get; private set; }
[NotMapped]
public string PersianMonthName=> Month.ToFarsiMonthByIntNumber();
/// <summary>
/// نام کامل کاربر
/// </summary>
public string FullName { get; private set; }
/// <summary>
/// آی دی کاربر
/// </summary>
public long UserId { get; private set; }
/// <summary>
/// ساعت موظفی
/// </summary>
public int MandatoryHours { get; private set; }
/// <summary>
/// مجموع ساعات کارکرد پرسنل
/// </summary>
public int TotalHoursWorked { get; private set; }
/// <summary>
/// مجمع روزهای کارکرد پرسنل
/// </summary>
public int TotalDaysWorked { get; private set; }
/// <summary>
/// ساعات باقی مانده
/// کسر کار یا اضافه کار
/// </summary>
public int RemainingHours { get; private set; }
/// <summary>
/// حقوق ماهانه
/// تعیین شده
/// </summary>
public double MonthlySalaryDefined { get; private set; }
/// <summary>
/// حقوق نهایی که به پرسنل داده می شود
/// </summary>
public double MonthlySalaryPay { get; private set; }
/// <summary>
/// کسر از حقوق
/// </summary>
public double DeductionFromSalary { get; private set; }
/// <summary>
/// ویرایش فیش
/// </summary>
/// <param name="mandatoryHours"></param>
/// <param name="totalHoursWorked"></param>
/// <param name="totalDaysWorked"></param>
/// <param name="remainingHours"></param>
/// <param name="monthlySalaryDefined"></param>
/// <param name="monthlySalaryPay"></param>
/// <param name="deductionFromSalary"></param>
public void Edit(int mandatoryHours, int totalHoursWorked, int totalDaysWorked, int remainingHours, double monthlySalaryDefined, double monthlySalaryPay, double deductionFromSalary)
{
MandatoryHours = mandatoryHours;
TotalHoursWorked = totalHoursWorked;
TotalDaysWorked = totalDaysWorked;
RemainingHours = remainingHours;
MonthlySalaryDefined = monthlySalaryDefined;
MonthlySalaryPay = monthlySalaryPay;
DeductionFromSalary = deductionFromSalary;
}
}

View File

@@ -0,0 +1,18 @@
namespace GozareshgirProgramManager.Domain.CheckoutAgg.Enums;
public enum CreateCheckoutStatus
{
/// <summary>
/// آماده ایجاد
/// </summary>
ReadyToCreate = 1,
/// <summary>
/// قبلا ایجاد شده
/// </summary>
AlreadyCreated = 2,
/// <summary>
/// تنظیمات حقوق انجام نشده
/// </summary>
NotSetSalaryPaymentSettings = 3
}

View File

@@ -0,0 +1,18 @@
namespace GozareshgirProgramManager.Domain.CheckoutAgg.Enums;
public enum PersianMonthName
{
فروردین = 1,
اردیبهشت = 2,
خرداد = 3,
تیر = 4,
مرداد = 5,
شهریور = 6,
مهر = 7,
آبان = 8,
آذر = 9,
دی = 10,
بهمن = 11,
اسفند = 12,
}

View File

@@ -0,0 +1,22 @@
namespace GozareshgirProgramManager.Domain.CheckoutAgg.Enums;
/// <summary>
/// انتخاب حالت ایجاد یا ویراش تکی / گروهی
/// </summary>
public enum TypeOfCheckoutHandler
{
/// <summary>
/// ایجاد گروهی
/// </summary>
CreateInGroup = 1,
/// <summary>
/// ویرایش گروهی
/// </summary>
GroupEditing = 2,
/// <summary>
/// ویرایش تکی
/// </summary>
SingleEdit = 3,
}

View File

@@ -0,0 +1,9 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.CheckoutAgg.Entities;
namespace GozareshgirProgramManager.Domain.CheckoutAgg.Repositories;
public interface ICheckoutRepository : IRepository<Guid, Checkout>
{
Task<List<Checkout>> GetCheckoutListByIds(List<Guid> checkoutIds);
}

View File

@@ -0,0 +1,34 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.CustomerAgg.Events;
namespace GozareshgirProgramManager.Domain.CustomerAgg;
public class Customer : EntityBase<Guid>, IAggregateRoot
{
public string Name { get; private set; }
public string Email { get; private set; }
public DateTime CreatedAt { get; private set; }
private Customer() { } // For EF Core
private Customer(Guid id, string name, string email)
{
Id = id;
Name = name;
Email = email;
CreatedAt = DateTime.UtcNow;
}
public static Customer Create(string name, string email)
{
var customer = new Customer(Guid.NewGuid(), name, email);
customer.AddDomainEvent(new CustomerRegistered(customer.Id, customer.Name, customer.Email));
return customer;
}
public void UpdateName(string name)
{
Name = name;
}
}

View File

@@ -0,0 +1,12 @@
using GozareshgirProgramManager.Domain._Common;
namespace GozareshgirProgramManager.Domain.CustomerAgg.Events;
public record CustomerRegistered(
Guid CustomerId,
string Name,
string Email) : IDomainEvent
{
public DateTime OccurredOn { get; } = DateTime.UtcNow;
}

View File

@@ -0,0 +1,10 @@
namespace GozareshgirProgramManager.Domain.CustomerAgg.Exceptions;
public class CustomerNotFoundException : Exception
{
public CustomerNotFoundException(Guid customerId)
: base($"Customer with ID '{customerId}' was not found.")
{
}
}

View File

@@ -0,0 +1,10 @@
namespace GozareshgirProgramManager.Domain.CustomerAgg.Repositories;
public interface ICustomerRepository
{
Task<Customer?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task AddAsync(Customer customer, CancellationToken cancellationToken = default);
void Update(Customer customer);
void Delete(Customer customer);
}

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DNTPersianUtils.Core" Version="6.7.1" />
<PackageReference Include="PersianTools.Core" Version="2.0.4" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,25 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.HolidayItemAgg;
namespace GozareshgirProgramManager.Domain.HolidayAgg;
public class Holiday : EntityBase<long>
{
public Holiday(string year)
{
Year = year;
}
public string Year { get; private set; }
public List<HolidayItem> HolidayItems { get; set; }
public Holiday()
{
HolidayItems = new List<HolidayItem>();
}
public void Edit(string year)
{
Year = year;
}
}

View File

@@ -0,0 +1,28 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.HolidayAgg;
namespace GozareshgirProgramManager.Domain.HolidayItemAgg;
public class HolidayItem : EntityBase<long>
{
public HolidayItem(DateTime holidaydate, long holidayId, string holidayYear)
{
Holidaydate = holidaydate;
HolidayId = holidayId;
HolidayYear = holidayYear;
}
public DateTime Holidaydate { get; private set; }
public long HolidayId { get; private set; }
public string HolidayYear { get; private set; }
public Holiday Holidayss { get; set; }
public void Edit(DateTime holidaydate, long holidayId, string holidayYear)
{
Holidaydate = holidaydate;
HolidayId = holidayId;
HolidayYear = holidayYear;
}
}

View File

@@ -0,0 +1,21 @@
using GozareshgirProgramManager.Domain.RoleAgg.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using GozareshgirProgramManager.Domain._Common;
namespace GozareshgirProgramManager.Domain.PermissionAgg.Entities;
public class Permission
{
public long Id { get; private set; }
public int Code { get; private set; }
public Role Role { get; private set; }
public Permission(int code)
{
Code = code;
}
}

View File

@@ -0,0 +1,41 @@
using GozareshgirProgramManager.Domain._Common;
namespace GozareshgirProgramManager.Domain.ProjectAgg.Entities;
/// <summary>
/// بخش فاز - برای ذخیره تخصیص کاربر و مهارت در سطح Phase
/// </summary>
public class PhaseSection : EntityBase<Guid>
{
private PhaseSection() { }
public PhaseSection(Guid phaseId, long userId, Guid skillId)
{
PhaseId = phaseId;
UserId = userId;
SkillId = skillId;
}
public Guid PhaseId { get; private set; }
public long UserId { get; private set; }
public Guid SkillId { get; private set; }
// Navigation property
public ProjectPhase Phase { get; private set; } = null!;
public void UpdateUser(long userId)
{
UserId = userId;
}
public void UpdateSkill(Guid skillId)
{
SkillId = skillId;
}
public void Update(long userId, Guid skillId)
{
UserId = userId;
SkillId = skillId;
}
}

View File

@@ -0,0 +1,174 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using GozareshgirProgramManager.Domain.ProjectAgg.Events;
namespace GozareshgirProgramManager.Domain.ProjectAgg.Entities;
/// <summary>
/// پروژه - بالاترین سطح در سلسله مراتب و Aggregate Root
/// </summary>
public class Project : ProjectHierarchyNode
{
private readonly List<ProjectPhase> _phases;
private readonly List<ProjectSection> _projectSections;
private Project()
{
_phases = new List<ProjectPhase>();
_projectSections = new List<ProjectSection>();
}
public Project(string name, string? description = null) : base(name, description)
{
_phases = new List<ProjectPhase>();
_projectSections = new List<ProjectSection>();
AddDomainEvent(new ProjectCreatedEvent(Id, name));
}
public IReadOnlyList<ProjectPhase> Phases => _phases.AsReadOnly();
public IReadOnlyList<ProjectSection> ProjectSections => _projectSections.AsReadOnly();
// Project-specific properties
public DateTime? StartDate { get; private set; }
public DateTime? EndDate { get; private set; }
public DateTime? PlannedStartDate { get; private set; }
public DateTime? PlannedEndDate { get; private set; }
public ProjectStatus Status { get; private set; } = ProjectStatus.Planning;
#region Phase Management
public ProjectPhase AddPhase(string name, string? description = null)
{
var phase = new ProjectPhase(name, Id, description);
_phases.Add(phase);
AddDomainEvent(new PhaseAddedEvent(phase.Id, Id, name));
return phase;
}
public void RemovePhase(Guid phaseId)
{
var phase = _phases.FirstOrDefault(p => p.Id == phaseId);
if (phase == null)
throw new InvalidOperationException("فاز مورد نظر یافت نشد");
if (phase.Tasks.Any())
throw new InvalidOperationException("نمی‌توان فازی را که شامل تسک است حذف کرد");
_phases.Remove(phase);
AddDomainEvent(new PhaseRemovedEvent(phaseId, Id));
}
#endregion
#region ProjectSection Management
public void AddProjectSection(long userId, Guid skillId)
{
var existingSection = _projectSections.FirstOrDefault(s => s.UserId == userId && s.SkillId == skillId);
if (existingSection == null)
{
var section = new ProjectSection(Id, userId, skillId);
_projectSections.Add(section);
}
}
public void ClearProjectSections()
{
_projectSections.Clear();
}
#endregion
#region Date Management
public void SetDates(DateTime? startDate = null, DateTime? endDate = null)
{
if (startDate.HasValue && endDate.HasValue && startDate > endDate)
throw new ArgumentException("تاریخ شروع نمی‌تواند بعد از تاریخ پایان باشد");
StartDate = startDate;
EndDate = endDate;
}
public void SetPlannedDates(DateTime? plannedStartDate = null, DateTime? plannedEndDate = null)
{
if (plannedStartDate.HasValue && plannedEndDate.HasValue && plannedStartDate > plannedEndDate)
throw new ArgumentException("تاریخ شروع برنامه‌ریزی شده نمی‌تواند بعد از تاریخ پایان برنامه‌ریزی شده باشد");
PlannedStartDate = plannedStartDate;
PlannedEndDate = plannedEndDate;
}
public void UpdateStatus(ProjectStatus status)
{
Status = status;
AddDomainEvent(new ProjectStatusUpdatedEvent(Id, status));
}
#endregion
#region Assignment Management
public void AssignToUser(long userId, bool cascadeToPhases = true, bool forceOverride = false)
{
HasAssignmentOverride = true;
if (cascadeToPhases)
{
foreach (var phase in _phases)
{
if (phase.HasAssignmentOverride && !forceOverride)
continue;
phase.AssignToUser(userId, cascadeToTasks: true, forceOverride, markAsOverride: false);
}
}
AddDomainEvent(new ProjectAssignedEvent(Id, userId));
}
public void Unassign(bool cascadeToPhases = true, bool forceOverride = false)
{
HasAssignmentOverride = true;
if (cascadeToPhases)
{
foreach (var phase in _phases)
{
if (phase.HasAssignmentOverride && !forceOverride)
continue;
phase.Unassign(cascadeToTasks: true, forceOverride, markAsOverride: false);
}
}
AddDomainEvent(new ProjectUnassignedEvent(Id));
}
#endregion
#region Time Calculation
public override TimeSpan GetTotalTimeSpent()
{
return TimeSpan.FromTicks(_phases.Sum(p => p.GetTotalTimeSpent().Ticks));
}
public override TimeSpan GetTotalEstimatedTime()
{
return TimeSpan.FromTicks(_phases.Sum(p => p.GetTotalEstimatedTime().Ticks));
}
#endregion
public void UpdateProjectSectionUser(Guid skillId, long userId)
{
var section = _projectSections.FirstOrDefault(s => s.SkillId == skillId);
if (section != null)
{
section.UpdateUser(userId);
}
}
}

View File

@@ -0,0 +1,78 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
namespace GozareshgirProgramManager.Domain.ProjectAgg.Entities;
/// <summary>
/// کلاس پایه برای تمام نودهای سلسله مراتبی پروژه
/// </summary>
public abstract class ProjectHierarchyNode : EntityBase<Guid>
{
protected ProjectHierarchyNode()
{
}
protected ProjectHierarchyNode(string name, string? description = null)
{
Name = name;
Description = description;
}
public string Name { get; protected set; } = string.Empty;
public string? Description { get; protected set; }
// Time allocation properties
public bool HasAssignmentOverride { get; protected set; }
#region Update Methods
public virtual void UpdateName(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("نام نمی‌تواند خالی باشد", nameof(name));
Name = name;
}
public virtual void UpdateDescription(string? description)
{
Description = description;
}
public void MarkAsOverridden()
{
HasAssignmentOverride = true;
}
#endregion
// #region Time Management
//
// public virtual void SetAllocatedTime(TimeSpan time, bool markAsOverride = true)
// {
// if (time < TimeSpan.Zero)
// throw new ArgumentException("زمان تخصیص‌یافته نمی‌تواند منفی باشد", nameof(time));
//
// AllocatedTime = time;
// if (markAsOverride)
// {
// HasTimeOverride = true;
// }
// }
//
// public virtual void ClearTimeOverride()
// {
// HasTimeOverride = false;
// AllocatedTime = null;
// }
//
// #endregion
#region Time Calculation (Abstract)
public abstract TimeSpan GetTotalTimeSpent();
public abstract TimeSpan GetTotalEstimatedTime();
#endregion
}

View File

@@ -0,0 +1,199 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using GozareshgirProgramManager.Domain.ProjectAgg.Events;
namespace GozareshgirProgramManager.Domain.ProjectAgg.Entities;
/// <summary>
/// فاز پروژه - سطح میانی در سلسله مراتب
/// </summary>
public class ProjectPhase : ProjectHierarchyNode
{
private readonly List<ProjectTask> _tasks;
private readonly List<PhaseSection> _phaseSections;
private ProjectPhase()
{
_tasks = new List<ProjectTask>();
_phaseSections = new List<PhaseSection>();
}
public ProjectPhase(string name, Guid projectId, string? description = null) : base(name, description)
{
ProjectId = projectId;
_tasks = new List<ProjectTask>();
_phaseSections = new List<PhaseSection>();
AddDomainEvent(new PhaseCreatedEvent(Id, projectId, name));
}
public Guid ProjectId { get; private set; }
public Project Project { get; private set; } = null!;
public IReadOnlyList<ProjectTask> Tasks => _tasks.AsReadOnly();
public IReadOnlyList<PhaseSection> PhaseSections => _phaseSections.AsReadOnly();
// Phase-specific properties
public PhaseStatus Status { get; private set; } = PhaseStatus.Planning;
public DateTime? StartDate { get; private set; }
public DateTime? EndDate { get; private set; }
public int OrderIndex { get; private set; }
#region Task Management
public ProjectTask AddTask(string name, string? description = null)
{
var task = new ProjectTask(name, Id, description);
_tasks.Add(task);
AddDomainEvent(new TaskAddedEvent(task.Id, Id, name));
return task;
}
public void RemoveTask(Guid taskId)
{
var task = _tasks.FirstOrDefault(t => t.Id == taskId);
if (task == null)
throw new InvalidOperationException("تسک مورد نظر یافت نشد");
if (task.Sections.Any())
throw new InvalidOperationException("نمی‌توان تسکی را که شامل بخش است حذف کرد");
_tasks.Remove(task);
AddDomainEvent(new TaskRemovedEvent(taskId, Id));
}
#endregion
#region PhaseSection Management
public void AddPhaseSection(long userId, Guid skillId)
{
var existingSection = _phaseSections.FirstOrDefault(s => s.UserId == userId && s.SkillId == skillId);
if (existingSection == null)
{
var section = new PhaseSection(Id, userId, skillId);
_phaseSections.Add(section);
}
}
public void RemovePhaseSection(long userId, Guid skillId)
{
var section = _phaseSections.FirstOrDefault(s => s.UserId == userId && s.SkillId == skillId);
if (section != null)
{
_phaseSections.Remove(section);
}
}
public void ClearPhaseSections()
{
_phaseSections.Clear();
}
#endregion
#region Status Management
public void UpdateStatus(PhaseStatus status)
{
Status = status;
AddDomainEvent(new PhaseStatusUpdatedEvent(Id, status));
}
public void SetDates(DateTime? startDate = null, DateTime? endDate = null)
{
if (startDate.HasValue && endDate.HasValue && startDate > endDate)
throw new ArgumentException("تاریخ شروع نمی‌تواند بعد از تاریخ پایان باشد");
StartDate = startDate;
EndDate = endDate;
}
public void SetOrderIndex(int orderIndex)
{
if (orderIndex < 0)
throw new ArgumentException("ترتیب نمی‌تواند منفی باشد", nameof(orderIndex));
OrderIndex = orderIndex;
}
#endregion
#region Assignment Management
public void AssignToUser(long userId, bool cascadeToTasks = true, bool forceOverride = false, bool markAsOverride = true)
{
if (markAsOverride)
{
HasAssignmentOverride = true;
}
if (cascadeToTasks)
{
foreach (var task in _tasks)
{
if (task.HasAssignmentOverride && !forceOverride)
continue;
task.AssignToUser(userId, cascadeToSections: true, forceOverride, markAsOverride: false);
}
}
AddDomainEvent(new PhaseAssignedEvent(Id, userId));
}
public void Unassign(bool cascadeToTasks = true, bool forceOverride = false, bool markAsOverride = true)
{
if (markAsOverride)
{
HasAssignmentOverride = true;
}
if (cascadeToTasks)
{
foreach (var task in _tasks)
{
if (task.HasAssignmentOverride && !forceOverride)
continue;
task.Unassign(cascadeToSections: true, forceOverride, markAsOverride: false);
}
}
AddDomainEvent(new PhaseUnassignedEvent(Id));
}
#endregion
#region Time Calculation
public override TimeSpan GetTotalTimeSpent()
{
return TimeSpan.FromTicks(_tasks.Sum(t => t.GetTotalTimeSpent().Ticks));
}
public override TimeSpan GetTotalEstimatedTime()
{
return TimeSpan.FromTicks(_tasks.Sum(t => t.GetTotalEstimatedTime().Ticks));
}
#endregion
#region Query Helpers
public IEnumerable<ProjectTask> GetTasksWithTimeOverride()
{
return _tasks.Where(t => t.HasTimeOverride);
}
public IEnumerable<ProjectTask> GetTasksWithAssignmentOverride()
{
return _tasks.Where(t => t.HasAssignmentOverride);
}
public IEnumerable<TaskSection> GetAllSections()
{
return _tasks.SelectMany(t => t.Sections);
}
#endregion
}

View File

@@ -0,0 +1,34 @@
using GozareshgirProgramManager.Domain._Common;
namespace GozareshgirProgramManager.Domain.ProjectAgg.Entities;
/// <summary>
/// ProjectSection: shortcut container for UserId + SkillId at Project level
/// </summary>
public class ProjectSection : EntityBase<Guid>
{
private ProjectSection() { }
public ProjectSection(Guid projectId, long userId, Guid skillId)
{
ProjectId = projectId;
UserId = userId;
SkillId = skillId;
}
public Guid ProjectId { get; private set; }
public long UserId { get; private set; }
public Guid SkillId { get; private set; }
public Project Project { get; private set; } = null!;
public void UpdateUser(long userId)
{
UserId = userId;
}
public void UpdateSkill(Guid skillId)
{
SkillId = skillId;
}
}

View File

@@ -0,0 +1,249 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using GozareshgirProgramManager.Domain.ProjectAgg.Events;
namespace GozareshgirProgramManager.Domain.ProjectAgg.Entities;
/// <summary>
/// تسک - پایین‌ترین سطح در سلسله مراتب که شامل بخش‌ها می‌شود
/// </summary>
public class ProjectTask : ProjectHierarchyNode
{
private readonly List<TaskSection> _sections;
private ProjectTask()
{
_sections = new List<TaskSection>();
}
public ProjectTask(string name, Guid phaseId, string? description = null) : base(name, description)
{
PhaseId = phaseId;
_sections = new List<TaskSection>();
Priority = TaskPriority.Medium;
AddDomainEvent(new TaskCreatedEvent(Id, phaseId, name));
}
public Guid PhaseId { get; private set; }
public ProjectPhase Phase { get; private set; } = null!;
public IReadOnlyList<TaskSection> Sections => _sections.AsReadOnly();
// Task-specific properties
public Enums.TaskStatus Status { get; private set; } = Enums.TaskStatus.NotStarted;
public TaskPriority Priority { get; private set; }
public DateTime? StartDate { get; private set; }
public DateTime? EndDate { get; private set; }
public DateTime? DueDate { get; private set; }
public int OrderIndex { get; private set; }
public TimeSpan? AllocatedTime { get; protected set; }
public bool HasTimeOverride { get; protected set; }
#region Section Management
public void AddSection(TaskSection section, bool cascadeToChildren = false)
{
var existingSection = _sections.FirstOrDefault(s => s.SkillId == section.SkillId);
if (existingSection != null)
{
// اگر userId متفاوت است، ویرایش شود
if (existingSection.CurrentAssignedUserId != section.CurrentAssignedUserId)
{
if (existingSection.CurrentAssignedUserId > 0)
{
existingSection.TransferToUser(existingSection.CurrentAssignedUserId, section.CurrentAssignedUserId);
}
else
{
existingSection.AssignToUser(section.CurrentAssignedUserId);
}
}
}
else
{
_sections.Add(section);
AddDomainEvent(new TaskSectionAddedEvent(Id, section.Id, section.SkillId));
}
}
public void AddSection(Guid skillId, long assignedUserId)
{
var existingSection = _sections.FirstOrDefault(s => s.SkillId == skillId);
if (existingSection != null)
{
if (existingSection.CurrentAssignedUserId != assignedUserId)
{
if (existingSection.CurrentAssignedUserId > 0)
{
existingSection.TransferToUser(existingSection.CurrentAssignedUserId, assignedUserId);
}
else
{
existingSection.AssignToUser(assignedUserId);
}
}
return;
}
var section = new TaskSection(Id, skillId, assignedUserId);
_sections.Add(section);
AddDomainEvent(new TaskSectionAddedEvent(Id, section.Id, skillId));
}
public void RemoveSection(Guid sectionId)
{
var section = _sections.FirstOrDefault(s => s.Id == sectionId);
if (section == null)
throw new InvalidOperationException("بخش مورد نظر یافت نشد");
_sections.Remove(section);
AddDomainEvent(new TaskSectionRemovedEvent(Id, sectionId));
}
public void RemoveSectionBySkill(Guid skillId)
{
var section = _sections.FirstOrDefault(s => s.SkillId == skillId);
if (section != null)
{
_sections.Remove(section);
AddDomainEvent(new TaskSectionRemovedEvent(Id, section.Id));
}
}
#endregion
#region Status Management
public void UpdateStatus(Enums.TaskStatus status)
{
Status = status;
AddDomainEvent(new TaskStatusUpdatedEvent(Id, status));
}
public void SetPriority(TaskPriority priority)
{
Priority = priority;
AddDomainEvent(new TaskPriorityUpdatedEvent(Id, priority));
}
public void SetDates(DateTime? startDate = null, DateTime? endDate = null, DateTime? dueDate = null)
{
if (startDate.HasValue && endDate.HasValue && startDate > endDate)
throw new ArgumentException("تاریخ شروع نمی‌تواند بعد از تاریخ پایان باشد");
if (dueDate.HasValue && endDate.HasValue && dueDate < endDate)
throw new ArgumentException("تاریخ مهلت نمی‌تواند قبل از تاریخ پایان باشد");
StartDate = startDate;
EndDate = endDate;
DueDate = dueDate;
}
public void SetOrderIndex(int orderIndex)
{
if (orderIndex < 0)
throw new ArgumentException("ترتیب نمی‌تواند منفی باشد", nameof(orderIndex));
OrderIndex = orderIndex;
}
#endregion
#region Assignment Management
public void AssignToUser(long userId, bool cascadeToSections = true, bool forceOverride = false, bool markAsOverride = true)
{
if (markAsOverride)
{
HasAssignmentOverride = true;
}
if (cascadeToSections)
{
foreach (var section in _sections)
{
section.AssignToUser(userId);
}
}
AddDomainEvent(new TaskAssignedEvent(Id, userId));
}
public void Unassign(bool cascadeToSections = true, bool forceOverride = false, bool markAsOverride = true)
{
if (markAsOverride)
{
HasAssignmentOverride = true;
}
if (cascadeToSections)
{
foreach (var section in _sections)
{
section.Unassign();
}
}
AddDomainEvent(new TaskUnassignedEvent(Id));
}
#endregion
#region Time Calculation
public override TimeSpan GetTotalTimeSpent()
{
return TimeSpan.FromTicks(_sections.Sum(s => s.GetTotalTimeSpent().Ticks));
}
public override TimeSpan GetTotalEstimatedTime()
{
return TimeSpan.FromTicks(_sections.Sum(s => s.EstimatedHours.Ticks));
}
#endregion
#region Query Helpers
public IEnumerable<TaskSection> GetSectionsBySkill(Guid skillId)
{
return _sections.Where(s => s.SkillId == skillId);
}
public TaskSection? GetSectionBySkill(Guid skillId)
{
return _sections.FirstOrDefault(s => s.SkillId == skillId);
}
public bool HasSection(Guid skillId)
{
return _sections.Any(s => s.SkillId == skillId);
}
public IEnumerable<TaskSection> GetAssignedSections(long userId)
{
return _sections.Where(s => s.CurrentAssignedUserId == userId);
}
#endregion
#region Time Management
public void SetAllocatedTime(TimeSpan time, bool markAsOverride = true)
{
if (time < TimeSpan.Zero)
throw new ArgumentException("زمان تخصیص‌یافته نمی‌تواند منفی باشد", nameof(time));
AllocatedTime = time;
if (markAsOverride)
{
HasTimeOverride = true;
}
}
public void ClearTimeOverride()
{
HasTimeOverride = false;
AllocatedTime = null;
}
#endregion
}

View File

@@ -0,0 +1,223 @@
using System.Linq;
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain._Common.Exceptions;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using GozareshgirProgramManager.Domain.ProjectAgg.Events;
using GozareshgirProgramManager.Domain.ProjectAgg.Models;
using GozareshgirProgramManager.Domain.SkillAgg.Entities;
namespace GozareshgirProgramManager.Domain.ProjectAgg.Entities;
/// <summary>
/// بخش تسک - برای ذخیره کار واقعی که کاربر روی یک مهارت خاص انجام می‌دهد
/// </summary>
public class TaskSection : EntityBase<Guid>
{
private readonly List<TaskSectionActivity> _activities;
private readonly List<TaskSectionAdditionalTime> _additionalTimes;
private TaskSection()
{
_activities = new List<TaskSectionActivity>();
_additionalTimes = new List<TaskSectionAdditionalTime>();
}
public TaskSection(Guid taskId, Guid skillId, long currentAssignedUserId)
{
TaskId = taskId;
SkillId = skillId;
CurrentAssignedUserId = currentAssignedUserId;
OriginalAssignedUserId = currentAssignedUserId;
InitialEstimatedHours = TimeSpan.Zero;
_activities = new List<TaskSectionActivity>();
_additionalTimes = new List<TaskSectionAdditionalTime>();
Status = TaskSectionStatus.ReadyToStart;
AddDomainEvent(new TaskSectionAddedEvent(taskId, Id, skillId));
}
public Guid TaskId { get; private set; }
public Guid SkillId { get; private set; }
public TimeSpan InitialEstimatedHours { get; private set; }
public string? InitialDescription { get; set; }
public TaskSectionStatus Status { get; private set; }
// شخصی که برای اولین بار این بخش به او اختصاص داده شده (مالک اصلی)
public long OriginalAssignedUserId { get; private set; }
// شخصی که در حال حاضر این بخش به او اختصاص داده شده
public long CurrentAssignedUserId { get; private set; }
// Navigation to ProjectTask (must be Task level)
public ProjectTask Task { get; private set; } = null!;
public Skill? Skill { get; set; }
public IReadOnlyList<TaskSectionActivity> Activities => _activities.AsReadOnly();
public IReadOnlyList<TaskSectionAdditionalTime> AdditionalTimes => _additionalTimes.AsReadOnly();
// محاسبه تایم نهایی (اولیه + تمام اضافه‌ها)
public TimeSpan FinalEstimatedHours =>
TimeSpan.FromTicks(InitialEstimatedHours.Ticks + _additionalTimes.Sum(at => at.Hours.Ticks));
// برای backward compatibility
public TimeSpan EstimatedHours => FinalEstimatedHours;
public void AddAdditionalTime(TimeSpan additionalHours, string? reason = null, long? addedByUserId = null)
{
if (additionalHours <= TimeSpan.Zero)
throw new BadRequestException("تایم اضافی باید بزرگتر از صفر باشد", nameof(additionalHours));
var additionalTime = new TaskSectionAdditionalTime(additionalHours, reason, addedByUserId);
_additionalTimes.Add(additionalTime);
}
public void UpdateInitialEstimatedHours(TimeSpan newInitialEstimate, string initialDescription)
{
if (newInitialEstimate <= TimeSpan.Zero)
throw new BadRequestException("تایم تخمینی اولیه باید بزرگتر از صفر باشد", nameof(newInitialEstimate));
InitialEstimatedHours = newInitialEstimate;
InitialDescription = initialDescription;
}
public void RemoveAdditionalTime(Guid additionalTimeId)
{
var additionalTime = _additionalTimes.FirstOrDefault(at => at.Id == additionalTimeId);
if (additionalTime == null)
throw new BadRequestException("تایم اضافی مورد نظر یافت نشد");
_additionalTimes.Remove(additionalTime);
}
public TimeSpan GetTotalAdditionalTime()
{
return TimeSpan.FromTicks(_additionalTimes.Sum(at => at.Hours.Ticks));
}
public void UnassignUser()
{
if (_activities.Any(a => a.IsActive))
{
throw new BadRequestException("نمی‌توان کاربری را که در حال کار است حذف کرد");
}
CurrentAssignedUserId = 0;
UpdateStatus(TaskSectionStatus.NotAssigned);
}
public void StartWork(long userId, string? notes = null)
{
if (CurrentAssignedUserId != userId)
{
throw new BadRequestException("کاربر مجاز به شروع این بخش نیست");
}
// if (Status == TaskSectionStatus.Completed)
// {
// throw new BadRequestException("این بخش قبلاً تکمیل شده است");
// }
if (_activities.Any(a => a.IsActive))
{
throw new BadRequestException("یک فعالیت در حال انجام وجود دارد");
}
var activity = new TaskSectionActivity(Id, userId, notes);
_activities.Add(activity);
UpdateStatus(TaskSectionStatus.InProgress);
}
public void StopWork(long userId, TaskSectionStatus taskSectionStatus, string? endNotes = null)
{
var activeActivity = _activities.FirstOrDefault(a => a.IsActive);
if (activeActivity == null)
{
throw new BadRequestException("هیچ فعالیت فعالی یافت نشد");
}
if (activeActivity.UserId != userId)
{
throw new BadRequestException("کاربر مجاز به توقف این فعالیت نیست");
}
UpdateStatus(taskSectionStatus);
activeActivity.StopWork(endNotes);
}
public void CompleteSection()
{
if (_activities.Any(a => a.IsActive))
{
throw new BadRequestException("نمی‌توان بخشی را که فعالیت فعال دارد تکمیل کرد");
}
UpdateStatus(TaskSectionStatus.Completed);
}
public void UpdateStatus(TaskSectionStatus status)
{
var oldStatus = Status;
Status = status;
AddDomainEvent(new TaskSectionStatusChangedEvent(Id, oldStatus, status));
}
public TimeSpan GetTotalTimeSpent()
{
return TimeSpan.FromTicks(_activities.Sum(a => a.GetTimeSpent().Ticks));
}
public bool IsCompleted()
{
return Status == TaskSectionStatus.Completed;
}
public bool IsInProgress()
{
return _activities.Any(a => a.IsActive);
}
public void AssignToUser(long userId)
{
if (OriginalAssignedUserId == 0)
{
OriginalAssignedUserId = userId;
}
CurrentAssignedUserId = userId;
if (Status == TaskSectionStatus.NotAssigned)
{
UpdateStatus(TaskSectionStatus.ReadyToStart);
}
AddDomainEvent(new TaskSectionAssignedEvent(Id, userId));
}
public void TransferToUser(long fromUserId, long toUserId)
{
if (CurrentAssignedUserId != fromUserId)
{
throw new BadRequestException("کاربر فعلی با کاربر منبع مطابقت ندارد");
}
if (_activities.Any(a => a.IsActive))
{
throw new BadRequestException("نمی‌توان بخشی را که در حال انجام است انتقال داد");
}
OriginalAssignedUserId = toUserId;
CurrentAssignedUserId = toUserId;
AddDomainEvent(new TaskSectionTransferredEvent(Id, fromUserId, toUserId));
}
public void Unassign()
{
UnassignUser();
}
public void ClearAdditionalTimes()
{
_additionalTimes.Clear();
}
}

View File

@@ -0,0 +1,57 @@
using System.Diagnostics.CodeAnalysis;
using GozareshgirProgramManager.Domain._Common;
namespace GozareshgirProgramManager.Domain.ProjectAgg.Entities;
/// <summary>
/// فعالیت کاری روی یک بخش
/// </summary>
public class TaskSectionActivity : EntityBase<Guid>
{
private TaskSectionActivity() { }
public TaskSectionActivity(Guid sectionId, long userId, string? notes = null)
{
SectionId = sectionId;
UserId = userId;
StartDate = DateTime.Now;
Notes = notes;
IsActive = true;
}
public Guid SectionId { get; private set; }
public long UserId { get; private set; }
public DateTime StartDate { get; private set; }
public DateTime? EndDate { get; private set; }
public string? Notes { get; private set; }
public string? EndNotes { get; private set; }
public bool IsActive { get; private set; }
// Navigation property
public TaskSection Section { get; private set; } = null!;
public void StopWork(string? endNotes = null)
{
if (!IsActive)
throw new InvalidOperationException("این فعالیت قبلاً متوقف شده است.");
EndDate = DateTime.Now;
EndNotes = endNotes;
IsActive = false;
}
public TimeSpan GetTimeSpent()
{
if (IsActive)
{
return DateTime.Now - StartDate;
}
return (EndDate ?? DateTime.Now) - StartDate;
}
public void UpdateNotes(string? notes)
{
Notes = notes;
}
}

View File

@@ -0,0 +1,29 @@
using GozareshgirProgramManager.Domain._Common;
namespace GozareshgirProgramManager.Domain.ProjectAgg.Entities;
/// <summary>
/// زمان اضافی اضافه شده بعد از تخمین اولیه
/// </summary>
public class TaskSectionAdditionalTime : EntityBase<Guid>
{
private TaskSectionAdditionalTime() { }
public TaskSectionAdditionalTime(TimeSpan hours, string? reason = null, long? addedByUserId = null)
{
Hours = hours;
Reason = reason;
AddedByUserId = addedByUserId;
AddedAt = DateTime.UtcNow;
}
public TimeSpan Hours { get; private set; }
public string? Reason { get; private set; }
public long? AddedByUserId { get; private set; }
public DateTime AddedAt { get; private set; }
public void UpdateReason(string? reason)
{
Reason = reason;
}
}

View File

@@ -0,0 +1,18 @@
namespace GozareshgirProgramManager.Domain.ProjectAgg.Entities;
public class UserTimeReport
{
public long UserId { get; set; }
public string UserName { get; set; } = string.Empty;
public TimeSpan TotalTimeSpent { get; set; }
public TimeSpan EstimatedTime { get; set; }
public int ActivityCount { get; set; }
public DateTime LastActivity { get; set; }
public DateTime? FirstActivity { get; set; }
public double EfficiencyRate => EstimatedTime.TotalHours > 0 ?
(EstimatedTime.TotalHours / TotalTimeSpent.TotalHours) * 100 : 0;
public bool IsOvertime => TotalTimeSpent > EstimatedTime;
public TimeSpan TimeDifference => TotalTimeSpent - EstimatedTime;
}

View File

@@ -0,0 +1,37 @@
namespace GozareshgirProgramManager.Domain.ProjectAgg.Enums;
/// <summary>
/// وضعیت فاز پروژه
/// </summary>
public enum PhaseStatus
{
/// <summary>
/// در حال برنامه‌ریزی
/// </summary>
Planning = 0,
/// <summary>
/// آماده شروع
/// </summary>
Ready = 1,
/// <summary>
/// در حال اجرا
/// </summary>
InProgress = 2,
/// <summary>
/// متوقف شده
/// </summary>
OnHold = 3,
/// <summary>
/// تکمیل شده
/// </summary>
Completed = 4,
/// <summary>
/// لغو شده
/// </summary>
Cancelled = 5
}

View File

@@ -0,0 +1,23 @@
namespace GozareshgirProgramManager.Domain.ProjectAgg.Enums;
/// <summary>
/// سطوح مختلف در سلسله مراتب پروژه
/// </summary>
public enum ProjectHierarchyLevel
{
/// <summary>
/// سطح پروژه (بالاترین سطح)
/// </summary>
Project = 0,
/// <summary>
/// سطح فاز پروژه
/// </summary>
Phase = 1,
/// <summary>
/// سطح تسک
/// </summary>
Task = 2,
}

View File

@@ -0,0 +1,37 @@
namespace GozareshgirProgramManager.Domain.ProjectAgg.Enums;
/// <summary>
/// وضعیت پروژه
/// </summary>
public enum ProjectStatus
{
/// <summary>
/// در حال برنامه‌ریزی
/// </summary>
Planning = 0,
/// <summary>
/// آماده شروع
/// </summary>
Ready = 1,
/// <summary>
/// در حال اجرا
/// </summary>
InProgress = 2,
/// <summary>
/// متوقف شده
/// </summary>
OnHold = 3,
/// <summary>
/// تکمیل شده
/// </summary>
Completed = 4,
/// <summary>
/// لغو شده
/// </summary>
Cancelled = 5
}

View File

@@ -0,0 +1,27 @@
namespace GozareshgirProgramManager.Domain.ProjectAgg.Enums;
/// <summary>
/// اولویت تسک
/// </summary>
public enum TaskPriority
{
/// <summary>
/// پایین
/// </summary>
Low = 0,
/// <summary>
/// متوسط
/// </summary>
Medium = 1,
/// <summary>
/// بالا
/// </summary>
High = 2,
/// <summary>
/// بحرانی
/// </summary>
Critical = 3
}

View File

@@ -0,0 +1,10 @@
namespace GozareshgirProgramManager.Domain.ProjectAgg.Enums;
public enum TaskSectionStatus
{
NotAssigned = 0, // تخصیص داده نشده
ReadyToStart = 1, // آماده شروع
InProgress = 2, // درحال انجام
Incomplete = 3, // ناتمام شده
Completed = 4 // تکمیل شده
}

View File

@@ -0,0 +1,9 @@
namespace GozareshgirProgramManager.Domain.ProjectAgg.Enums;
public enum TaskStatus
{
NotStarted = 1,
InProgress = 2,
Completed = 3,
OnHold = 4
}

View File

@@ -0,0 +1,177 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain.ProjectAgg.Enums;
using TaskStatus = GozareshgirProgramManager.Domain.ProjectAgg.Enums.TaskStatus;
namespace GozareshgirProgramManager.Domain.ProjectAgg.Events;
// Project Events
public record ProjectCreatedEvent(Guid ProjectId, string Name) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record ProjectStatusUpdatedEvent(Guid ProjectId, ProjectStatus Status) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record ProjectAssignedEvent(Guid ProjectId, long UserId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record ProjectUnassignedEvent(Guid ProjectId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
// Phase Events
public record PhaseCreatedEvent(Guid PhaseId, Guid ProjectId, string Name) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record PhaseAddedEvent(Guid PhaseId, Guid ProjectId, string Name) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record PhaseRemovedEvent(Guid PhaseId, Guid ProjectId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record PhaseStatusUpdatedEvent(Guid PhaseId, PhaseStatus Status) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record PhaseAssignedEvent(Guid PhaseId, long UserId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record PhaseUnassignedEvent(Guid PhaseId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
// Task Events
public record TaskCreatedEvent(Guid TaskId, Guid PhaseId, string Name) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record TaskAddedEvent(Guid TaskId, Guid PhaseId, string Name) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record TaskRemovedEvent(Guid TaskId, Guid PhaseId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record TaskStatusUpdatedEvent(Guid TaskId, TaskStatus Status) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record TaskPriorityUpdatedEvent(Guid TaskId, TaskPriority Priority) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record TaskAssignedEvent(Guid TaskId, long UserId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record TaskUnassignedEvent(Guid TaskId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record TaskSectionAddedEvent(Guid TaskId, Guid SectionId, Guid SkillId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record TaskSectionRemovedEvent(Guid TaskId, Guid SectionId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
// TaskSection Events
public record TaskSectionStatusChangedEvent(Guid SectionId, TaskSectionStatus OldStatus, TaskSectionStatus NewStatus) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record TaskSectionAssignedEvent(Guid SectionId, long UserId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record TaskSectionTransferredEvent(Guid SectionId, long FromUserId, long ToUserId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
// Section Events (Legacy - keeping for backward compatibility)
public record ProjectPhaseAddedEvent(Guid ProjectId, Guid PhaseId, string PhaseName) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record ProjectTaskStatusChangedEvent(Guid SectionId, TaskStatus OldStatus, TaskStatus NewStatus) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record ProjectSectionAddedEvent(Guid SectionId, Guid TaskId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record ProjectSectionAssignedEvent(Guid SectionId, long UserId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record ProjectSectionTransferredEvent(Guid SectionId, long FromUserId, long ToUserId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record WorkStartedEvent(Guid SectionId, long UserId, DateTime StartTime, string? Notes) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record WorkStoppedEvent(Guid SectionId, long UserId, DateTime StartTime, DateTime EndTime, TimeSpan Duration, string? Notes) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record ProjectSectionCompletedEvent(Guid ProjectId, long UserId, TimeSpan TotalTimeSpent, string? Notes) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record AdditionalTimeAddedEvent(Guid ProjectId, Guid AdditionalTimeId, TimeSpan Hours, string? Reason, long? AddedByUserId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record AdditionalTimeRemovedEvent(Guid ProjectId, Guid AdditionalTimeId, TimeSpan RemovedHours) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record InitialEstimatedTimeUpdatedEvent(Guid ProjectId, TimeSpan OldEstimate, TimeSpan NewEstimate) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}

View File

@@ -0,0 +1,12 @@
namespace GozareshgirProgramManager.Domain.ProjectAgg.Models;
/// <summary>
/// گزارش زمان کاربر برای یک بخش پروژه
/// </summary>
public class UserTimeReport
{
public long UserId { get; set; }
public TimeSpan TotalTimeSpent { get; set; }
public int ActivityCount { get; set; }
public DateTime LastActivity { get; set; }
}

View File

@@ -0,0 +1,6 @@
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
using GozareshgirProgramManager.Domain._Common;
namespace GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
public interface IPhaseSectionRepository : IRepository<Guid, PhaseSection>
{
}

View File

@@ -0,0 +1,33 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
namespace GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
/// <summary>
/// Repository interface for ProjectPhase entity
/// </summary>
public interface IProjectPhaseRepository : IRepository<Guid, ProjectPhase>
{
/// <summary>
/// Get phase with all its tasks
/// </summary>
Task<ProjectPhase?> GetWithTasksAsync(Guid phaseId);
/// <summary>
/// Get phases by project ID
/// </summary>
Task<List<ProjectPhase>> GetByProjectIdAsync(Guid projectId);
/// <summary>
/// Get phases with assignment overrides
/// </summary>
Task<List<ProjectPhase>> GetPhasesWithAssignmentOverridesAsync();
/// <summary>
/// Get phases by status
/// </summary>
Task<List<ProjectPhase>> GetByStatusAsync(ProjectAgg.Enums.PhaseStatus status);
void Remove(ProjectPhase phase);
}

View File

@@ -0,0 +1,38 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
namespace GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
/// <summary>
/// Repository interface for Project aggregate root
/// </summary>
public interface IProjectRepository : IRepository<Guid, Project>
{
/// <summary>
/// Get project with all its phases and tasks
/// </summary>
Task<Project?> GetWithFullHierarchyAsync(Guid projectId);
/// <summary>
/// Get project with phases only
/// </summary>
Task<Project?> GetWithPhasesAsync(Guid projectId);
/// <summary>
/// Get projects by status
/// </summary>
Task<List<Project>> GetByStatusAsync(ProjectAgg.Enums.ProjectStatus status);
/// <summary>
/// Get projects with assignment overrides
/// </summary>
Task<List<Project>> GetProjectsWithAssignmentOverridesAsync();
/// <summary>
/// Search projects by name
/// </summary>
Task<List<Project>> SearchByNameAsync(string searchTerm);
void Remove(Project project);
}

View File

@@ -0,0 +1,9 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
namespace GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
public interface IProjectSectionRepository : IRepository<Guid, ProjectSection>
{
}

View File

@@ -0,0 +1,47 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
namespace GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
/// <summary>
/// Repository interface for ProjectTask entity
/// </summary>
public interface IProjectTaskRepository : IRepository<Guid, ProjectTask>
{
/// <summary>
/// Get task with all its sections
/// </summary>
Task<ProjectTask?> GetWithSectionsAsync(Guid taskId);
/// <summary>
/// Get tasks by phase ID
/// </summary>
Task<List<ProjectTask>> GetByPhaseIdAsync(Guid phaseId);
/// <summary>
/// Get tasks with time overrides
/// </summary>
Task<List<ProjectTask>> GetTasksWithTimeOverridesAsync();
/// <summary>
/// Get tasks with assignment overrides
/// </summary>
Task<List<ProjectTask>> GetTasksWithAssignmentOverridesAsync();
/// <summary>
/// Get tasks by status
/// </summary>
Task<List<ProjectTask>> GetByStatusAsync(ProjectAgg.Enums.TaskStatus status);
/// <summary>
/// Get tasks by priority
/// </summary>
Task<List<ProjectTask>> GetByPriorityAsync(ProjectAgg.Enums.TaskPriority priority);
/// <summary>
/// Get tasks assigned to user
/// </summary>
Task<List<ProjectTask>> GetAssignedToUserAsync(long userId);
void Remove(ProjectTask task);
}

View File

@@ -0,0 +1,16 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
namespace GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
public interface ITaskSectionActivityRepository:IRepository<Guid,TaskSectionActivity>
{
Task<TaskSectionActivity?> GetByIdAsync(Guid id);
Task<List<TaskSectionActivity>> GetBySectionIdAsync(Guid sectionId);
Task<List<TaskSectionActivity>> GetByUserIdAsync(long userId);
Task<List<TaskSectionActivity>> GetActiveByUserAsync(long userId);
Task<List<TaskSectionActivity>> GetByDateRangeAsync(DateTime from, DateTime to);
Task<List<TaskSectionActivity>> GetByDateRangeUserIdAsync(DateTime from, DateTime to,long userId);
Task<TimeSpan> GetTotalTimeSpentByUserInRangeAsync(long userId, DateTime from, DateTime to);
Task<List<(TimeSpan TotalTime,long UserId)>> GetTotalTimeSpentPerUserInRangeAsync(DateTime from, DateTime to);
}

View File

@@ -0,0 +1,12 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
namespace GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
public interface ITaskSectionRepository: IRepository<Guid,TaskSection>
{
Task<TaskSection?> GetByIdWithActivitiesAsync(Guid id, CancellationToken cancellationToken = default);
Task<TaskSection?> GetByIdWithFullDataAsync(Guid id, CancellationToken cancellationToken = default);
}

View File

@@ -0,0 +1,75 @@
using GozareshgirProgramManager.Domain._Common;
namespace GozareshgirProgramManager.Domain.ProjectAgg.ValueObjects;
public class TimeRange : ValueObject
{
public DateTime Start { get; private set; }
public DateTime? End { get; private set; }
public TimeRange(DateTime start)
{
Start = start;
}
public TimeRange(DateTime start, DateTime end)
{
if (start >= end)
throw new ArgumentException("Start time must be before end time");
Start = start;
End = end;
}
public TimeSpan Duration => End.HasValue ? End.Value - Start : DateTime.UtcNow - Start;
public bool IsActive => !End.HasValue;
public void Complete(DateTime endTime)
{
if (End.HasValue)
throw new InvalidOperationException("Time range is already completed");
if (endTime <= Start)
throw new ArgumentException("End time must be after start time");
End = endTime;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Start;
yield return End ?? DateTime.MinValue;
}
}
public class WorkSession : ValueObject
{
public long UserId { get; private set; }
public TimeRange TimeRange { get; private set; }
public string? Notes { get; private set; }
public string? EndNotes { get; private set; }
public WorkSession(long userId, DateTime startTime, string? notes = null)
{
UserId = userId;
TimeRange = new TimeRange(startTime);
Notes = notes;
}
public void Complete(DateTime endTime, string? endNotes = null)
{
TimeRange.Complete(endTime);
EndNotes = endNotes;
}
public TimeSpan Duration => TimeRange.Duration;
public bool IsActive => TimeRange.IsActive;
protected override IEnumerable<object> GetEqualityComponents()
{
yield return UserId;
yield return TimeRange;
yield return Notes ?? string.Empty;
yield return EndNotes ?? string.Empty;
}
}

View File

@@ -0,0 +1,31 @@
namespace GozareshgirProgramManager.Domain.ProjectAgg.ValueObjects;
/// <summary>
/// Value Object برای ذخیره تخصیص کاربر و مهارت در سطوح بالاتر (Project/Phase)
/// این اطلاعات می‌تواند به صورت shortcut در Task و Section استفاده شود
/// </summary>
public class UserSkillAssignment
{
public UserSkillAssignment(long userId, Guid skillId)
{
UserId = userId;
SkillId = skillId;
}
public long UserId { get; private set; }
public Guid SkillId { get; private set; }
public override bool Equals(object? obj)
{
if (obj is not UserSkillAssignment other)
return false;
return UserId == other.UserId && SkillId == other.SkillId;
}
public override int GetHashCode()
{
return HashCode.Combine(UserId, SkillId);
}
}

View File

@@ -0,0 +1,48 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.PermissionAgg.Entities;
using System.Security.Principal;
using System.Xml.Linq;
using GozareshgirProgramManager.Domain.UserAgg.Entities;
namespace GozareshgirProgramManager.Domain.RoleAgg.Entities;
public class Role : EntityBase<long>
{
/// <summary>
/// نام نقش
/// </summary>
public string RoleName { get; private set; }
/// <summary>
/// لیست پرمیشن کد ها
/// </summary>
public List<Permission> Permissions { get; private set; }
/// <summary>
/// ای دی نقش در گزارشگیر
/// </summary>
public long? GozareshgirRoleId { get; private set; }
protected Role()
{
}
public Role(string roleName,long? gozareshgirRolId, List<Permission> permissions)
{
RoleName = roleName;
Permissions = permissions;
GozareshgirRoleId = gozareshgirRolId;
}
public void Edit(string roleName, List<Permission> permissions)
{
RoleName = roleName;
Permissions = permissions;
}
}

View File

@@ -0,0 +1,12 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.RoleAgg.Entities;
using GozareshgirProgramManager.Domain.UserAgg.Entities;
namespace GozareshgirProgramManager.Domain.RoleAgg.Repositories;
public interface IRoleRepository : IRepository<long, Role>
{
Task<Role?> GetByGozareshgirRoleIdAsync(long? gozareshgirRolId);
}

View File

@@ -0,0 +1,19 @@
using GozareshgirProgramManager.Domain.UserAgg.Entities;
namespace GozareshgirProgramManager.Domain.RoleUserAgg;
public class RoleUser
{
public RoleUser(long roleId)
{
RoleId = roleId;
}
public long Id { get; private set; }
public long RoleId { get; private set; }
public User User { get; set; }
}

View File

@@ -0,0 +1,43 @@
using GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Enums;
namespace GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.DTOs;
public record UserSalarySettingDto
{
/// <summary>
/// آی دی کاربر
/// </summary>
public long UserId { get; set; }
/// <summary>
/// کار در تعطیلات رسمی
/// </summary>
public bool HolidayWorking { get; set; }
/// <summary>
/// حقوق ماهانه
/// </summary>
public double MonthlySalary { get; set; }
public string FullName { get; set; }
public List<WorkingHoursListDto> WorkingHoursListDto { get; set; }
}
public record WorkingHoursListDto
{
/// <summary>
/// مدت زمان شیفت
/// </summary>
public TimeSpan ShiftDuration { get; set; }
/// <summary>
/// عدد روز از ماه
/// </summary>
public PersianDayOfWeek PersianDayOfWeek { get; set; }
}

View File

@@ -0,0 +1,94 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.UserAgg.Entities;
namespace GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Entities;
/// <summary>
/// تنظیمات پرداخت حقوق
/// </summary>
public class SalaryPaymentSetting : EntityBase<long>
{
/// <summary>
/// ایجاد تنظیمات حقوق
/// برای اولین بار
/// </summary>
/// <param name="holidayWorking"></param>
/// <param name="userId"></param>
/// <param name="monthlySalary"></param>
/// <param name="workingHoursList"></param>
public SalaryPaymentSetting(bool holidayWorking, long userId, double monthlySalary, List<WorkingHours> workingHoursList)
{
HolidayWorking = holidayWorking;
UserId = userId;
MonthlySalary = monthlySalary;
WorkingHoursList = workingHoursList;
StartSettingDate = new DateTime(2025, 3, 21);
}
/// <summary>
/// افزودن تنظیمات جدید
/// </summary>
/// <param name="holidayWorking"></param>
/// <param name="userId"></param>
/// <param name="monthlySalary"></param>
/// <param name="workingHoursList"></param>
/// <param name="startSettingDate"></param>
public SalaryPaymentSetting(bool holidayWorking, long userId, List<WorkingHours> workingHoursList, DateTime startSettingDate)
{
HolidayWorking = holidayWorking;
UserId = userId;
WorkingHoursList = workingHoursList;
StartSettingDate = startSettingDate;
}
protected SalaryPaymentSetting()
{
}
/// <summary>
/// کارکردن در تعطیلات رسمی
/// </summary>
public bool HolidayWorking { get; private set; }
/// <summary>
/// آی دی کاربر
/// </summary>
public long UserId { get; private set; }
/// <summary>
/// دستمزد ماهانه
/// </summary>
public double MonthlySalary { get; private set; }
/// <summary>
/// تاریخ شروع تنظیمات
/// </summary>
public DateTime? StartSettingDate { get; private set; }
/// <summary>
/// تاریخ پایان تنظیمات
/// </summary>
public DateTime? EndSettingDate { get; private set; }
public List<WorkingHours> WorkingHoursList { get; set; }
/// <summary>
/// ویرایش تنظیمات حقوق
/// </summary>
/// <param name="holidayWorking"></param>
/// <param name="workingHoursList"></param>
public void Edit(bool holidayWorking, double monthlySalary, List<WorkingHours> workingHoursList)
{
HolidayWorking = holidayWorking;
WorkingHoursList = workingHoursList;
MonthlySalary = monthlySalary;
}
}

View File

@@ -0,0 +1,164 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Enums;
using System.ComponentModel.DataAnnotations.Schema;
namespace GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Entities;
/// <summary>
/// تخصیص ساعت کاری به کاربر
/// </summary>
public class WorkingHours
{
public WorkingHours(TimeSpan startShiftOne, TimeSpan endShiftOne, TimeSpan startShiftTwo, TimeSpan endShiftTwo, TimeSpan restTime, bool hasShiftOne, bool hasShiftTwo, bool hasRestTime, PersianDayOfWeek persianDayOfWeek, bool isActiveDay)
{
StartShiftOne = hasShiftOne? startShiftOne : TimeSpan.Zero;
EndShiftOne = hasShiftOne ? endShiftOne : TimeSpan.Zero;
StartShiftTwo = hasShiftTwo ? startShiftTwo : TimeSpan.Zero;
EndShiftTwo = hasShiftTwo ? endShiftTwo : TimeSpan.Zero;
RestTime = hasRestTime ? restTime : TimeSpan.Zero;
HasShiftOne = hasShiftOne;
HasShiftTow = hasShiftTwo;
HasRestTime = hasRestTime;
PersianDayOfWeek = persianDayOfWeek;
IsActiveDay = isActiveDay;
ComputeShiftDuration(startShiftOne, endShiftOne, startShiftTwo, endShiftTwo, restTime, hasShiftOne, hasShiftTwo,hasRestTime);
}
private WorkingHours(bool isActiveDay)
{
IsActiveDay = isActiveDay;
}
public long Id { get; private set; }
/// <summary>
/// ساعت شروع شیفت کاری
/// </summary>
public TimeSpan StartShiftOne { get; private set; }
/// <summary>
/// ساعت پایان شیفت کاری
/// </summary>
public TimeSpan EndShiftOne { get; private set; }
/// <summary>
/// ساعت شروع شیفت دوم کاری
/// </summary>
public TimeSpan StartShiftTwo { get; private set; }
/// <summary>
/// ساعت پایان شیفت دوم کاری
/// </summary>
public TimeSpan EndShiftTwo { get; private set; }
/// <summary>
/// مدت استراحت
/// </summary>
public TimeSpan RestTime { get; private set; }
/// <summary>
/// آیا مقطع مار اول دارد
/// </summary>
public bool HasShiftOne { get; private set; }
/// <summary>
/// آیا مقطع کار دوم دارد
/// </summary>
public bool HasShiftTow { get; private set; }
/// <summary>
/// آیا ساعت استراحت دارد
/// </summary>
public bool HasRestTime { get; private set; }
/// <summary>
/// بازه زمانی شیفت
/// </summary>
public int ShiftDurationInMinutes { get; private set; }
[NotMapped]
public TimeSpan ShiftDuration
{
get => TimeSpan.FromMinutes(ShiftDurationInMinutes);
private set => ShiftDurationInMinutes = (int)value.TotalMinutes;
}
/// <summary>
/// عدد روز از ماه
/// </summary>
public PersianDayOfWeek PersianDayOfWeek { get; private set; }
/// <summary>
/// آیا این روز هفته
/// فعال است
/// </summary>
public bool IsActiveDay { get; private set; }
public SalaryPaymentSetting SalaryPaymentSetting { get; set; }
/// <summary>
/// بدست آوردن بازه زمانی شیفت
/// </summary>
/// <param name="startShift"></param>
/// <param name="endShift"></param>
/// <returns></returns>
private void ComputeShiftDuration(TimeSpan startShift, TimeSpan endShift, TimeSpan startShiftTwo, TimeSpan endShiftTwo, TimeSpan restTime, bool hasShiftOne, bool hasShiftTwo, bool hasRestTime)
{
var now = DateTime.Now.Date;
if (hasShiftOne && !hasShiftTwo)
{
DateTime startOne = new DateTime(now.Year, now.Month, now.Day, startShift.Hours, startShift.Minutes,0);
DateTime endOne = new DateTime(now.Year, now.Month, now.Day, endShift.Hours, endShift.Minutes, 0);
if (endOne <= startOne)
endOne = endOne.AddDays(1);
var shiftDuration = (endOne - startOne);
ShiftDuration = hasRestTime ? (shiftDuration - restTime) : shiftDuration;
}
else if (!hasShiftOne && hasShiftTwo)
{
DateTime startTow = new DateTime(now.Year, now.Month, now.Day, startShiftTwo.Hours, startShiftTwo.Minutes, 0);
DateTime endTow = new DateTime(now.Year, now.Month, now.Day, endShiftTwo.Hours, endShiftTwo.Minutes, 0);
if (endTow <= startTow)
endTow = endTow.AddDays(1);
ShiftDuration = (endTow - startTow);
}
else if (hasShiftOne && hasShiftTwo)
{
DateTime startOne = new DateTime(now.Year, now.Month, now.Day, startShift.Hours, startShift.Minutes, 0);
DateTime endOne = new DateTime(now.Year, now.Month, now.Day, endShift.Hours, endShift.Minutes, 0);
if (endOne <= startOne){}
endOne = endOne.AddDays(1);
var shiftOneDuration = (endOne - startOne);
DateTime startTow = new DateTime(endOne.Year, endOne.Month, endOne.Day, startShiftTwo.Hours, startShiftTwo.Minutes, 0);
DateTime endTow = new DateTime(endOne.Year, endOne.Month, endOne.Day, endShiftTwo.Hours, endShiftTwo.Minutes, 0);
if (endTow <= startTow)
endTow = endTow.AddDays(1);
var shiftDurationTow = (endTow - startTow);
ShiftDuration = shiftOneDuration.Add(shiftDurationTow);
}
else
{
ShiftDurationInMinutes = 0;
}
}
}

View File

@@ -0,0 +1,20 @@
namespace GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Enums;
public enum HasSalarySettings
{
/// <summary>
/// پیش فرض هر دو حالت
/// </summary>
Default = 0,
/// <summary>
/// تنظیمات حقوق و ساعت دارد
/// </summary>
HasSettings = 1,
/// <summary>
/// تنظیمات حقوق و ساعت ندارد
/// </summary>
DoesNotHaveSettings = 2
}

View File

@@ -0,0 +1,29 @@
namespace GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Enums;
public enum PersianDayOfWeek
{
/// <summary> شنبه </summary>
Shanbeh = 1,
/// <summary> یکشنبه </summary>
YekShanbeh = 2,
/// <summary> دوشنبه </summary>
DoShanbeh = 3,
/// <summary> سه شنبه </summary>
SeShanbeh = 4,
/// <summary> چهارشنبه </summary>
CheharShanbeh = 5,
/// <summary> پنجشنبه </summary>
PanjShanbeh = 6,
/// <summary> جمعه </summary>
Jomeh = 7,
}

View File

@@ -0,0 +1,26 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.DTOs;
using GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Entities;
namespace GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Repositories;
public interface ISalaryPaymentSettingRepository : IRepository<long,SalaryPaymentSetting>
{
/// <summary>
/// دریافت لیست تنظیمات حقوق
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
Task<SalaryPaymentSetting?> GetSalarySettingByUserId(long userId);
Task<List<UserSalarySettingDto>> GetAllSettings(List<long> userIdList);
/// <summary>
/// حذف گروهی تنظیمات حقوق
/// </summary>
/// <param name="removedItems"></param>
/// <returns></returns>
void RemoveRangeSalarySettings(List<SalaryPaymentSetting> removedItems);
}

View File

@@ -0,0 +1,16 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
namespace GozareshgirProgramManager.Domain.SkillAgg.Entities;
public class Skill:EntityBase<Guid>, IAggregateRoot
{
public Skill(string name)
{
Name = name;
Sections = [];
}
public string Name { get; set; }
public List<TaskSection> Sections { get; set; }
}

View File

@@ -0,0 +1,9 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.SkillAgg.Entities;
namespace GozareshgirProgramManager.Domain.SkillAgg.Repositories;
public interface ISkillRepository: IRepository<Guid,Skill>
{
}

View File

@@ -0,0 +1,169 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.PermissionAgg.Entities;
using GozareshgirProgramManager.Domain.RoleAgg.Entities;
using GozareshgirProgramManager.Domain.RoleUserAgg;
namespace GozareshgirProgramManager.Domain.UserAgg.Entities;
/// <summary>
/// کاربر
/// </summary>
public class User : EntityBase<long>
{
/// <summary>
/// ایجاد
/// </summary>
/// <param name="fullName"></param>
/// <param name="userName"></param>
/// <param name="password"></param>
/// <param name="mobile"></param>
/// <param name="email"></param>
/// <param name="accountId"></param>
/// <param name="roles"></param>
public User(string fullName, string userName, string password, string mobile, string? email, long? accountId, List<RoleUser> roles)
{
FullName = fullName;
UserName = userName;
Password = password;
Mobile = mobile;
Email = email;
IsActive = true;
AccountId = accountId;
RoleUser = roles;
}
protected User()
{
}
/// <summary>
/// نام و نام خانوادگی
/// </summary>
public string FullName { get; private set; }
/// <summary>
/// نام کاربری
/// </summary>
public string UserName { get; private set; }
/// <summary>
/// گذرواژه
/// </summary>
public string Password { get; private set; }
/// <summary>
/// مسیر عکس پروفایل
/// </summary>
public string ProfilePhotoPath { get; private set; }
/// <summary>
/// شماره موبایل
/// </summary>
public string Mobile { get; set; }
/// <summary>
/// ایمیل
/// </summary>
public string? Email { get; private set; }
/// <summary>
/// فعال/غیر فعال بودن یوزر
/// </summary>
public bool IsActive { get; private set; }
/// <summary>
/// کد یکبارمصرف ورود
/// </summary>
public string? VerifyCode { get; private set; }
/// <summary>
/// آی دی کاربر در گزارشگیر
/// </summary>
public long? AccountId { get; private set; }
/// <summary>
/// لیست پرمیشن کد ها
/// </summary>
public List<RoleUser> RoleUser { get; private set; }
/// <summary>
/// لیست توکن‌های تازه‌سازی
/// </summary>
private List<UserRefreshToken> _refreshTokens = new();
public IReadOnlyCollection<UserRefreshToken> RefreshTokens => _refreshTokens.AsReadOnly();
/// <summary>
/// آپدیت کاربر
/// </summary>
/// <param name="fullName"></param>
/// <param name="userName"></param>
/// <param name="mobile"></param>
/// <param name="roles"></param>
/// <param name="isActive"></param>
public void Edit(string fullName, string userName, string mobile, List<RoleUser> roles, bool isActive)
{
FullName = fullName;
UserName = userName;
Mobile = mobile;
RoleUser = roles;
IsActive = isActive;
}
/// <summary>
/// غیرفعال سازی
/// </summary>
public void DeActive()
{
IsActive = false;
}
/// <summary>
/// فعال سازی
/// </summary>
public void ReActive()
{
IsActive = true;
}
/// <summary>
/// افزودن توکن تازه‌سازی
/// </summary>
public void AddRefreshToken(string token, DateTime expiresAt, string? ipAddress = null, string? userAgent = null)
{
var refreshToken = new UserRefreshToken(Id, token, expiresAt, ipAddress, userAgent);
_refreshTokens.Add(refreshToken);
}
/// <summary>
/// لغو توکن تازه‌سازی
/// </summary>
public void RevokeRefreshToken(string token)
{
var refreshToken = _refreshTokens.FirstOrDefault(x => x.Token == token);
if (refreshToken == null)
throw new InvalidOperationException("توکن یافت نشد");
refreshToken.Revoke();
}
/// <summary>
/// لغو تمام توکن‌های فعال
/// </summary>
public void RevokeAllRefreshTokens()
{
foreach (var token in _refreshTokens.Where(x => x.IsActive))
{
token.Revoke();
}
}
/// <summary>
/// پاکسازی توکن‌های منقضی شده
/// </summary>
public void RemoveExpiredRefreshTokens()
{
_refreshTokens.RemoveAll(x => !x.IsActive);
}
}

View File

@@ -0,0 +1,90 @@
using GozareshgirProgramManager.Domain._Common;
namespace GozareshgirProgramManager.Domain.UserAgg.Entities;
/// <summary>
/// توکن تازه‌سازی برای احراز هویت
/// </summary>
public class UserRefreshToken : EntityBase<Guid>
{
/// <summary>
/// سازنده محافظت شده برای EF Core
/// </summary>
protected UserRefreshToken()
{
}
/// <summary>
/// ایجاد توکن تازه‌سازی
/// </summary>
public UserRefreshToken(long userId, string token, DateTime expiresAt, string? ipAddress = null, string? userAgent = null)
{
UserId = userId;
Token = token;
ExpiresAt = expiresAt;
IpAddress = ipAddress;
UserAgent = userAgent;
}
/// <summary>
/// شناسه کاربر
/// </summary>
public long UserId { get; private set; }
/// <summary>
/// توکن تازه‌سازی
/// </summary>
public string Token { get; private set; }
/// <summary>
/// تاریخ انقضا
/// </summary>
public DateTime ExpiresAt { get; private set; }
/// <summary>
/// تاریخ لغو
/// </summary>
public DateTime? RevokedAt { get; private set; }
/// <summary>
/// آی‌پی کاربر
/// </summary>
public string? IpAddress { get; private set; }
/// <summary>
/// User Agent مرورگر
/// </summary>
public string? UserAgent { get; private set; }
/// <summary>
/// آیا منقضی شده؟
/// </summary>
public bool IsExpired => DateTime.Now >= ExpiresAt;
/// <summary>
/// آیا لغو شده؟
/// </summary>
public bool IsRevoked => RevokedAt.HasValue;
/// <summary>
/// آیا فعال است؟
/// </summary>
public bool IsActive => !IsRevoked && !IsExpired;
/// <summary>
/// لغو توکن
/// </summary>
public void Revoke()
{
if (IsRevoked)
throw new InvalidOperationException("توکن قبلاً لغو شده است");
RevokedAt = DateTime.Now;
}
/// <summary>
/// کاربر صاحب توکن
/// </summary>
public User User { get; private set; }
}

View File

@@ -0,0 +1,47 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.UserAgg.Entities;
namespace GozareshgirProgramManager.Domain.UserAgg.Events;
public record UserCreatedEvent(long UserId, string FirstName, string LastName, string Email) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record UserPersonalInfoUpdatedEvent(long UserId, string OldFirstName, string OldLastName, string NewFirstName, string NewLastName) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record UserEmailUpdatedEvent(long UserId, string OldEmail, string NewEmail) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record UserWorkInfoUpdatedEvent(long UserId, string? Department, string? Position) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record UserDeactivatedEvent(long UserId, string? Reason) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record UserActivatedEvent(long UserId) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
public record UserLoggedInEvent(long UserId, DateTime LoginTime) : IDomainEvent
{
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}

View File

@@ -0,0 +1,9 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.UserAgg.Entities;
namespace GozareshgirProgramManager.Domain.UserAgg.Repositories;
public interface IUserRefreshTokenRepository : IRepository<Guid, UserRefreshToken>
{
}

View File

@@ -0,0 +1,32 @@
using GozareshgirProgramManager.Domain._Common;
using GozareshgirProgramManager.Domain.UserAgg.Entities;
namespace GozareshgirProgramManager.Domain.UserAgg.Repositories;
public interface IUserRepository: IRepository<long, User>
{
Task<User?> GetByIdAsync(long id);
/// <summary>
/// یافتن کاربر با آی دی اکانت گزارشگیر او
/// </summary>
/// <param name="accountId"></param>
/// <returns></returns>
Task<User?> GetByGozareshgirAccountId(long accountId);
Task<User?> GetByEmailAsync(string email);
Task<User?> GetByMobileAsync(string mobile);
Task<IEnumerable<User>> GetAllAsync();
Task<IEnumerable<User>> GetActiveUsersAsync();
Task<User> AddAsync(User user);
void Update(User user);
void Delete(User user);
Task<bool> ExistsAsync(long id);
Task<bool> UsernameExistsAsync(string username);
Task<bool> EmailExistsAsync(string email);
Task<bool> MobileExistsAsync(string mobile);
Task<User?> GetUserWithRolesByIdAsync(long userId, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace GozareshgirProgramManager.Domain._Common;
public abstract class EntityBase<TId>
{
public EntityBase()
{
if (typeof(TId) == typeof(Guid))
{
Id = (TId)(object)Guid.NewGuid();
}
CreationDate = DateTime.Now;
}
public TId Id { get; protected set; }
public DateTime CreationDate { get; protected set; }
private readonly List<IDomainEvent> _domainEvents = new();
[NotMapped]
public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
protected void AddDomainEvent(IDomainEvent domainEvent) => _domainEvents.Add(domainEvent);
public void ClearDomainEvents() => _domainEvents.Clear();
}

View File

@@ -0,0 +1,24 @@
namespace GozareshgirProgramManager.Domain._Common.Exceptions;
public class BadRequestException:Exception
{
public BadRequestException(string message):base(message)
{
}
public BadRequestException(string message, string details) : base(message)
{
Details = details;
}
public BadRequestException(string message, Dictionary<string, object?> extra) :
base(message)
{
Extra = extra;
}
public string Details { get; }
public Dictionary<string,object> Extra { get; set; }
}

View File

@@ -0,0 +1,13 @@
namespace GozareshgirProgramManager.Domain._Common.Exceptions;
public class NotFoundException:Exception
{
public NotFoundException(string message) : base(message)
{
}
public NotFoundException(string name, object key) : base($"Entity \"{name}\" ({key}) was not found.")
{
}
}

View File

@@ -0,0 +1,8 @@
namespace GozareshgirProgramManager.Domain._Common.Exceptions;
public class UnAuthorizedException:Exception
{
public UnAuthorizedException(string message) : base(message)
{
}
}

View File

@@ -0,0 +1,8 @@
namespace GozareshgirProgramManager.Domain._Common;
/// <summary>
/// Marker interface for Aggregate Roots
/// </summary>
public interface IAggregateRoot
{
}

View File

@@ -0,0 +1,6 @@
namespace GozareshgirProgramManager.Domain._Common;
public interface IDomainEvent
{
DateTime OccurredOn { get; }
}

View File

@@ -0,0 +1,17 @@
using System.Linq.Expressions;
namespace GozareshgirProgramManager.Domain._Common;
public interface IRepository<TKey, T> where T:class
{
T Get(TKey id);
Task<T?> GetByIdAsync(TKey id);
List<T> Get();
IQueryable<T> GetQueryable();
void Create(T entity);
Task CreateAsync(T entity);
bool ExistsIgnoreQueryFilter(Expression<Func<T, bool>> expression);
bool Exists(Expression<Func<T, bool>> expression);
//void SaveChanges();
//Task SaveChangesAsync();
}

View File

@@ -0,0 +1,9 @@
namespace GozareshgirProgramManager.Domain._Common;
/// <summary>
/// Unit of Work pattern interface
/// </summary>
public interface IUnitOfWork
{
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
namespace GozareshgirProgramManager.Domain._Common;
public abstract class ValueObject
{
protected abstract IEnumerable<object?> GetEqualityComponents();
public override bool Equals(object? obj)
{
if (obj == null || obj.GetType() != GetType())
return false;
var other = (ValueObject)obj;
return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x?.GetHashCode() ?? 0)
.Aggregate((x, y) => x ^ y);
}
public static bool operator ==(ValueObject? left, ValueObject? right)
{
if (left is null && right is null)
return true;
if (left is null || right is null)
return false;
return left.Equals(right);
}
public static bool operator !=(ValueObject? left, ValueObject? right)
{
return !(left == right);
}
}