Add new domain models and interfaces for project management features
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace GozareshgirProgramManager.Domain.CheckoutAgg.Enums;
|
||||
|
||||
public enum CreateCheckoutStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// آماده ایجاد
|
||||
/// </summary>
|
||||
ReadyToCreate = 1,
|
||||
/// <summary>
|
||||
/// قبلا ایجاد شده
|
||||
/// </summary>
|
||||
AlreadyCreated = 2,
|
||||
/// <summary>
|
||||
/// تنظیمات حقوق انجام نشده
|
||||
/// </summary>
|
||||
NotSetSalaryPaymentSettings = 3
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace GozareshgirProgramManager.Domain.CheckoutAgg.Enums;
|
||||
|
||||
public enum PersianMonthName
|
||||
{
|
||||
فروردین = 1,
|
||||
اردیبهشت = 2,
|
||||
خرداد = 3,
|
||||
تیر = 4,
|
||||
مرداد = 5,
|
||||
شهریور = 6,
|
||||
مهر = 7,
|
||||
آبان = 8,
|
||||
آذر = 9,
|
||||
دی = 10,
|
||||
بهمن = 11,
|
||||
اسفند = 12,
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace GozareshgirProgramManager.Domain.ProjectAgg.Enums;
|
||||
|
||||
public enum TaskSectionStatus
|
||||
{
|
||||
NotAssigned = 0, // تخصیص داده نشده
|
||||
ReadyToStart = 1, // آماده شروع
|
||||
InProgress = 2, // درحال انجام
|
||||
Incomplete = 3, // ناتمام شده
|
||||
Completed = 4 // تکمیل شده
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace GozareshgirProgramManager.Domain.ProjectAgg.Enums;
|
||||
|
||||
public enum TaskStatus
|
||||
{
|
||||
NotStarted = 1,
|
||||
InProgress = 2,
|
||||
Completed = 3,
|
||||
OnHold = 4
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
|
||||
using GozareshgirProgramManager.Domain._Common;
|
||||
namespace GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
|
||||
public interface IPhaseSectionRepository : IRepository<Guid, PhaseSection>
|
||||
{
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using GozareshgirProgramManager.Domain._Common;
|
||||
using GozareshgirProgramManager.Domain.ProjectAgg.Entities;
|
||||
|
||||
namespace GozareshgirProgramManager.Domain.ProjectAgg.Repositories;
|
||||
|
||||
public interface IProjectSectionRepository : IRepository<Guid, ProjectSection>
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace GozareshgirProgramManager.Domain.SalaryPaymentSettingAgg.Enums;
|
||||
|
||||
public enum HasSalarySettings
|
||||
{
|
||||
/// <summary>
|
||||
/// پیش فرض هر دو حالت
|
||||
/// </summary>
|
||||
Default = 0,
|
||||
|
||||
/// <summary>
|
||||
/// تنظیمات حقوق و ساعت دارد
|
||||
/// </summary>
|
||||
HasSettings = 1,
|
||||
/// <summary>
|
||||
/// تنظیمات حقوق و ساعت ندارد
|
||||
/// </summary>
|
||||
DoesNotHaveSettings = 2
|
||||
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using GozareshgirProgramManager.Domain._Common;
|
||||
using GozareshgirProgramManager.Domain.SkillAgg.Entities;
|
||||
|
||||
namespace GozareshgirProgramManager.Domain.SkillAgg.Repositories;
|
||||
|
||||
public interface ISkillRepository: IRepository<Guid,Skill>
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using GozareshgirProgramManager.Domain._Common;
|
||||
using GozareshgirProgramManager.Domain.UserAgg.Entities;
|
||||
|
||||
namespace GozareshgirProgramManager.Domain.UserAgg.Repositories;
|
||||
|
||||
public interface IUserRefreshTokenRepository : IRepository<Guid, UserRefreshToken>
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
}
|
||||
@@ -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.")
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace GozareshgirProgramManager.Domain._Common.Exceptions;
|
||||
|
||||
public class UnAuthorizedException:Exception
|
||||
{
|
||||
public UnAuthorizedException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace GozareshgirProgramManager.Domain._Common;
|
||||
|
||||
/// <summary>
|
||||
/// Marker interface for Aggregate Roots
|
||||
/// </summary>
|
||||
public interface IAggregateRoot
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace GozareshgirProgramManager.Domain._Common;
|
||||
|
||||
public interface IDomainEvent
|
||||
{
|
||||
DateTime OccurredOn { get; }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user