Ashutosh_Task#513 #96

Merged
ashutosh.nehete merged 58 commits from Ashutosh_Task#513 into Issues_June_3W 2025-06-28 04:27:28 +00:00
43 changed files with 15402 additions and 491 deletions

View File

@ -44,6 +44,7 @@ namespace Marco.Pms.DataAccess.Data
public DbSet<TaskAllocation> TaskAllocations { get; set; } public DbSet<TaskAllocation> TaskAllocations { get; set; }
public DbSet<TaskComment> TaskComments { get; set; } public DbSet<TaskComment> TaskComments { get; set; }
public DbSet<TaskMembers> TaskMembers { get; set; } public DbSet<TaskMembers> TaskMembers { get; set; }
public DbSet<TaskAttachment> TaskAttachments { get; set; }
public DbSet<Attendance> Attendes { get; set; } public DbSet<Attendance> Attendes { get; set; }
public DbSet<AttendanceLog> AttendanceLogs { get; set; } public DbSet<AttendanceLog> AttendanceLogs { get; set; }
public DbSet<Employee> Employees { get; set; } public DbSet<Employee> Employees { get; set; }
@ -68,6 +69,7 @@ namespace Marco.Pms.DataAccess.Data
public DbSet<Document> Documents { get; set; } public DbSet<Document> Documents { get; set; }
public DbSet<TicketTag> TicketTags { get; set; } public DbSet<TicketTag> TicketTags { get; set; }
public DbSet<WorkCategoryMaster> WorkCategoryMasters { get; set; } public DbSet<WorkCategoryMaster> WorkCategoryMasters { get; set; }
public DbSet<WorkStatusMaster> WorkStatusMasters { get; set; }
public DbSet<Contact> Contacts { get; set; } public DbSet<Contact> Contacts { get; set; }
public DbSet<ContactCategoryMaster> ContactCategoryMasters { get; set; } public DbSet<ContactCategoryMaster> ContactCategoryMasters { get; set; }
public DbSet<ContactEmail> ContactsEmails { get; set; } public DbSet<ContactEmail> ContactsEmails { get; set; }
@ -160,14 +162,19 @@ namespace Marco.Pms.DataAccess.Data
}, },
new StatusMaster new StatusMaster
{ {
Id = new Guid("603e994b-a27f-4e5d-a251-f3d69b0498ba"), Id = new Guid("cdad86aa-8a56-4ff4-b633-9c629057dfef"),
Status = "In Progress", Status = "In Progress",
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26") TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
}, new StatusMaster
{
Id = new Guid("603e994b-a27f-4e5d-a251-f3d69b0498ba"),
Status = "On Hold",
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
}, },
new StatusMaster new StatusMaster
{ {
Id = new Guid("ef1c356e-0fe0-42df-a5d3-8daee355492d"), Id = new Guid("ef1c356e-0fe0-42df-a5d3-8daee355492d"),
Status = "On Hold", Status = "In Active",
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26") TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
}, },
new StatusMaster new StatusMaster
@ -428,6 +435,32 @@ namespace Marco.Pms.DataAccess.Data
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26") TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
} }
); );
modelBuilder.Entity<WorkStatusMaster>().HasData(
new WorkStatusMaster
{
Id = new Guid("030bb085-e230-4370-aec7-9a74d652864e"),
Name = "Approve",
Description = "Confirm the tasks are actually finished as reported",
IsSystem = true,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
},
new WorkStatusMaster
{
Id = new Guid("2a1a5b96-cf93-4111-b4b1-76c19d6333b4"),
Name = "Partially Approve",
Description = "Not all tasks are actually finished as reported",
IsSystem = true,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
},
new WorkStatusMaster
{
Id = new Guid("00a062e6-62e6-42c5-b6b1-024328651b72"),
Name = "NCR",
Description = "Tasks are not finished as reported or have any issues in al the tasks",
IsSystem = true,
TenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26")
}
);
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_TaskAttachments_Table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "TaskAttachments",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
ReferenceId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
DocumentId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
},
constraints: table =>
{
table.PrimaryKey("PK_TaskAttachments", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "TaskAttachments");
}
}
}

View File

@ -0,0 +1,188 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_Apporved_By_In_TaskAllocation_Table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "ApprovedById",
table: "TaskAllocations",
type: "char(36)",
nullable: true,
collation: "ascii_general_ci");
migrationBuilder.AddColumn<DateTime>(
name: "ApprovedDate",
table: "TaskAllocations",
type: "datetime(6)",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "ParentTaskId",
table: "TaskAllocations",
type: "char(36)",
nullable: true,
collation: "ascii_general_ci");
migrationBuilder.AddColumn<Guid>(
name: "ReportedById",
table: "TaskAllocations",
type: "char(36)",
nullable: true,
collation: "ascii_general_ci");
migrationBuilder.AddColumn<double>(
name: "ReportedTask",
table: "TaskAllocations",
type: "double",
nullable: false,
defaultValue: 0.0);
migrationBuilder.AddColumn<Guid>(
name: "WorkStatusId",
table: "TaskAllocations",
type: "char(36)",
nullable: true,
collation: "ascii_general_ci");
migrationBuilder.CreateTable(
name: "WorkStatusMasters",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
Name = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Description = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
IsSystem = table.Column<bool>(type: "tinyint(1)", nullable: false),
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
},
constraints: table =>
{
table.PrimaryKey("PK_WorkStatusMasters", x => x.Id);
table.ForeignKey(
name: "FK_WorkStatusMasters_Tenants_TenantId",
column: x => x.TenantId,
principalTable: "Tenants",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.InsertData(
table: "WorkStatusMasters",
columns: new[] { "Id", "Description", "IsSystem", "Name", "TenantId" },
values: new object[,]
{
{ new Guid("00a062e6-62e6-42c5-b6b1-024328651b72"), "Tasks are not finished as reported or have any issues in al the tasks", true, "NCR", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
{ new Guid("030bb085-e230-4370-aec7-9a74d652864e"), "Confirm the tasks are actually finished as reported", true, "Approve", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") },
{ new Guid("2a1a5b96-cf93-4111-b4b1-76c19d6333b4"), "Not all tasks are actually finished as reported", true, "Partially Approve", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") }
});
migrationBuilder.CreateIndex(
name: "IX_TaskAllocations_ApprovedById",
table: "TaskAllocations",
column: "ApprovedById");
migrationBuilder.CreateIndex(
name: "IX_TaskAllocations_ReportedById",
table: "TaskAllocations",
column: "ReportedById");
migrationBuilder.CreateIndex(
name: "IX_TaskAllocations_WorkStatusId",
table: "TaskAllocations",
column: "WorkStatusId");
migrationBuilder.CreateIndex(
name: "IX_WorkStatusMasters_TenantId",
table: "WorkStatusMasters",
column: "TenantId");
migrationBuilder.AddForeignKey(
name: "FK_TaskAllocations_Employees_ApprovedById",
table: "TaskAllocations",
column: "ApprovedById",
principalTable: "Employees",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_TaskAllocations_Employees_ReportedById",
table: "TaskAllocations",
column: "ReportedById",
principalTable: "Employees",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_TaskAllocations_WorkStatusMasters_WorkStatusId",
table: "TaskAllocations",
column: "WorkStatusId",
principalTable: "WorkStatusMasters",
principalColumn: "Id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_TaskAllocations_Employees_ApprovedById",
table: "TaskAllocations");
migrationBuilder.DropForeignKey(
name: "FK_TaskAllocations_Employees_ReportedById",
table: "TaskAllocations");
migrationBuilder.DropForeignKey(
name: "FK_TaskAllocations_WorkStatusMasters_WorkStatusId",
table: "TaskAllocations");
migrationBuilder.DropTable(
name: "WorkStatusMasters");
migrationBuilder.DropIndex(
name: "IX_TaskAllocations_ApprovedById",
table: "TaskAllocations");
migrationBuilder.DropIndex(
name: "IX_TaskAllocations_ReportedById",
table: "TaskAllocations");
migrationBuilder.DropIndex(
name: "IX_TaskAllocations_WorkStatusId",
table: "TaskAllocations");
migrationBuilder.DropColumn(
name: "ApprovedById",
table: "TaskAllocations");
migrationBuilder.DropColumn(
name: "ApprovedDate",
table: "TaskAllocations");
migrationBuilder.DropColumn(
name: "ParentTaskId",
table: "TaskAllocations");
migrationBuilder.DropColumn(
name: "ReportedById",
table: "TaskAllocations");
migrationBuilder.DropColumn(
name: "ReportedTask",
table: "TaskAllocations");
migrationBuilder.DropColumn(
name: "WorkStatusId",
table: "TaskAllocations");
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class EnhancedWorkItemForParentId_Description : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Description",
table: "WorkItems",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.AddColumn<Guid>(
name: "ParentTaskId",
table: "WorkItems",
type: "char(36)",
nullable: true,
collation: "ascii_general_ci");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Description",
table: "WorkItems");
migrationBuilder.DropColumn(
name: "ParentTaskId",
table: "WorkItems");
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,57 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_New_Status_Master_In_Progress : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.UpdateData(
table: "StatusMasters",
keyColumn: "Id",
keyValue: new Guid("603e994b-a27f-4e5d-a251-f3d69b0498ba"),
column: "Status",
value: "On Hold");
migrationBuilder.UpdateData(
table: "StatusMasters",
keyColumn: "Id",
keyValue: new Guid("ef1c356e-0fe0-42df-a5d3-8daee355492d"),
column: "Status",
value: "In Active");
migrationBuilder.InsertData(
table: "StatusMasters",
columns: new[] { "Id", "Status", "TenantId" },
values: new object[] { new Guid("cdad86aa-8a56-4ff4-b633-9c629057dfef"), "In Progress", new Guid("b3466e83-7e11-464c-b93a-daf047838b26") });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: "StatusMasters",
keyColumn: "Id",
keyValue: new Guid("cdad86aa-8a56-4ff4-b633-9c629057dfef"));
migrationBuilder.UpdateData(
table: "StatusMasters",
keyColumn: "Id",
keyValue: new Guid("603e994b-a27f-4e5d-a251-f3d69b0498ba"),
column: "Status",
value: "In Progress");
migrationBuilder.UpdateData(
table: "StatusMasters",
keyColumn: "Id",
keyValue: new Guid("ef1c356e-0fe0-42df-a5d3-8daee355492d"),
column: "Status",
value: "On Hold");
}
}
}

View File

@ -28,6 +28,12 @@ namespace Marco.Pms.DataAccess.Migrations
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.Property<Guid?>("ApprovedById")
.HasColumnType("char(36)");
b.Property<DateTime?>("ApprovedDate")
.HasColumnType("datetime(6)");
b.Property<Guid>("AssignedBy") b.Property<Guid>("AssignedBy")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
@ -40,29 +46,64 @@ namespace Marco.Pms.DataAccess.Migrations
b.Property<string>("Description") b.Property<string>("Description")
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<Guid?>("ParentTaskId")
.HasColumnType("char(36)");
b.Property<double>("PlannedTask") b.Property<double>("PlannedTask")
.HasColumnType("double"); .HasColumnType("double");
b.Property<Guid?>("ReportedById")
.HasColumnType("char(36)");
b.Property<DateTime?>("ReportedDate") b.Property<DateTime?>("ReportedDate")
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");
b.Property<double>("ReportedTask")
.HasColumnType("double");
b.Property<Guid>("TenantId") b.Property<Guid>("TenantId")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.Property<Guid>("WorkItemId") b.Property<Guid>("WorkItemId")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
b.Property<Guid?>("WorkStatusId")
.HasColumnType("char(36)");
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ApprovedById");
b.HasIndex("AssignedBy"); b.HasIndex("AssignedBy");
b.HasIndex("ReportedById");
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.HasIndex("WorkItemId"); b.HasIndex("WorkItemId");
b.HasIndex("WorkStatusId");
b.ToTable("TaskAllocations"); b.ToTable("TaskAllocations");
}); });
modelBuilder.Entity("Marco.Pms.Model.Activities.TaskAttachment", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<Guid>("DocumentId")
.HasColumnType("char(36)");
b.Property<Guid>("ReferenceId")
.HasColumnType("char(36)");
b.HasKey("Id");
b.ToTable("TaskAttachments");
});
modelBuilder.Entity("Marco.Pms.Model.Activities.TaskComment", b => modelBuilder.Entity("Marco.Pms.Model.Activities.TaskComment", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -1658,17 +1699,23 @@ namespace Marco.Pms.DataAccess.Migrations
}, },
new new
{ {
Id = new Guid("603e994b-a27f-4e5d-a251-f3d69b0498ba"), Id = new Guid("cdad86aa-8a56-4ff4-b633-9c629057dfef"),
Status = "In Progress", Status = "In Progress",
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
}, },
new new
{ {
Id = new Guid("ef1c356e-0fe0-42df-a5d3-8daee355492d"), Id = new Guid("603e994b-a27f-4e5d-a251-f3d69b0498ba"),
Status = "On Hold", Status = "On Hold",
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26") TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
}, },
new new
{
Id = new Guid("ef1c356e-0fe0-42df-a5d3-8daee355492d"),
Status = "In Active",
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
},
new
{ {
Id = new Guid("33deaef9-9af1-4f2a-b443-681ea0d04f81"), Id = new Guid("33deaef9-9af1-4f2a-b443-681ea0d04f81"),
Status = "Completed", Status = "Completed",
@ -1919,6 +1966,59 @@ namespace Marco.Pms.DataAccess.Migrations
}); });
}); });
modelBuilder.Entity("Marco.Pms.Model.Master.WorkStatusMaster", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("longtext");
b.Property<bool>("IsSystem")
.HasColumnType("tinyint(1)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
b.HasKey("Id");
b.HasIndex("TenantId");
b.ToTable("WorkStatusMasters");
b.HasData(
new
{
Id = new Guid("030bb085-e230-4370-aec7-9a74d652864e"),
Description = "Confirm the tasks are actually finished as reported",
IsSystem = true,
Name = "Approve",
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
},
new
{
Id = new Guid("2a1a5b96-cf93-4111-b4b1-76c19d6333b4"),
Description = "Not all tasks are actually finished as reported",
IsSystem = true,
Name = "Partially Approve",
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
},
new
{
Id = new Guid("00a062e6-62e6-42c5-b6b1-024328651b72"),
Description = "Tasks are not finished as reported or have any issues in al the tasks",
IsSystem = true,
Name = "NCR",
TenantId = new Guid("b3466e83-7e11-464c-b93a-daf047838b26")
});
});
modelBuilder.Entity("Marco.Pms.Model.Projects.Building", b => modelBuilder.Entity("Marco.Pms.Model.Projects.Building", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -2098,6 +2198,12 @@ namespace Marco.Pms.DataAccess.Migrations
b.Property<double>("CompletedWork") b.Property<double>("CompletedWork")
.HasColumnType("double"); .HasColumnType("double");
b.Property<string>("Description")
.HasColumnType("longtext");
b.Property<Guid?>("ParentTaskId")
.HasColumnType("char(36)");
b.Property<double>("PlannedWork") b.Property<double>("PlannedWork")
.HasColumnType("double"); .HasColumnType("double");
@ -2428,12 +2534,20 @@ namespace Marco.Pms.DataAccess.Migrations
modelBuilder.Entity("Marco.Pms.Model.Activities.TaskAllocation", b => modelBuilder.Entity("Marco.Pms.Model.Activities.TaskAllocation", b =>
{ {
b.HasOne("Marco.Pms.Model.Employees.Employee", "ApprovedBy")
.WithMany()
.HasForeignKey("ApprovedById");
b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee") b.HasOne("Marco.Pms.Model.Employees.Employee", "Employee")
.WithMany() .WithMany()
.HasForeignKey("AssignedBy") .HasForeignKey("AssignedBy")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("Marco.Pms.Model.Employees.Employee", "ReportedBy")
.WithMany()
.HasForeignKey("ReportedById");
b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant")
.WithMany() .WithMany()
.HasForeignKey("TenantId") .HasForeignKey("TenantId")
@ -2446,11 +2560,21 @@ namespace Marco.Pms.DataAccess.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("Marco.Pms.Model.Master.WorkStatusMaster", "WorkStatus")
.WithMany()
.HasForeignKey("WorkStatusId");
b.Navigation("ApprovedBy");
b.Navigation("Employee"); b.Navigation("Employee");
b.Navigation("ReportedBy");
b.Navigation("Tenant"); b.Navigation("Tenant");
b.Navigation("WorkItem"); b.Navigation("WorkItem");
b.Navigation("WorkStatus");
}); });
modelBuilder.Entity("Marco.Pms.Model.Activities.TaskComment", b => modelBuilder.Entity("Marco.Pms.Model.Activities.TaskComment", b =>
@ -3063,6 +3187,17 @@ namespace Marco.Pms.DataAccess.Migrations
b.Navigation("Tenant"); b.Navigation("Tenant");
}); });
modelBuilder.Entity("Marco.Pms.Model.Master.WorkStatusMaster", b =>
{
b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant")
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Tenant");
});
modelBuilder.Entity("Marco.Pms.Model.Projects.Building", b => modelBuilder.Entity("Marco.Pms.Model.Projects.Building", b =>
{ {
b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant") b.HasOne("Marco.Pms.Model.Entitlements.Tenant", "Tenant")

View File

@ -1,5 +1,6 @@
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using Marco.Pms.Model.Employees; using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Master;
using Marco.Pms.Model.Projects; using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
@ -10,29 +11,43 @@ namespace Marco.Pms.Model.Activities
public class TaskAllocation : TenantRelation public class TaskAllocation : TenantRelation
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid? ParentTaskId { get; set; }
public DateTime AssignmentDate { get; set; } public DateTime AssignmentDate { get; set; }
public double PlannedTask { get; set; } public double PlannedTask { get; set; }
public double CompletedTask { get; set; } public double CompletedTask { get; set; }
public double ReportedTask { get; set; }
public DateTime? ReportedDate { get; set; } public DateTime? ReportedDate { get; set; }
public DateTime? ApprovedDate { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
//public int? WorkItemMappingId { get; set; } public Guid AssignedBy { get; set; } //Employee Id
//[ForeignKey("WorkItemMappingId")]
//[ValidateNever]
//public WorkItemMapping? WorkItemMapping { get; set; }
public Guid AssignedBy { get; set; } //Employee Id
[ForeignKey("AssignedBy")] [ForeignKey("AssignedBy")]
[ValidateNever] [ValidateNever]
public Employee? Employee { get; set; } public Employee? Employee { get; set; }
public Guid? ReportedById { get; set; } //Employee Id
[ForeignKey("ReportedById")]
[ValidateNever]
public Employee? ReportedBy { get; set; }
public Guid? ApprovedById { get; set; } //Employee Id
[ForeignKey("ApprovedById")]
[ValidateNever]
public Employee? ApprovedBy { get; set; }
public Guid WorkItemId { get; set; } public Guid WorkItemId { get; set; }
[ForeignKey("WorkItemId")] [ForeignKey("WorkItemId")]
[ValidateNever] [ValidateNever]
public WorkItem? WorkItem { get; set; } public WorkItem? WorkItem { get; set; }
public Guid? WorkStatusId { get; set; }
[ForeignKey("WorkStatusId")]
[ValidateNever]
public WorkStatusMaster? WorkStatus { get; set; }
} }
} }

View File

@ -0,0 +1,9 @@
namespace Marco.Pms.Model.Activities
{
public class TaskAttachment
{
public Guid Id { get; set; }
public Guid ReferenceId { get; set; }
public Guid DocumentId { get; set; }
}
}

View File

@ -1,17 +0,0 @@
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
namespace Marco.Pms.Model.Activities
{
public class TaskImages
{
public Guid Id { get; set; }
public Guid TaskAllocationId { get; set; }
[ValidateNever]
[ForeignKey(nameof(TaskAllocationId))]
public TaskAllocation? TaskAllocation { get; set; }
public string? ImagePath { get; set; }
}
}

View File

@ -0,0 +1,12 @@
using Marco.Pms.Model.Utilities;
namespace Marco.Pms.Model.Master
{
public class WorkStatusMaster : TenantRelation
{
public Guid Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public bool IsSystem { get; set; } = false;
}
}

View File

@ -0,0 +1,13 @@
using Marco.Pms.Model.Utilities;
namespace Marco.Pms.Model.Dtos.Activities
{
public class ApproveTaskDto
{
public Guid Id { get; set; }
public Guid WorkStatus { get; set; }
public long ApprovedTask { get; set; }
public string? Comment { get; set; }
public List<FileUploadModel>? Images { get; set; }
}
}

View File

@ -3,6 +3,7 @@
public class AssignTaskDto public class AssignTaskDto
{ {
public DateTime AssignmentDate { get; set; } public DateTime AssignmentDate { get; set; }
public Guid? ParentTaskId { get; set; }
public double PlannedTask { get; set; } public double PlannedTask { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public List<Guid>? TaskTeam { get; set; } //Employee Ids public List<Guid>? TaskTeam { get; set; } //Employee Ids

View File

@ -1,11 +1,12 @@
namespace Marco.Pms.Model.Dtos.Activities using Marco.Pms.Model.Utilities;
namespace Marco.Pms.Model.Dtos.Activities
{ {
public class CreateCommentDto public class CreateCommentDto
{ {
public Guid TaskAllocationId { get; set; } public Guid TaskAllocationId { get; set; }
public DateTime CommentDate { get; set; } public DateTime CommentDate { get; set; }
public string? Comment { get; set; } public string? Comment { get; set; }
public List<FileUploadModel>? Images { get; set; }
} }
} }

View File

@ -0,0 +1,8 @@
namespace Marco.Pms.Model.Dtos.Master
{
public class CreateWorkStatusMasterDto
{
public string? Name { get; set; }
public string? Description { get; set; }
}
}

View File

@ -1,11 +1,15 @@
namespace Marco.Pms.Model.Dtos.Activities using Marco.Pms.Model.Utilities;
namespace Marco.Pms.Model.Dtos.Activities
{ {
public class ReportTaskDto public class ReportTaskDto
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid? ParentTaskId { get; set; }
public double CompletedTask { get; set; } public double CompletedTask { get; set; }
public DateTime ReportedDate { get; set; } public DateTime ReportedDate { get; set; }
public string? Comment { get; set; } public string? Comment { get; set; }
public List<ReportCheckListDto>? CheckList { get; set; } public List<ReportCheckListDto>? CheckList { get; set; }
public List<FileUploadModel>? Images { get; set; }
} }
} }

View File

@ -0,0 +1,9 @@
namespace Marco.Pms.Model.Dtos.Master
{
public class UpdateWorkStatusMasterDto
{
public Guid Id { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
}
}

View File

@ -8,7 +8,7 @@
public List<UpdateContactPhoneDto>? ContactPhones { get; set; } public List<UpdateContactPhoneDto>? ContactPhones { get; set; }
public List<UpdateContactEmailDto>? ContactEmails { get; set; } public List<UpdateContactEmailDto>? ContactEmails { get; set; }
public List<Guid>? BucketIds { get; set; } public List<Guid>? BucketIds { get; set; }
public Guid ContactCategoryId { get; set; } public Guid? ContactCategoryId { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public string? Organization { get; set; } public string? Organization { get; set; }
public string? Address { get; set; } public string? Address { get; set; }

View File

@ -11,5 +11,7 @@ namespace Marco.Pms.Model.Dtos.Project
public Guid ActivityID { get; set; } public Guid ActivityID { get; set; }
public int PlannedWork { get; set; } public int PlannedWork { get; set; }
public int CompletedWork { get; set; } public int CompletedWork { get; set; }
public Guid? ParentTaskId { get; set; }
public string? Comment { get; set; }
} }
} }

View File

@ -13,6 +13,7 @@ namespace Marco.Pms.Model.Mapper
return new TaskAllocation return new TaskAllocation
{ {
AssignmentDate = assignTask.AssignmentDate, AssignmentDate = assignTask.AssignmentDate,
ParentTaskId = assignTask.ParentTaskId,
PlannedTask = assignTask.PlannedTask, PlannedTask = assignTask.PlannedTask,
CompletedTask = 0, CompletedTask = 0,
Description = assignTask.Description, Description = assignTask.Description,
@ -43,18 +44,23 @@ namespace Marco.Pms.Model.Mapper
TenantId = tenantId TenantId = tenantId
}; };
} }
public static TaskVM TaskAllocationToTaskVM(this TaskAllocation taskAllocation, string employeeName) public static TaskVM TaskAllocationToTaskVM(this TaskAllocation taskAllocation)
{ {
return new TaskVM return new TaskVM
{ {
Id = taskAllocation.Id, Id = taskAllocation.Id,
AssignmentDate = taskAllocation.AssignmentDate, AssignmentDate = taskAllocation.AssignmentDate,
ReportedDate = taskAllocation.ReportedDate,
ApprovedDate = taskAllocation.ApprovedDate,
PlannedTask = taskAllocation.PlannedTask, PlannedTask = taskAllocation.PlannedTask,
CompletedTask = taskAllocation.CompletedTask, CompletedTask = taskAllocation.CompletedTask,
ReportedDate = taskAllocation.ReportedDate, NotApprovedTask = taskAllocation.ApprovedById == null ? taskAllocation.CompletedTask : (taskAllocation.CompletedTask - taskAllocation.ReportedTask),
Description = taskAllocation.Description, Description = taskAllocation.Description,
AssignBy = employeeName, AssignedBy = taskAllocation.Employee?.ToBasicEmployeeVMFromEmployee(),
WorkItem = taskAllocation.WorkItem ReportedBy = taskAllocation.ReportedBy?.ToBasicEmployeeVMFromEmployee(),
ApprovedBy = taskAllocation.ApprovedBy?.ToBasicEmployeeVMFromEmployee(),
WorkItem = taskAllocation.WorkItem,
WorkStatus = taskAllocation.WorkStatus
}; };
} }
public static AssignedTaskVM ToAssignTaskVMFromTaskAllocation(this TaskAllocation taskAllocation) public static AssignedTaskVM ToAssignTaskVMFromTaskAllocation(this TaskAllocation taskAllocation)
@ -100,11 +106,18 @@ namespace Marco.Pms.Model.Mapper
return new ListTaskVM return new ListTaskVM
{ {
Id = taskAllocation.Id, Id = taskAllocation.Id,
ParentTaskId = taskAllocation.ParentTaskId,
AssignmentDate = taskAllocation.AssignmentDate, AssignmentDate = taskAllocation.AssignmentDate,
ApprovedDate = taskAllocation.ApprovedDate,
Description = taskAllocation.Description,
PlannedTask = taskAllocation.PlannedTask, PlannedTask = taskAllocation.PlannedTask,
ReportedDate = taskAllocation.ReportedDate, ReportedDate = taskAllocation.ReportedDate,
WorkStatus = taskAllocation.WorkStatus,
CompletedTask = taskAllocation.CompletedTask, CompletedTask = taskAllocation.CompletedTask,
AssignedBy = taskAllocation.Employee != null ? taskAllocation.Employee.ToBasicEmployeeVMFromEmployee() : new BasicEmployeeVM(), NotApprovedTask = taskAllocation.ApprovedById == null ? taskAllocation.CompletedTask : (taskAllocation.CompletedTask - taskAllocation.ReportedTask),
AssignedBy = taskAllocation.Employee?.ToBasicEmployeeVMFromEmployee(),
ReportedBy = taskAllocation.ReportedBy?.ToBasicEmployeeVMFromEmployee(),
ApprovedBy = taskAllocation.ApprovedBy?.ToBasicEmployeeVMFromEmployee(),
WorkItemId = taskAllocation.WorkItemId, WorkItemId = taskAllocation.WorkItemId,
WorkItem = taskAllocation.WorkItem WorkItem = taskAllocation.WorkItem
}; };

View File

@ -59,7 +59,9 @@ namespace Marco.Pms.Model.Mapper
WorkCategoryId = model.WorkCategoryId, WorkCategoryId = model.WorkCategoryId,
TaskDate = DateTime.Now, TaskDate = DateTime.Now,
TenantId = tenantId, TenantId = tenantId,
WorkAreaId = model.WorkAreaID WorkAreaId = model.WorkAreaID,
ParentTaskId = model.ParentTaskId,
Description = model.Comment
}; };
} }

View File

@ -20,11 +20,17 @@ namespace Marco.Pms.Model.Projects
[ValidateNever] [ValidateNever]
public ActivityMaster? ActivityMaster { get; set; } public ActivityMaster? ActivityMaster { get; set; }
[ForeignKey("WorkCategoryId")] [ForeignKey("WorkCategoryId")]
[ValidateNever] [ValidateNever]
public WorkCategoryMaster? WorkCategoryMaster { get; set; } public WorkCategoryMaster? WorkCategoryMaster { get; set; }
public Guid? ParentTaskId { get; set; }
public double PlannedWork { get; set; } public double PlannedWork { get; set; }
public double CompletedWork { get; set; } public double CompletedWork { get; set; }
public string? Description { get; set; }
public DateTime TaskDate { get; set; } public DateTime TaskDate { get; set; }
} }
} }

View File

@ -8,5 +8,6 @@
public string? Comment { get; set; } public string? Comment { get; set; }
public Guid CommentedBy { get; set; } public Guid CommentedBy { get; set; }
public BasicEmployeeVM? Employee { get; set; } public BasicEmployeeVM? Employee { get; set; }
public List<string>? PreSignedUrls { get; set; }
} }
} }

View File

@ -1,16 +1,25 @@
using Marco.Pms.Model.Projects; using Marco.Pms.Model.Master;
using Marco.Pms.Model.Projects;
namespace Marco.Pms.Model.ViewModels.Activities namespace Marco.Pms.Model.ViewModels.Activities
{ {
public class ListTaskVM public class ListTaskVM
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid? ParentTaskId { get; set; }
public DateTime AssignmentDate { get; set; } public DateTime AssignmentDate { get; set; }
public DateTime? ReportedDate { get; set; } public DateTime? ReportedDate { get; set; }
public DateTime? ApprovedDate { get; set; }
public double PlannedTask { get; set; } public double PlannedTask { get; set; }
public double CompletedTask { get; set; } public double CompletedTask { get; set; }
public double NotApprovedTask { get; set; }
public BasicEmployeeVM? AssignedBy { get; set; } public BasicEmployeeVM? AssignedBy { get; set; }
public BasicEmployeeVM? ReportedBy { get; set; }
public BasicEmployeeVM? ApprovedBy { get; set; }
public WorkStatusMaster? WorkStatus { get; set; }
public string? Description { get; set; }
public Guid WorkItemId { get; set; } public Guid WorkItemId { get; set; }
public List<string>? ReportedPreSignedUrls { get; set; }
public WorkItem? WorkItem { get; set; } public WorkItem? WorkItem { get; set; }
public List<BasicEmployeeVM>? teamMembers { get; set; } public List<BasicEmployeeVM>? teamMembers { get; set; }
public List<CommentVM>? comments { get; set; } public List<CommentVM>? comments { get; set; }

View File

@ -10,7 +10,6 @@
public string? Description { get; set; } public string? Description { get; set; }
public Guid AssignedBy { get; set; } public Guid AssignedBy { get; set; }
public Guid WorkItemId { get; set; } public Guid WorkItemId { get; set; }
public List<CommentVM>? Comments { get; set; } public List<CommentVM>? Comments { get; set; }
public List<CheckListVM>? checkList { get; set; } public List<CheckListVM>? checkList { get; set; }
} }

View File

@ -1,5 +1,5 @@
using Marco.Pms.Model.Projects; using Marco.Pms.Model.Master;
using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.Projects;
namespace Marco.Pms.Model.ViewModels.Activities namespace Marco.Pms.Model.ViewModels.Activities
{ {
@ -7,13 +7,19 @@ namespace Marco.Pms.Model.ViewModels.Activities
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public DateTime AssignmentDate { get; set; } public DateTime AssignmentDate { get; set; }
public DateTime? ReportedDate { get; set; }
public DateTime? ApprovedDate { get; set; }
public double PlannedTask { get; set; } public double PlannedTask { get; set; }
public double CompletedTask { get; set; } public double CompletedTask { get; set; }
public DateTime? ReportedDate { get; set; } public double NotApprovedTask { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public string? AssignBy { get; set; } public BasicEmployeeVM? AssignedBy { get; set; }
public BasicEmployeeVM? ReportedBy { get; set; }
public BasicEmployeeVM? ApprovedBy { get; set; }
public WorkStatusMaster? WorkStatus { get; set; }
public WorkItem? WorkItem { get; set; } public WorkItem? WorkItem { get; set; }
public List<string>? PreSignedUrls { get; set; }
public List<CommentVM>? Comments { get; set; } public List<CommentVM>? Comments { get; set; }
public List<EmployeeVM>? TeamMembers { get; set; } public List<BasicEmployeeVM>? TeamMembers { get; set; }
} }
} }

View File

@ -0,0 +1,9 @@
namespace Marco.Pms.Model.ViewModels.DashBoard
{
public class AttendanceOverviewVM
{
public string? Role { get; set; }
public string? Date { get; set; }
public int Present { get; set; }
}
}

View File

@ -7,11 +7,13 @@ using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.Projects; using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.AttendanceVM; using Marco.Pms.Model.ViewModels.AttendanceVM;
using Marco.Pms.Services.Hubs;
using Marco.Pms.Services.Service; using Marco.Pms.Services.Service;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service; using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Document = Marco.Pms.Model.DocumentManager.Document; using Document = Marco.Pms.Model.DocumentManager.Document;
@ -30,10 +32,11 @@ namespace MarcoBMS.Services.Controllers
private readonly S3UploadService _s3Service; private readonly S3UploadService _s3Service;
private readonly PermissionServices _permission; private readonly PermissionServices _permission;
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly IHubContext<MarcoHub> _signalR;
public AttendanceController( public AttendanceController(
ApplicationDbContext context, EmployeeHelper employeeHelper, ProjectsHelper projectsHelper, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permission) ApplicationDbContext context, EmployeeHelper employeeHelper, ProjectsHelper projectsHelper, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permission, IHubContext<MarcoHub> signalR)
{ {
_context = context; _context = context;
_employeeHelper = employeeHelper; _employeeHelper = employeeHelper;
@ -42,6 +45,7 @@ namespace MarcoBMS.Services.Controllers
_s3Service = s3Service; _s3Service = s3Service;
_logger = logger; _logger = logger;
_permission = permission; _permission = permission;
_signalR = signalR;
} }
private Guid GetTenantId() private Guid GetTenantId()
@ -558,6 +562,13 @@ namespace MarcoBMS.Services.Controllers
Activity = attendance.Activity, Activity = attendance.Activity,
JobRoleName = employee.JobRole.Name JobRoleName = employee.JobRole.Name
}; };
var sendActivity = 0;
if (recordAttendanceDot.Id == Guid.Empty)
{
sendActivity = 1;
}
var notification = new { LoggedInUserId = currentEmployee.Id, Keyword = "Attendance", Activity = sendActivity, ProjectId = attendance.ProjectID, Response = vm };
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
_logger.LogInfo("Attendance for employee {FirstName} {LastName} has been marked", employee.FirstName ?? string.Empty, employee.LastName ?? string.Empty); _logger.LogInfo("Attendance for employee {FirstName} {LastName} has been marked", employee.FirstName ?? string.Empty, employee.LastName ?? string.Empty);
return Ok(ApiResponse<object>.SuccessResponse(vm, "Attendance marked successfully.", 200)); return Ok(ApiResponse<object>.SuccessResponse(vm, "Attendance marked successfully.", 200));
} }
@ -586,248 +597,185 @@ namespace MarcoBMS.Services.Controllers
{ {
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
var errors = ModelState.Values var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage).ToList();
.SelectMany(v => v.Errors) _logger.LogError("Invalid attendance model received.");
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogError("User sent Invalid Date while marking attendance");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
Guid TenantId = GetTenantId(); Guid tenantId = GetTenantId();
var currentEmployee = await _userHelper.GetCurrentEmployeeAsync();
using var transaction = await _context.Database.BeginTransactionAsync(); using var transaction = await _context.Database.BeginTransactionAsync();
try try
{ {
Attendance? attendance = await _context.Attendes.FirstOrDefaultAsync(a => a.Id == recordAttendanceDot.Id && a.TenantId == TenantId); ; // Validate mark time
if (recordAttendanceDot.MarkTime == null) if (recordAttendanceDot.MarkTime == null)
{ {
_logger.LogError("User sent Invalid Mark Time while marking attendance"); _logger.LogWarning("Null mark time provided.");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Mark Time", "Invalid Mark Time", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Mark Time", "Mark time is required", 400));
} }
DateTime finalDateTime = GetDateFromTimeStamp(recordAttendanceDot.Date, recordAttendanceDot.MarkTime); if (string.IsNullOrWhiteSpace(recordAttendanceDot.Comment))
if (recordAttendanceDot.Comment == null)
{ {
_logger.LogError("User sent Invalid comment while marking attendance"); _logger.LogWarning("Empty comment provided.");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Comment", "Invalid Comment", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid Comment", "Comment is required", 400));
} }
if (attendance != null) var finalDateTime = GetDateFromTimeStamp(recordAttendanceDot.Date, recordAttendanceDot.MarkTime);
var attendance = await _context.Attendes
.FirstOrDefaultAsync(a => a.Id == recordAttendanceDot.Id && a.TenantId == tenantId);
// Create or update attendance
if (attendance == null)
{ {
attendance.Comment = recordAttendanceDot.Comment; attendance = new Attendance
if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.CHECK_IN)
{ {
attendance.InTime = finalDateTime; TenantId = tenantId,
attendance.OutTime = null; AttendanceDate = recordAttendanceDot.Date,
attendance.Activity = ATTENDANCE_MARK_TYPE.CHECK_OUT; Comment = recordAttendanceDot.Comment,
} EmployeeID = recordAttendanceDot.EmployeeID,
else if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.CHECK_OUT) ProjectID = recordAttendanceDot.ProjectID,
{ Date = DateTime.UtcNow,
attendance.IsApproved = true; InTime = finalDateTime,
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE; Activity = ATTENDANCE_MARK_TYPE.CHECK_OUT
};
_context.Attendes.Add(attendance);
//string timeString = "10:30 PM"; // Format: "hh:mm tt"
attendance.OutTime = finalDateTime;
}
else if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE)
{
DateTime date = attendance.AttendanceDate;
finalDateTime = GetDateFromTimeStamp(date.Date, recordAttendanceDot.MarkTime);
if (attendance.InTime < finalDateTime)
{
attendance.OutTime = finalDateTime;
attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE;
}
else
{
_logger.LogError("Employee {EmployeeId} sent regularization request but it check-out time is earlier than check-out");
return BadRequest(ApiResponse<object>.ErrorResponse("Check-out time must be later than check-in time", "Check-out time must be later than check-in time", 400));
}
// do nothing
}
else if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.REGULARIZE)
{
attendance.IsApproved = true;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
// do nothing
}
else if (recordAttendanceDot.Action == ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT)
{
attendance.IsApproved = false;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT;
// do nothing
}
attendance.Date = DateTime.UtcNow;
// update code
_context.Attendes.Update(attendance);
} }
else else
{ {
attendance = new Attendance();
attendance.TenantId = TenantId;
attendance.AttendanceDate = recordAttendanceDot.Date;
// attendance.Activity = recordAttendanceDot.Action;
attendance.Comment = recordAttendanceDot.Comment; attendance.Comment = recordAttendanceDot.Comment;
attendance.EmployeeID = recordAttendanceDot.EmployeeID;
attendance.ProjectID = recordAttendanceDot.ProjectID;
attendance.Date = DateTime.UtcNow; attendance.Date = DateTime.UtcNow;
switch (recordAttendanceDot.Action)
{
case ATTENDANCE_MARK_TYPE.CHECK_IN:
attendance.InTime = finalDateTime;
attendance.OutTime = null;
attendance.Activity = ATTENDANCE_MARK_TYPE.CHECK_OUT;
break;
case ATTENDANCE_MARK_TYPE.CHECK_OUT:
attendance.OutTime = finalDateTime;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
attendance.IsApproved = true;
break;
case ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE:
DateTime date = attendance.InTime ?? recordAttendanceDot.Date;
finalDateTime = GetDateFromTimeStamp(date.Date, recordAttendanceDot.MarkTime);
if (attendance.InTime < finalDateTime)
{
attendance.OutTime = finalDateTime;
attendance.Activity = ATTENDANCE_MARK_TYPE.REQUEST_REGULARIZE;
}
else
{
_logger.LogWarning("Regularization check-out time is before check-in.");
return BadRequest(ApiResponse<object>.ErrorResponse("Check-out time must be later than check-in time", "Invalid regularization", 400));
}
break;
case ATTENDANCE_MARK_TYPE.REGULARIZE:
attendance.IsApproved = true;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE;
break;
case ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT:
attendance.IsApproved = false;
attendance.Activity = ATTENDANCE_MARK_TYPE.REGULARIZE_REJECT;
break;
}
_context.Attendes.Update(attendance);
attendance.InTime = finalDateTime;
attendance.OutTime = null;
attendance.Activity = ATTENDANCE_MARK_TYPE.CHECK_OUT;
_context.Attendes.Add(attendance);
} }
// Upload image if present
Document? document = null; Document? document = null;
var Image = recordAttendanceDot.Image; string? preSignedUrl = null;
var objectKey = string.Empty;
var preSignedUrl = string.Empty;
if (Image != null && Image.ContentType != null) if (recordAttendanceDot.Image != null && recordAttendanceDot.Image.ContentType != null)
{ {
string base64 = recordAttendanceDot.Image.Base64Data?.Split(',').LastOrDefault() ?? "";
if (string.IsNullOrWhiteSpace(base64))
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Image data missing", 400));
if (string.IsNullOrEmpty(Image.Base64Data)) var fileType = _s3Service.GetContentTypeFromBase64(base64);
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); var fileName = _s3Service.GenerateFileName(fileType, tenantId, "attendance");
var objectKey = $"tenant-{tenantId}/Employee/{recordAttendanceDot.EmployeeID}/Attendance/{fileName}";
objectKey = await _s3Service.UploadFileAsync(Image.Base64Data, TenantId, "attendance"); await _s3Service.UploadFileAsync(base64, fileType, objectKey);
preSignedUrl = _s3Service.GeneratePreSignedUrlAsync(objectKey); preSignedUrl = _s3Service.GeneratePreSignedUrlAsync(objectKey);
document = new Document document = new Document
{ {
FileName = Image.FileName ?? "", FileName = recordAttendanceDot.Image.FileName ?? "",
ContentType = Image.ContentType, ContentType = recordAttendanceDot.Image.ContentType,
S3Key = objectKey, S3Key = objectKey,
Base64Data = Image.Base64Data, Base64Data = recordAttendanceDot.Image.Base64Data,
FileSize = Image.FileSize, FileSize = recordAttendanceDot.Image.FileSize,
UploadedAt = recordAttendanceDot.Date, UploadedAt = recordAttendanceDot.Date,
TenantId = TenantId TenantId = tenantId
}; };
_context.Documents.Add(document); _context.Documents.Add(document);
await _context.SaveChangesAsync();
} }
// Log attendance
var attendanceLog = new AttendanceLog
// Step 3: Always insert a new log entry
if (document != null)
{ {
var attendanceLog = new AttendanceLog AttendanceId = attendance.Id,
{ Activity = attendance.Activity,
AttendanceId = attendance.Id, // Use existing or new AttendanceId ActivityTime = finalDateTime,
Activity = attendance.Activity, Comment = recordAttendanceDot.Comment,
EmployeeID = recordAttendanceDot.EmployeeID,
Latitude = recordAttendanceDot.Latitude,
Longitude = recordAttendanceDot.Longitude,
DocumentId = document?.Id,
TenantId = tenantId,
UpdatedBy = recordAttendanceDot.EmployeeID,
UpdatedOn = recordAttendanceDot.Date
};
_context.AttendanceLogs.Add(attendanceLog);
ActivityTime = finalDateTime,
Comment = recordAttendanceDot.Comment,
EmployeeID = recordAttendanceDot.EmployeeID,
Latitude = recordAttendanceDot.Latitude,
Longitude = recordAttendanceDot.Longitude,
DocumentId = document.Id,
TenantId = TenantId,
UpdatedBy = recordAttendanceDot.EmployeeID,
UpdatedOn = recordAttendanceDot.Date
};
_context.AttendanceLogs.Add(attendanceLog);
}
else
{
var attendanceLog = new AttendanceLog
{
AttendanceId = attendance.Id, // Use existing or new AttendanceId
Activity = attendance.Activity,
ActivityTime = finalDateTime,
Comment = recordAttendanceDot.Comment,
EmployeeID = recordAttendanceDot.EmployeeID,
Latitude = recordAttendanceDot.Latitude,
Longitude = recordAttendanceDot.Longitude,
DocumentId = document != null ? document.Id : null,
TenantId = TenantId,
UpdatedBy = recordAttendanceDot.EmployeeID,
UpdatedOn = recordAttendanceDot.Date
};
_context.AttendanceLogs.Add(attendanceLog);
}
//if (recordAttendanceDot.Image != null && recordAttendanceDot.Image.Count > 0)
//{
// attendanceLog.Photo = recordAttendanceDot.Image[0].Base64Data;
//}
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
await transaction.CommitAsync();
await transaction.CommitAsync(); // Commit transaction // Construct view model
var employee = await _employeeHelper.GetEmployeeByID(recordAttendanceDot.EmployeeID);
Employee employee = await _employeeHelper.GetEmployeeByID(recordAttendanceDot.EmployeeID); var vm = new EmployeeAttendanceVM
if (employee.JobRole != null)
{ {
EmployeeAttendanceVM vm = new EmployeeAttendanceVM(); Id = attendance.Id,
if (document != null) EmployeeId = employee.Id,
{ FirstName = employee.FirstName,
vm = new EmployeeAttendanceVM() LastName = employee.LastName,
{ CheckInTime = attendance.InTime,
CheckInTime = attendance.InTime, CheckOutTime = attendance.OutTime,
CheckOutTime = attendance.OutTime, Activity = attendance.Activity,
EmployeeAvatar = null, JobRoleName = employee.JobRole?.Name,
EmployeeId = recordAttendanceDot.EmployeeID, DocumentId = document?.Id ?? Guid.Empty,
FirstName = employee.FirstName, ThumbPreSignedUrl = preSignedUrl ?? "",
LastName = employee.LastName, PreSignedUrl = preSignedUrl ?? ""
Id = attendance.Id, };
Activity = attendance.Activity,
JobRoleName = employee.JobRole.Name,
DocumentId = document.Id,
ThumbPreSignedUrl = preSignedUrl,
PreSignedUrl = preSignedUrl
};
}
else
{
vm = new EmployeeAttendanceVM()
{
CheckInTime = attendance.InTime,
CheckOutTime = attendance.OutTime,
EmployeeAvatar = null,
EmployeeId = recordAttendanceDot.EmployeeID,
FirstName = employee.FirstName,
LastName = employee.LastName,
Id = attendance.Id,
Activity = attendance.Activity,
JobRoleName = employee.JobRole.Name,
DocumentId = Guid.Empty,
ThumbPreSignedUrl = string.Empty,
PreSignedUrl = string.Empty
};
}
_logger.LogInfo("Attendance for employee {FirstName} {LastName} has been marked", employee.FirstName ?? string.Empty, employee.LastName ?? string.Empty); var notification = new
return Ok(ApiResponse<object>.SuccessResponse(vm, "Attendance marked successfully.", 200)); {
} LoggedInUserId = currentEmployee.Id,
_logger.LogInfo("Attendance for employee {FirstName} {LastName} has been marked", employee.FirstName ?? string.Empty, employee.LastName ?? string.Empty); Keyword = "Attendance",
return Ok(ApiResponse<object>.SuccessResponse(new EmployeeAttendanceVM(), "Attendance marked successfully.", 200)); Activity = recordAttendanceDot.Id == Guid.Empty ? 1 : 0,
ProjectId = attendance.ProjectID,
Response = vm
};
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
_logger.LogInfo("Attendance recorded for employee: {FullName}", $"{employee.FirstName} {employee.LastName}");
return Ok(ApiResponse<object>.SuccessResponse(vm, "Attendance marked successfully.", 200));
} }
catch (Exception ex) catch (Exception ex)
{ {
await transaction.RollbackAsync(); // Rollback on failure await transaction.RollbackAsync();
_logger.LogError("{Error} while marking attendance", ex.Message); _logger.LogError("Error while recording attendance : {Error}", ex.Message);
var response = new return BadRequest(ApiResponse<object>.ErrorResponse("Something went wrong", ex.Message, 500));
{
message = ex.Message,
detail = ex.StackTrace,
statusCode = StatusCodes.Status500InternalServerError
};
return BadRequest(ApiResponse<object>.ErrorResponse(ex.Message, response, 400));
} }
} }
private static DateTime GetDateFromTimeStamp(DateTime date, string timeString) private static DateTime GetDateFromTimeStamp(DateTime date, string timeString)
{ {
//DateTime date = recordAttendanceDot.Date; //DateTime date = recordAttendanceDot.Date;

View File

@ -5,6 +5,7 @@ using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Projects; using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.DashBoard; using Marco.Pms.Model.ViewModels.DashBoard;
using Marco.Pms.Services.Service;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service; using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -21,11 +22,13 @@ namespace Marco.Pms.Services.Controllers
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly UserHelper _userHelper; private readonly UserHelper _userHelper;
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
public DashboardController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger) private readonly PermissionServices _permissionServices;
public DashboardController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, PermissionServices permissionServices)
{ {
_context = context; _context = context;
_userHelper = userHelper; _userHelper = userHelper;
_logger = logger; _logger = logger;
_permissionServices = permissionServices;
} }
[HttpGet("progression")] [HttpGet("progression")]
public async Task<IActionResult> GetGraph([FromQuery] double days, [FromQuery] string FromDate, [FromQuery] Guid? projectId) public async Task<IActionResult> GetGraph([FromQuery] double days, [FromQuery] string FromDate, [FromQuery] Guid? projectId)
@ -354,5 +357,102 @@ namespace Marco.Pms.Services.Controllers
_logger.LogInfo($"Record of performed activities for project {projectId} for date {currentDate.Date} by employee {LoggedInEmployee.Id}"); _logger.LogInfo($"Record of performed activities for project {projectId} for date {currentDate.Date} by employee {LoggedInEmployee.Id}");
return Ok(ApiResponse<object>.SuccessResponse(report, $"Record of performed activities for project {project.Name} for date {currentDate.Date}", 200)); return Ok(ApiResponse<object>.SuccessResponse(report, $"Record of performed activities for project {project.Name} for date {currentDate.Date}", 200));
} }
[HttpGet("attendance-overview/{projectId}")]
public async Task<IActionResult> GetAttendanceOverView(Guid projectId, [FromQuery] string days)
{
_logger.LogInfo("GetAttendanceOverView called for ProjectId: {ProjectId}, Days: {Days}", projectId, days);
// Step 1: Validate project existence
var project = await _context.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Id == projectId);
if (project == null)
{
_logger.LogWarning("Project not found for ProjectId: {ProjectId}", projectId);
return BadRequest(ApiResponse<object>.ErrorResponse("Project not found", "Project not found", 400));
}
// Step 2: Permission check
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
bool hasAssigned = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.ToString());
if (!hasAssigned)
{
_logger.LogWarning("Unauthorized access attempt. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}", loggedInEmployee.Id, projectId);
return StatusCode(403, ApiResponse<object>.ErrorResponse(
"You don't have permission to access this feature",
"You don't have permission to access this feature", 403));
}
// Step 3: Validate and parse days input
if (!int.TryParse(days, out int dayCount) || dayCount <= 0)
{
_logger.LogWarning("Invalid days input received: {Days}", days);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid number of days", "Days must be a positive integer", 400));
}
// Step 4: Define date range
DateTime today = DateTime.UtcNow.Date;
DateTime startDate = today.AddDays(-dayCount);
// Step 5: Load project allocations and related job roles
var allocations = await _context.ProjectAllocations
.Where(pa => pa.ProjectId == projectId)
.ToListAsync();
if (!allocations.Any())
{
_logger.LogInfo("No employee allocations found for project: {ProjectId}", projectId);
return Ok(ApiResponse<object>.SuccessResponse(new List<AttendanceOverviewVM>(), "No allocations found", 200));
}
var jobRoleIds = allocations.Select(pa => pa.JobRoleId).Distinct().ToList();
var jobRoles = await _context.JobRoles
.Where(jr => jobRoleIds.Contains(jr.Id))
.ToListAsync();
// Step 6: Load attendance records for given date range
var attendances = await _context.Attendes
.Where(a =>
a.ProjectID == projectId &&
a.InTime.HasValue &&
a.InTime.Value.Date >= startDate &&
a.InTime.Value.Date <= today)
.ToListAsync();
var overviewList = new List<AttendanceOverviewVM>();
// Step 7: Process attendance per date per role
for (DateTime date = today; date > startDate; date = date.AddDays(-1))
{
foreach (var jobRole in jobRoles)
{
var employeeIds = allocations
.Where(pa => pa.JobRoleId == jobRole.Id)
.Select(pa => pa.EmployeeId)
.ToList();
int presentCount = attendances
.Count(a => employeeIds.Contains(a.EmployeeID) && a.InTime!.Value.Date == date);
overviewList.Add(new AttendanceOverviewVM
{
Role = jobRole.Name,
Date = date.ToString("yyyy-MM-dd"),
Present = presentCount
});
}
}
// Step 8: Order result for consistent presentation
var sortedResult = overviewList
.OrderByDescending(r => r.Date)
.ThenByDescending(r => r.Present)
.ToList();
_logger.LogInfo("Attendance overview fetched. ProjectId: {ProjectId}, Records: {Count}", projectId, sortedResult.Count);
return Ok(ApiResponse<object>.SuccessResponse(sortedResult, $"{sortedResult.Count} records fetched for attendance overview", 200));
}
} }
} }

View File

@ -9,11 +9,13 @@ using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.Projects; using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Employee;
using Marco.Pms.Services.Hubs;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service; using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace MarcoBMS.Services.Controllers namespace MarcoBMS.Services.Controllers
@ -32,9 +34,11 @@ namespace MarcoBMS.Services.Controllers
private readonly UserHelper _userHelper; private readonly UserHelper _userHelper;
private readonly IConfiguration _configuration; private readonly IConfiguration _configuration;
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly IHubContext<MarcoHub> _signalR;
public EmployeeController(UserManager<ApplicationUser> userManager, IEmailSender emailSender, public EmployeeController(UserManager<ApplicationUser> userManager, IEmailSender emailSender,
ApplicationDbContext context, EmployeeHelper employeeHelper, UserHelper userHelper, IConfiguration configuration, ILoggingService logger) ApplicationDbContext context, EmployeeHelper employeeHelper, UserHelper userHelper, IConfiguration configuration, ILoggingService logger,
IHubContext<MarcoHub> signalR)
{ {
_context = context; _context = context;
_userManager = userManager; _userManager = userManager;
@ -43,6 +47,7 @@ namespace MarcoBMS.Services.Controllers
_userHelper = userHelper; _userHelper = userHelper;
_configuration = configuration; _configuration = configuration;
_logger = logger; _logger = logger;
_signalR = signalR;
} }
[HttpGet] [HttpGet]
@ -154,6 +159,8 @@ namespace MarcoBMS.Services.Controllers
public async Task<IActionResult> CreateUser([FromBody] CreateUserDto model) public async Task<IActionResult> CreateUser([FromBody] CreateUserDto model)
{ {
Guid tenantId = _userHelper.GetTenantId(); Guid tenantId = _userHelper.GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
Guid employeeId = Guid.Empty;
if (model == null) if (model == null)
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", "Invaild Data", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", "Invaild Data", 400));
@ -180,6 +187,7 @@ namespace MarcoBMS.Services.Controllers
_context.Employees.Update(existingEmployee); _context.Employees.Update(existingEmployee);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
employeeId = existingEmployee.Id;
responsemessage = "User updated successfully."; responsemessage = "User updated successfully.";
} }
else else
@ -214,7 +222,7 @@ namespace MarcoBMS.Services.Controllers
_context.Employees.Add(newEmployee); _context.Employees.Add(newEmployee);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
employeeId = newEmployee.Id;
/* SEND USER REGISTRATION MAIL*/ /* SEND USER REGISTRATION MAIL*/
var token = await _userManager.GeneratePasswordResetTokenAsync(user); var token = await _userManager.GeneratePasswordResetTokenAsync(user);
@ -233,6 +241,7 @@ namespace MarcoBMS.Services.Controllers
_context.Employees.Update(existingEmployee); _context.Employees.Update(existingEmployee);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
employeeId = existingEmployee.Id;
/* SEND USER REGISTRATION MAIL*/ /* SEND USER REGISTRATION MAIL*/
var token = await _userManager.GeneratePasswordResetTokenAsync(user); var token = await _userManager.GeneratePasswordResetTokenAsync(user);
@ -256,17 +265,22 @@ namespace MarcoBMS.Services.Controllers
existingEmployee = GetUpdateEmployeeModel(model, existingEmployee); existingEmployee = GetUpdateEmployeeModel(model, existingEmployee);
_context.Employees.Update(existingEmployee); _context.Employees.Update(existingEmployee);
responsemessage = "User updated successfully."; responsemessage = "User updated successfully.";
employeeId = existingEmployee.Id;
} }
else else
{ {
// Create Employee record if missing // Create Employee record if missing
Employee newEmployee = GetNewEmployeeModel(model, tenantId, string.Empty); Employee newEmployee = GetNewEmployeeModel(model, tenantId, string.Empty);
_context.Employees.Add(newEmployee); _context.Employees.Add(newEmployee);
employeeId = newEmployee.Id;
} }
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
responsemessage = "User created successfully."; responsemessage = "User created successfully.";
} }
var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Employee", EmployeeId = employeeId };
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
return Ok(ApiResponse<object>.SuccessResponse("Success.", responsemessage, 200)); return Ok(ApiResponse<object>.SuccessResponse("Success.", responsemessage, 200));
} }
@ -420,6 +434,9 @@ namespace MarcoBMS.Services.Controllers
} }
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
_logger.LogInfo("Employee with ID {EmployeId} Deleted successfully", employee.Id); _logger.LogInfo("Employee with ID {EmployeId} Deleted successfully", employee.Id);
var notification = new { LoggedInUserId = LoggedEmployee.Id, Keyword = "Employee", EmployeeId = employee.Id };
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
} }
} }
else else

View File

@ -68,7 +68,16 @@ namespace Marco.Pms.Services.Controllers
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
} }
var objectKey = await _s3Service.UploadFileAsync(Image.Base64Data, tenantId, "forum"); //If base64 has a data URI prefix, strip it
var base64 = Image.Base64Data.Contains(",")
? Image.Base64Data.Substring(Image.Base64Data.IndexOf(",") + 1)
: Image.Base64Data;
string fileType = _s3Service.GetContentTypeFromBase64(base64);
string fileName = _s3Service.GenerateFileName(fileType, tenantId, "forum");
string objectKey = $"tenant-{tenantId}/project-{createTicketDto.LinkedProjectId}/froum/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey);
Document document = attachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, createTicketDto.CreatedAt, tenantId); Document document = attachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, createTicketDto.CreatedAt, tenantId);
_context.Documents.Add(document); _context.Documents.Add(document);
@ -182,7 +191,16 @@ namespace Marco.Pms.Services.Controllers
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
} }
var objectKey = await _s3Service.UploadFileAsync(Image.Base64Data, tenantId, "forum"); //If base64 has a data URI prefix, strip it
var base64 = Image.Base64Data.Contains(",")
? Image.Base64Data.Substring(Image.Base64Data.IndexOf(",") + 1)
: Image.Base64Data;
string fileType = _s3Service.GetContentTypeFromBase64(base64);
string fileName = _s3Service.GenerateFileName(fileType, tenantId, "forum");
string objectKey = $"tenant-{tenantId}/project-{updateTicketDto.LinkedProjectId}/froum/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey);
Document document = attachmentDto.ToDocumentFromUpdateAttachmentDto(objectKey, objectKey, updateTicketDto.CreatedAt, tenantId); Document document = attachmentDto.ToDocumentFromUpdateAttachmentDto(objectKey, objectKey, updateTicketDto.CreatedAt, tenantId);
_context.Documents.Add(document); _context.Documents.Add(document);
@ -329,6 +347,14 @@ namespace Marco.Pms.Services.Controllers
List<TicketAttachment> attachments = new List<TicketAttachment>(); List<TicketAttachment> attachments = new List<TicketAttachment>();
List<Document> documents = new List<Document>(); List<Document> documents = new List<Document>();
TicketForum? ticket = await _context.Tickets.FirstOrDefaultAsync(t => t.Id == addCommentDto.TicketId);
if (ticket == null)
{
_logger.LogError("Ticket {TicketId} not Found in database", addCommentDto.TicketId);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket not Found", "Ticket not Found", 404));
}
TicketComment comment = addCommentDto.ToTicketCommentFromAddCommentDto(tenantId); TicketComment comment = addCommentDto.ToTicketCommentFromAddCommentDto(tenantId);
_context.TicketComments.Add(comment); _context.TicketComments.Add(comment);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
@ -344,7 +370,16 @@ namespace Marco.Pms.Services.Controllers
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
} }
var objectKey = await _s3Service.UploadFileAsync(Image.Base64Data, tenantId, "forum"); //If base64 has a data URI prefix, strip it
var base64 = Image.Base64Data.Contains(",")
? Image.Base64Data.Substring(Image.Base64Data.IndexOf(",") + 1)
: Image.Base64Data;
string fileType = _s3Service.GetContentTypeFromBase64(base64);
string fileName = _s3Service.GenerateFileName(fileType, tenantId, "forum");
string objectKey = $"tenant-{tenantId}/project-{ticket.LinkedProjectId}/froum/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey);
Document document = attachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, addCommentDto.SentAt, tenantId); Document document = attachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, addCommentDto.SentAt, tenantId);
_context.Documents.Add(document); _context.Documents.Add(document);
@ -396,6 +431,14 @@ namespace Marco.Pms.Services.Controllers
Guid tenantId = _userHelper.GetTenantId(); Guid tenantId = _userHelper.GetTenantId();
List<TicketAttachment> attachments = new List<TicketAttachment>(); List<TicketAttachment> attachments = new List<TicketAttachment>();
TicketForum? ticket = await _context.Tickets.FirstOrDefaultAsync(t => t.Id == updateCommentDto.TicketId);
if (ticket == null)
{
_logger.LogError("Ticket {TicketId} not Found in database", updateCommentDto.TicketId);
return NotFound(ApiResponse<object>.ErrorResponse("Ticket not Found", "Ticket not Found", 404));
}
TicketComment existingComment = await _context.TicketComments.AsNoTracking().FirstOrDefaultAsync(c => c.Id == updateCommentDto.Id) ?? new TicketComment(); TicketComment existingComment = await _context.TicketComments.AsNoTracking().FirstOrDefaultAsync(c => c.Id == updateCommentDto.Id) ?? new TicketComment();
TicketComment updateComment = updateCommentDto.ToTicketCommentFromUpdateCommentDto(tenantId, existingComment); TicketComment updateComment = updateCommentDto.ToTicketCommentFromUpdateCommentDto(tenantId, existingComment);
_context.TicketComments.Update(updateComment); _context.TicketComments.Update(updateComment);
@ -419,7 +462,16 @@ namespace Marco.Pms.Services.Controllers
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
} }
var objectKey = await _s3Service.UploadFileAsync(Image.Base64Data, tenantId, "forum"); //If base64 has a data URI prefix, strip it
var base64 = Image.Base64Data.Contains(",")
? Image.Base64Data.Substring(Image.Base64Data.IndexOf(",") + 1)
: Image.Base64Data;
string fileType = _s3Service.GetContentTypeFromBase64(base64);
string fileName = _s3Service.GenerateFileName(fileType, tenantId, "forum");
string objectKey = $"tenant-{tenantId}/project-{ticket.LinkedProjectId}/froum/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey);
Document document = attachmentDto.ToDocumentFromUpdateAttachmentDto(objectKey, objectKey, existingComment.SentAt, tenantId); Document document = attachmentDto.ToDocumentFromUpdateAttachmentDto(objectKey, objectKey, existingComment.SentAt, tenantId);
_context.Documents.Add(document); _context.Documents.Add(document);
@ -491,6 +543,16 @@ namespace Marco.Pms.Services.Controllers
Guid tenantId = _userHelper.GetTenantId(); Guid tenantId = _userHelper.GetTenantId();
List<TicketAttachmentVM> ticketAttachmentVMs = new List<TicketAttachmentVM>(); List<TicketAttachmentVM> ticketAttachmentVMs = new List<TicketAttachmentVM>();
List<Guid> ticketIds = forumAttachmentDtos.Select(f => f.TicketId.HasValue ? f.TicketId.Value : Guid.Empty).ToList();
List<TicketForum> tickets = await _context.Tickets.Where(t => ticketIds.Contains(t.Id)).ToListAsync();
if (tickets == null || tickets.Count > 0)
{
_logger.LogError("Tickets not Found in database");
return NotFound(ApiResponse<object>.ErrorResponse("Ticket not Found", "Ticket not Found", 404));
}
TicketAttachment attachment = new TicketAttachment(); TicketAttachment attachment = new TicketAttachment();
foreach (var forumAttachmentDto in forumAttachmentDtos) foreach (var forumAttachmentDto in forumAttachmentDtos)
@ -505,7 +567,17 @@ namespace Marco.Pms.Services.Controllers
_logger.LogError("ticket ID is missing"); _logger.LogError("ticket ID is missing");
return BadRequest(ApiResponse<object>.ErrorResponse("ticket ID is missing", "ticket ID is missing", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("ticket ID is missing", "ticket ID is missing", 400));
} }
var objectKey = await _s3Service.UploadFileAsync(forumAttachmentDto.Base64Data, tenantId, "forum"); var ticket = tickets.FirstOrDefault(t => t.Id == forumAttachmentDto.TicketId);
//If base64 has a data URI prefix, strip it
var base64 = forumAttachmentDto.Base64Data.Contains(",")
? forumAttachmentDto.Base64Data.Substring(forumAttachmentDto.Base64Data.IndexOf(",") + 1)
: forumAttachmentDto.Base64Data;
string fileType = _s3Service.GetContentTypeFromBase64(base64);
string fileName = _s3Service.GenerateFileName(fileType, tenantId, "forum");
string objectKey = $"tenant-{tenantId}/project-{ticket?.LinkedProjectId}/froum/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey);
Document document = forumAttachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, forumAttachmentDto.SentAt, tenantId); Document document = forumAttachmentDto.ToDocumentFromForumAttachmentDto(objectKey, objectKey, forumAttachmentDto.SentAt, tenantId);
_context.Documents.Add(document); _context.Documents.Add(document);

View File

@ -671,6 +671,45 @@ namespace Marco.Pms.Services.Controllers
} }
} }
// -------------------------------- Work Status --------------------------------
[HttpGet("work-status")]
public async Task<IActionResult> GetWorkStatusMasterList()
{
var response = await _masterHelper.GetWorkStatusList();
return StatusCode(response.StatusCode, response);
}
[HttpPost("work-status")]
public async Task<IActionResult> CreateWorkStatusMaster([FromBody] CreateWorkStatusMasterDto createWorkStatusDto)
{
if (!ModelState.IsValid)
{
var errors = ModelState.Values
.SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage)
.ToList();
_logger.LogError("User sent Invalid Date while marking attendance");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
}
var response = await _masterHelper.CreateWorkStatus(createWorkStatusDto);
return StatusCode(response.StatusCode, response);
}
[HttpPost("work-status/edit/{id}")]
public async Task<IActionResult> UpdateWorkStatusMaster(Guid id, [FromBody] UpdateWorkStatusMasterDto updateWorkStatusDto)
{
var response = await _masterHelper.UpdateWorkStatus(id, updateWorkStatusDto);
return StatusCode(response.StatusCode, response);
}
[HttpDelete("work-status/{id}")]
public async Task<IActionResult> DeleteWorkStatusMaster(Guid id)
{
var response = await _masterHelper.DeleteWorkStatus(id);
return StatusCode(response.StatusCode, response);
}
// -------------------------------- Contact Category -------------------------------- // -------------------------------- Contact Category --------------------------------
[HttpGet("contact-categories")] [HttpGet("contact-categories")]
@ -749,11 +788,11 @@ namespace Marco.Pms.Services.Controllers
return Ok(response); return Ok(response);
} }
[HttpGet("contact-tag/{id}")] //[HttpGet("contact-tag/{id}")]
public async Task<IActionResult> GetContactTagMaster(Guid id) //public async Task<IActionResult> GetContactTagMaster(Guid id)
{ //{
return Ok(); // return Ok();
} //}
[HttpPost("contact-tag")] [HttpPost("contact-tag")]
public async Task<IActionResult> CreateContactTagMaster([FromBody] CreateContactTagDto contactTagDto) public async Task<IActionResult> CreateContactTagMaster([FromBody] CreateContactTagDto contactTagDto)

View File

@ -8,12 +8,13 @@ using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Model.ViewModels.Employee;
using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Model.ViewModels.Projects;
using Marco.Pms.Services.Hubs;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service; using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace MarcoBMS.Services.Controllers namespace MarcoBMS.Services.Controllers
{ {
@ -27,15 +28,18 @@ namespace MarcoBMS.Services.Controllers
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly RolesHelper _rolesHelper; private readonly RolesHelper _rolesHelper;
private readonly ProjectsHelper _projectsHelper; private readonly ProjectsHelper _projectsHelper;
private readonly IHubContext<MarcoHub> _signalR;
public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper) public ProjectController(ApplicationDbContext context, UserHelper userHelper, ILoggingService logger, RolesHelper rolesHelper, ProjectsHelper projectHelper, IHubContext<MarcoHub> signalR)
{ {
_context = context; _context = context;
_userHelper = userHelper; _userHelper = userHelper;
_logger = logger; _logger = logger;
_rolesHelper = rolesHelper; _rolesHelper = rolesHelper;
_projectsHelper = projectHelper; _projectsHelper = projectHelper;
_signalR = signalR;
} }
[HttpGet("list/basic")] [HttpGet("list/basic")]
@ -59,9 +63,9 @@ namespace MarcoBMS.Services.Controllers
return Unauthorized(ApiResponse<object>.ErrorResponse("Employee not found.", null, 401)); return Unauthorized(ApiResponse<object>.ErrorResponse("Employee not found.", null, 401));
} }
List<Project> projects = await _projectsHelper.GetMyProjects(tenantId, LoggedInEmployee); List<Project> projects = await _projectsHelper.GetMyProjects(tenantId, LoggedInEmployee);
// 4. Project projection to ProjectInfoVM // 4. Project projection to ProjectInfoVM
// This part is already quite efficient. // This part is already quite efficient.
@ -84,8 +88,6 @@ namespace MarcoBMS.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(response, "Success.", 200)); return Ok(ApiResponse<object>.SuccessResponse(response, "Success.", 200));
} }
[HttpGet("list")] [HttpGet("list")]
public async Task<IActionResult> GetAll() public async Task<IActionResult> GetAll()
{ {
@ -314,6 +316,7 @@ namespace MarcoBMS.Services.Controllers
[HttpPost] [HttpPost]
public async Task<IActionResult> Create([FromBody] CreateProjectDto projectDto) public async Task<IActionResult> Create([FromBody] CreateProjectDto projectDto)
{ {
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
var errors = ModelState.Values var errors = ModelState.Values
@ -330,6 +333,9 @@ namespace MarcoBMS.Services.Controllers
_context.Projects.Add(project); _context.Projects.Add(project);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Create_Project", Response = project.ToProjectDto() };
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
return Ok(ApiResponse<object>.SuccessResponse(project.ToProjectDto(), "Success.", 200)); return Ok(ApiResponse<object>.SuccessResponse(project.ToProjectDto(), "Success.", 200));
} }
@ -338,6 +344,7 @@ namespace MarcoBMS.Services.Controllers
[Route("update/{id}")] [Route("update/{id}")]
public async Task<IActionResult> Update([FromRoute] Guid id, [FromBody] UpdateProjectDto updateProjectDto) public async Task<IActionResult> Update([FromRoute] Guid id, [FromBody] UpdateProjectDto updateProjectDto)
{ {
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
var errors = ModelState.Values var errors = ModelState.Values
@ -356,6 +363,10 @@ namespace MarcoBMS.Services.Controllers
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Update_Project", Response = project.ToProjectDto() };
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
return Ok(ApiResponse<object>.SuccessResponse(project.ToProjectDto(), "Success.", 200)); return Ok(ApiResponse<object>.SuccessResponse(project.ToProjectDto(), "Success.", 200));
} }
@ -365,7 +376,6 @@ namespace MarcoBMS.Services.Controllers
} }
} }
//[HttpPost("assign-employee")] //[HttpPost("assign-employee")]
//public async Task<IActionResult> AssignEmployee(int? allocationid, int employeeId, int projectId) //public async Task<IActionResult> AssignEmployee(int? allocationid, int employeeId, int projectId)
//{ //{
@ -506,7 +516,11 @@ namespace MarcoBMS.Services.Controllers
if (projectAllocationDot != null) if (projectAllocationDot != null)
{ {
Guid TenentID = GetTenantId(); Guid TenentID = GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
List<object>? result = new List<object>(); List<object>? result = new List<object>();
List<Guid> employeeIds = new List<Guid>();
List<Guid> projectIds = new List<Guid>();
foreach (var item in projectAllocationDot) foreach (var item in projectAllocationDot)
{ {
@ -535,6 +549,9 @@ namespace MarcoBMS.Services.Controllers
projectAllocationFromDb.IsActive = false; projectAllocationFromDb.IsActive = false;
_context.Entry(projectAllocationFromDb).Property(e => e.ReAllocationDate).IsModified = true; _context.Entry(projectAllocationFromDb).Property(e => e.ReAllocationDate).IsModified = true;
_context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true;
employeeIds.Add(projectAllocation.EmployeeId);
projectIds.Add(projectAllocation.ProjectId);
} }
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var result1 = new var result1 = new
@ -556,6 +573,9 @@ namespace MarcoBMS.Services.Controllers
projectAllocation.IsActive = true; projectAllocation.IsActive = true;
_context.ProjectAllocations.Add(projectAllocation); _context.ProjectAllocations.Add(projectAllocation);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
employeeIds.Add(projectAllocation.EmployeeId);
projectIds.Add(projectAllocation.ProjectId);
} }
} }
@ -564,7 +584,9 @@ namespace MarcoBMS.Services.Controllers
return Ok(ApiResponse<object>.ErrorResponse(ex.Message, ex, 400)); return Ok(ApiResponse<object>.ErrorResponse(ex.Message, ex, 400));
} }
} }
var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeList = employeeIds };
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
return Ok(ApiResponse<object>.SuccessResponse(result, "Data saved successfully", 200)); return Ok(ApiResponse<object>.SuccessResponse(result, "Data saved successfully", 200));
} }
@ -573,52 +595,91 @@ namespace MarcoBMS.Services.Controllers
} }
[HttpPost("task")] [HttpPost("task")]
public async Task<IActionResult> CreateProjectTask(List<WorkItemDot> workItemDot) public async Task<IActionResult> CreateProjectTask(List<WorkItemDot> workItemDtos)
{ {
Guid tenantId = GetTenantId(); _logger.LogInfo("CreateProjectTask called with {Count} items", workItemDtos?.Count ?? 0);
List<WorkItemVM> workItems = new List<WorkItemVM> { };
string responseMessage = "";
if (workItemDot != null)
{
foreach (var item in workItemDot)
{
WorkItem workItem = item.ToWorkItemFromWorkItemDto(tenantId);
if (item.Id != null) // Validate request
{ if (workItemDtos == null || !workItemDtos.Any())
//update {
_context.WorkItems.Update(workItem); _logger.LogWarning("No work items provided in the request.");
await _context.SaveChangesAsync(); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid details.", "Work Item details are not valid.", 400));
responseMessage = "Task Added Successfully";
}
else
{
//create
_context.WorkItems.Add(workItem);
await _context.SaveChangesAsync();
responseMessage = "Task Updated Successfully";
}
var result = new WorkItemVM
{
WorkItemId = workItem.Id,
WorkItem = workItem
};
workItems.Add(result);
}
var activity = await _context.ActivityMasters.ToListAsync();
var category = await _context.WorkCategoryMasters.ToListAsync();
return Ok(ApiResponse<object>.SuccessResponse(workItems, responseMessage, 200));
} }
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid details.", "Work Item Details are not valid.", 400)); Guid tenantId = GetTenantId();
var workItemsToCreate = new List<WorkItem>();
var workItemsToUpdate = new List<WorkItem>();
var responseList = new List<WorkItemVM>();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
string message = "";
List<Guid> projectIds = new List<Guid>();
foreach (var itemDto in workItemDtos)
{
var workItem = itemDto.ToWorkItemFromWorkItemDto(tenantId);
var workArea = await _context.WorkAreas.Include(a => a.Floor).FirstOrDefaultAsync(a => a.Id == workItem.WorkAreaId) ?? new WorkArea();
Building building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == (workArea.Floor != null ? workArea.Floor.BuildingId : Guid.Empty)) ?? new Building();
if (itemDto.Id != null && itemDto.Id != Guid.Empty)
{
// Update existing
workItemsToUpdate.Add(workItem);
message = $"Task Updated in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}";
}
else
{
// Create new
workItem.Id = Guid.NewGuid();
workItemsToCreate.Add(workItem);
message = $"Task Added in Building: {building.Name}, on Floor: {workArea.Floor?.FloorName}, in Area: {workArea.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}";
}
responseList.Add(new WorkItemVM
{
WorkItemId = workItem.Id,
WorkItem = workItem
});
projectIds.Add(building.ProjectId);
}
string responseMessage = "";
// Apply DB changes
if (workItemsToCreate.Any())
{
_logger.LogInfo("Adding {Count} new work items", workItemsToCreate.Count);
await _context.WorkItems.AddRangeAsync(workItemsToCreate);
responseMessage = "Task Added Successfully";
}
if (workItemsToUpdate.Any())
{
_logger.LogInfo("Updating {Count} existing work items", workItemsToUpdate.Count);
_context.WorkItems.UpdateRange(workItemsToUpdate);
responseMessage = "Task Updated Successfully";
}
await _context.SaveChangesAsync();
_logger.LogInfo("CreateProjectTask completed successfully. Created: {Created}, Updated: {Updated}", workItemsToCreate.Count, workItemsToUpdate.Count);
var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message };
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
return Ok(ApiResponse<object>.SuccessResponse(responseList, responseMessage, 200));
} }
[HttpDelete("task/{id}")] [HttpDelete("task/{id}")]
public async Task<IActionResult> DeleteProjectTask(Guid id) public async Task<IActionResult> DeleteProjectTask(Guid id)
{ {
Guid tenantId = _userHelper.GetTenantId(); Guid tenantId = _userHelper.GetTenantId();
WorkItem? task = await _context.WorkItems.AsNoTracking().FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId); var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
List<Guid> projectIds = new List<Guid>();
WorkItem? task = await _context.WorkItems.AsNoTracking().Include(t => t.WorkArea).FirstOrDefaultAsync(t => t.Id == id && t.TenantId == tenantId);
if (task != null) if (task != null)
{ {
if (task.CompletedWork == 0) if (task.CompletedWork == 0)
@ -629,6 +690,15 @@ namespace MarcoBMS.Services.Controllers
_context.WorkItems.Remove(task); _context.WorkItems.Remove(task);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
_logger.LogInfo("Task with ID {WorkItemId} has been successfully deleted.", task.Id); _logger.LogInfo("Task with ID {WorkItemId} has been successfully deleted.", task.Id);
var floorId = task.WorkArea?.FloorId;
var floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == floorId);
projectIds.Add(floor?.Building?.ProjectId ?? Guid.Empty);
var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = $"Task Deleted in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}, in Area: {task.WorkArea?.AreaName} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}" };
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
} }
else else
{ {
@ -656,8 +726,12 @@ namespace MarcoBMS.Services.Controllers
public async Task<IActionResult> ManageProjectInfra(List<InfraDot> infraDots) public async Task<IActionResult> ManageProjectInfra(List<InfraDot> infraDots)
{ {
Guid tenantId = GetTenantId(); Guid tenantId = GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var responseData = new InfraVM { }; var responseData = new InfraVM { };
string responseMessage = ""; string responseMessage = "";
string message = "";
List<Guid> projectIds = new List<Guid>();
if (infraDots != null) if (infraDots != null)
{ {
foreach (var item in infraDots) foreach (var item in infraDots)
@ -675,6 +749,7 @@ namespace MarcoBMS.Services.Controllers
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
responseData.building = building; responseData.building = building;
responseMessage = "Buliding Added Successfully"; responseMessage = "Buliding Added Successfully";
message = "Building Added";
} }
else else
{ {
@ -683,8 +758,10 @@ namespace MarcoBMS.Services.Controllers
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
responseData.building = building; responseData.building = building;
responseMessage = "Buliding Updated Successfully"; responseMessage = "Buliding Updated Successfully";
message = "Building Updated";
} }
projectIds.Add(building.ProjectId);
} }
if (item.Floor != null) if (item.Floor != null)
{ {
@ -698,6 +775,7 @@ namespace MarcoBMS.Services.Controllers
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
responseData.floor = floor; responseData.floor = floor;
responseMessage = "Floor Added Successfully"; responseMessage = "Floor Added Successfully";
message = "Floor Added";
} }
else else
{ {
@ -706,7 +784,11 @@ namespace MarcoBMS.Services.Controllers
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
responseData.floor = floor; responseData.floor = floor;
responseMessage = "Floor Updated Successfully"; responseMessage = "Floor Updated Successfully";
message = "Floor Updated";
} }
Building? building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == floor.BuildingId);
projectIds.Add(building?.ProjectId ?? Guid.Empty);
message = $"{message} in Building: {building?.Name}";
} }
if (item.WorkArea != null) if (item.WorkArea != null)
{ {
@ -720,6 +802,7 @@ namespace MarcoBMS.Services.Controllers
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
responseData.workArea = workArea; responseData.workArea = workArea;
responseMessage = "Work Area Added Successfully"; responseMessage = "Work Area Added Successfully";
message = "Work Area Added";
} }
else else
{ {
@ -728,9 +811,17 @@ namespace MarcoBMS.Services.Controllers
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
responseData.workArea = workArea; responseData.workArea = workArea;
responseMessage = "Work Area Updated Successfully"; responseMessage = "Work Area Updated Successfully";
message = "Work Area Updated";
} }
Floor? floor = await _context.Floor.Include(f => f.Building).FirstOrDefaultAsync(f => f.Id == workArea.FloorId);
projectIds.Add(floor?.Building?.ProjectId ?? Guid.Empty);
message = $"{message} in Building: {floor?.Building?.Name}, on Floor: {floor?.FloorName}";
} }
} }
message = $"{message} by {LoggedInEmployee.FirstName} {LoggedInEmployee.LastName}";
var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Infra", ProjectIds = projectIds, Message = message };
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
return Ok(ApiResponse<object>.SuccessResponse(responseData, responseMessage, 200)); return Ok(ApiResponse<object>.SuccessResponse(responseData, responseMessage, 200));
} }
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid details.", "Infra Details are not valid.", 400));
@ -776,16 +867,15 @@ namespace MarcoBMS.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(projects, "Success.", 200)); return Ok(ApiResponse<object>.SuccessResponse(projects, "Success.", 200));
} }
[HttpPost("assign-projects/{employeeId}")] [HttpPost("assign-projects/{employeeId}")]
public async Task<ActionResult> AssigneProjectsToEmployee([FromBody] List<ProjectsAllocationDto> projectAllocationDtos, [FromRoute] Guid employeeId) public async Task<ActionResult> AssigneProjectsToEmployee([FromBody] List<ProjectsAllocationDto> projectAllocationDtos, [FromRoute] Guid employeeId)
{ {
if (projectAllocationDtos != null && employeeId != Guid.Empty) if (projectAllocationDtos != null && employeeId != Guid.Empty)
{ {
Guid TenentID = GetTenantId(); Guid TenentID = GetTenantId();
var LoggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
List<object>? result = new List<object>(); List<object>? result = new List<object>();
List<Guid> projectIds = new List<Guid>();
foreach (var projectAllocationDto in projectAllocationDtos) foreach (var projectAllocationDto in projectAllocationDtos)
{ {
@ -813,6 +903,8 @@ namespace MarcoBMS.Services.Controllers
projectAllocationFromDb.IsActive = false; projectAllocationFromDb.IsActive = false;
_context.Entry(projectAllocationFromDb).Property(e => e.ReAllocationDate).IsModified = true; _context.Entry(projectAllocationFromDb).Property(e => e.ReAllocationDate).IsModified = true;
_context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true; _context.Entry(projectAllocationFromDb).Property(e => e.IsActive).IsModified = true;
projectIds.Add(projectAllocation.ProjectId);
} }
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var result1 = new var result1 = new
@ -835,6 +927,8 @@ namespace MarcoBMS.Services.Controllers
_context.ProjectAllocations.Add(projectAllocation); _context.ProjectAllocations.Add(projectAllocation);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
projectIds.Add(projectAllocation.ProjectId);
} }
@ -845,6 +939,9 @@ namespace MarcoBMS.Services.Controllers
return Ok(ApiResponse<object>.ErrorResponse(ex.Message, ex, 400)); return Ok(ApiResponse<object>.ErrorResponse(ex.Message, ex, 400));
} }
} }
var notification = new { LoggedInUserId = LoggedInEmployee.Id, Keyword = "Assign_Project", ProjectIds = projectIds, EmployeeId = employeeId };
await _signalR.Clients.All.SendAsync("NotificationEventHandler", notification);
return Ok(ApiResponse<object>.SuccessResponse(result, "Data saved successfully", 200)); return Ok(ApiResponse<object>.SuccessResponse(result, "Data saved successfully", 200));
} }

View File

@ -1,17 +1,19 @@
using Marco.Pms.DataAccess.Data; using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Activities; using Marco.Pms.Model.Activities;
using Marco.Pms.Model.Dtos.Activities; using Marco.Pms.Model.Dtos.Activities;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.Projects; using Marco.Pms.Model.Projects;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities; using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Employee; using Marco.Pms.Services.Service;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Document = Marco.Pms.Model.DocumentManager.Document;
namespace MarcoBMS.Services.Controllers namespace MarcoBMS.Services.Controllers
{ {
@ -23,12 +25,21 @@ namespace MarcoBMS.Services.Controllers
{ {
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly UserHelper _userHelper; private readonly UserHelper _userHelper;
private readonly S3UploadService _s3Service;
private readonly ILoggingService _logger;
private readonly PermissionServices _permissionServices;
private readonly Guid Approve_Task;
private readonly Guid Assign_Report_Task;
public TaskController(ApplicationDbContext context, UserHelper userHelper, S3UploadService s3Service, ILoggingService logger, PermissionServices permissionServices)
public TaskController(ApplicationDbContext context, UserHelper userHelper)
{ {
_context = context; _context = context;
_userHelper = userHelper; _userHelper = userHelper;
_s3Service = s3Service;
_logger = logger;
_permissionServices = permissionServices;
Approve_Task = Guid.Parse("db4e40c5-2ba9-4b6d-b8a6-a16a250ff99c");
Assign_Report_Task = Guid.Parse("6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2");
} }
private Guid GetTenantId() private Guid GetTenantId()
@ -39,51 +50,69 @@ namespace MarcoBMS.Services.Controllers
[HttpPost("assign")] [HttpPost("assign")]
public async Task<IActionResult> AssignTask([FromBody] AssignTaskDto assignTask) public async Task<IActionResult> AssignTask([FromBody] AssignTaskDto assignTask)
{ {
// Validate the incoming model
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
var errors = ModelState.Values var errors = ModelState.Values
.SelectMany(v => v.Errors) .SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage) .Select(e => e.ErrorMessage)
.ToList(); .ToList();
_logger.LogWarning("AssignTask failed validation: {@Errors}", errors);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
var tenantId = GetTenantId();
var Employee = await _userHelper.GetCurrentEmployeeAsync();
var taskAllocation = assignTask.ToTaskAllocationFromAssignTaskDto(Employee.Id, tenantId); // Retrieve tenant and employee context
var tenantId = GetTenantId();
var employee = await _userHelper.GetCurrentEmployeeAsync();
// Check for permission to approve tasks
var hasPermission = await _permissionServices.HasPermission(Assign_Report_Task, employee.Id);
if (!hasPermission)
{
_logger.LogWarning("Employee {EmployeeId} attempted to assign Task without permission", employee.Id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("You don't have access", "User not authorized to approve tasks", 403));
}
_logger.LogInfo("Employee {EmployeeId} is assigning a new task", employee.Id);
// Convert DTO to entity and save TaskAllocation
var taskAllocation = assignTask.ToTaskAllocationFromAssignTaskDto(employee.Id, tenantId);
_context.TaskAllocations.Add(taskAllocation); _context.TaskAllocations.Add(taskAllocation);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
_logger.LogInfo("Task {TaskId} assigned by Employee {EmployeeId}", taskAllocation.Id, employee.Id);
var response = taskAllocation.ToAssignTaskVMFromTaskAllocation(); var response = taskAllocation.ToAssignTaskVMFromTaskAllocation();
var teamMembers = new List<TaskMembers> { }; // Map team members
if (assignTask.TaskTeam != null) var teamMembers = new List<TaskMembers>();
if (assignTask.TaskTeam != null && assignTask.TaskTeam.Any())
{ {
foreach (var teamMember in assignTask.TaskTeam) teamMembers = assignTask.TaskTeam.Select(memberId => new TaskMembers
{ {
var result = new TaskMembers TaskAllocationId = taskAllocation.Id,
{ EmployeeId = memberId,
TaskAllocationId = taskAllocation.Id, TenantId = tenantId
EmployeeId = teamMember, }).ToList();
TenantId = tenantId,
};
teamMembers.Add(result);
}
}
_context.TaskMembers.AddRange(teamMembers);
await _context.SaveChangesAsync();
var idList = teamMembers.Select(m => m.EmployeeId); _context.TaskMembers.AddRange(teamMembers);
List<Employee> employees = await _context.Employees.Where(e => idList.Contains(e.Id)).ToListAsync(); await _context.SaveChangesAsync();
List<BasicEmployeeVM> team = new List<BasicEmployeeVM>();
foreach (var employee in employees) _logger.LogInfo("Team members added to Task {TaskId}: {@TeamMemberIds}", taskAllocation.Id, assignTask.TaskTeam);
{
team.Add(employee.ToBasicEmployeeVMFromEmployee());
} }
// Get team member details
var employeeIds = teamMembers.Select(m => m.EmployeeId).ToList();
var employees = await _context.Employees
.Where(e => employeeIds.Contains(e.Id))
.ToListAsync();
var team = employees.Select(e => e.ToBasicEmployeeVMFromEmployee()).ToList();
response.teamMembers = team; response.teamMembers = team;
return Ok(ApiResponse<object>.SuccessResponse(response, "Task assignned successfully", 200));
}
return Ok(ApiResponse<object>.SuccessResponse(response, "Task assigned successfully", 200));
}
[HttpPost("report")] [HttpPost("report")]
public async Task<IActionResult> ReportTaskProgress([FromBody] ReportTaskDto reportTask) public async Task<IActionResult> ReportTaskProgress([FromBody] ReportTaskDto reportTask)
@ -94,211 +123,661 @@ namespace MarcoBMS.Services.Controllers
.SelectMany(v => v.Errors) .SelectMany(v => v.Errors)
.Select(e => e.ErrorMessage) .Select(e => e.ErrorMessage)
.ToList(); .ToList();
_logger.LogWarning("Task report validation failed: {@Errors}", errors);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", errors, 400));
} }
var tenantId = GetTenantId(); var tenantId = GetTenantId();
var Employee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var taskAllocation = await _context.TaskAllocations.Include(t => t.WorkItem).FirstOrDefaultAsync(t => t.Id == reportTask.Id); var hasPermission = await _permissionServices.HasPermission(Assign_Report_Task, loggedInEmployee.Id);
if (!hasPermission)
{
_logger.LogWarning("Unauthorized task report attempt by Employee {EmployeeId} for Task {TaskId}", loggedInEmployee.Id, reportTask.Id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("You don't have access", "User not authorized to report tasks", 403));
}
var taskAllocation = await _context.TaskAllocations
.Include(t => t.WorkItem)
.FirstOrDefaultAsync(t => t.Id == reportTask.Id);
var checkListIds = reportTask.CheckList != null ? reportTask.CheckList.Select(c => c.Id).ToList() : new List<Guid>();
var checkList = await _context.ActivityCheckLists.Where(c => checkListIds.Contains(c.Id)).ToListAsync();
if (taskAllocation == null) if (taskAllocation == null)
{ {
_logger.LogWarning("No task allocation found with ID {TaskId}", reportTask.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("No such task has been allocated.", "No such task has been allocated.", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("No such task has been allocated.", "No such task has been allocated.", 400));
} }
var checkListIds = reportTask.CheckList?.Select(c => c.Id).ToList() ?? new List<Guid>();
var checkList = await _context.ActivityCheckLists
.Where(c => checkListIds.Contains(c.Id))
.ToListAsync();
if (taskAllocation.WorkItem != null) if (taskAllocation.WorkItem != null)
{ {
if (taskAllocation.CompletedTask != 0) if (taskAllocation.CompletedTask > 0)
{
taskAllocation.WorkItem.CompletedWork -= taskAllocation.CompletedTask; taskAllocation.WorkItem.CompletedWork -= taskAllocation.CompletedTask;
}
taskAllocation.ReportedDate = reportTask.ReportedDate;
taskAllocation.CompletedTask = reportTask.CompletedTask;
taskAllocation.WorkItem.CompletedWork += reportTask.CompletedTask; taskAllocation.WorkItem.CompletedWork += reportTask.CompletedTask;
} }
List<CheckListMappings> checkListMappings = new List<CheckListMappings>();
List<CheckListVM> checkListVMs = new List<CheckListVM>(); taskAllocation.ParentTaskId = reportTask.ParentTaskId;
taskAllocation.ReportedDate = reportTask.ReportedDate;
taskAllocation.ReportedById = loggedInEmployee.Id;
taskAllocation.CompletedTask = reportTask.CompletedTask;
//taskAllocation.ReportedTask = reportTask.CompletedTask;
var checkListMappings = new List<CheckListMappings>();
var checkListVMs = new List<CheckListVM>();
if (reportTask.CheckList != null) if (reportTask.CheckList != null)
{ {
var activityId = taskAllocation.WorkItem?.ActivityId ?? Guid.Empty;
foreach (var checkDto in reportTask.CheckList) foreach (var checkDto in reportTask.CheckList)
{ {
checkListVMs.Add(checkDto.ToCheckListVMFromReportCheckListDto(taskAllocation.WorkItem != null ? taskAllocation.WorkItem.ActivityId : Guid.Empty)); checkListVMs.Add(checkDto.ToCheckListVMFromReportCheckListDto(activityId));
if (checkDto.IsChecked)
if (checkDto.IsChecked && checkList.Any(c => c.Id == checkDto.Id))
{ {
var check = checkList.Find(c => c.Id == checkDto.Id); checkListMappings.Add(new CheckListMappings
if (check != null)
{ {
CheckListMappings checkListMapping = new CheckListMappings CheckListId = checkDto.Id,
{ TaskAllocationId = reportTask.Id
CheckListId = check.Id, });
TaskAllocationId = reportTask.Id
};
checkListMappings.Add(checkListMapping);
}
} }
} }
}
_context.CheckListMappings.AddRange(checkListMappings);
var comment = reportTask.ToCommentFromReportTaskDto(tenantId, Employee.Id);
_context.CheckListMappings.AddRange(checkListMappings);
}
var comment = reportTask.ToCommentFromReportTaskDto(tenantId, loggedInEmployee.Id);
_context.TaskComments.Add(comment); _context.TaskComments.Add(comment);
if (reportTask.Images?.Any() == true)
{
var workAreaId = taskAllocation.WorkItem?.WorkAreaId;
var workArea = await _context.WorkAreas.Include(a => a.Floor)
.FirstOrDefaultAsync(a => a.Id == workAreaId) ?? new WorkArea();
var buildingId = workArea.Floor?.BuildingId;
var building = await _context.Buildings
.FirstOrDefaultAsync(b => b.Id == buildingId);
foreach (var image in reportTask.Images)
{
if (string.IsNullOrEmpty(image.Base64Data))
{
_logger.LogWarning("Image upload failed: Base64 data is missing");
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
}
var base64 = image.Base64Data.Contains(',')
? image.Base64Data[(image.Base64Data.IndexOf(",") + 1)..]
: image.Base64Data;
var fileType = _s3Service.GetContentTypeFromBase64(base64);
var fileName = _s3Service.GenerateFileName(fileType, tenantId, "task_report");
var objectKey = $"tenant-{tenantId}/project-{building?.ProjectId}/Actitvity/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey);
var document = new Document
{
FileName = image.FileName ?? "",
ContentType = image.ContentType ?? "",
S3Key = objectKey,
Base64Data = image.Base64Data,
FileSize = image.FileSize,
UploadedAt = DateTime.UtcNow,
TenantId = tenantId
};
_context.Documents.Add(document);
var attachment = new TaskAttachment
{
DocumentId = document.Id,
ReferenceId = reportTask.Id
};
_context.TaskAttachments.Add(attachment);
}
}
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
var response = taskAllocation.ToReportTaskVMFromTaskAllocation(); var response = taskAllocation.ToReportTaskVMFromTaskAllocation();
List<TaskComment> comments = await _context.TaskComments.Where(c => c.TaskAllocationId == taskAllocation.Id).ToListAsync(); var comments = await _context.TaskComments
List<CommentVM> resultComments = new List<CommentVM> { }; .Where(c => c.TaskAllocationId == taskAllocation.Id)
foreach (var result in comments) .ToListAsync();
{
resultComments.Add(result.ToCommentVMFromTaskComment()); response.Comments = comments.Select(c => c.ToCommentVMFromTaskComment()).ToList();
}
response.Comments = resultComments;
response.checkList = checkListVMs; response.checkList = checkListVMs;
_logger.LogInfo("Task {TaskId} reported successfully by Employee {EmployeeId}", taskAllocation.Id, loggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse(response, "Task reported successfully", 200)); return Ok(ApiResponse<object>.SuccessResponse(response, "Task reported successfully", 200));
} }
[HttpPost("comment")] [HttpPost("comment")]
public async Task<IActionResult> AddCommentForTask([FromBody] CreateCommentDto createComment) public async Task<IActionResult> AddCommentForTask([FromBody] CreateCommentDto createComment)
{ {
var tenantId = GetTenantId(); _logger.LogInfo("AddCommentForTask called for TaskAllocationId: {TaskId}", createComment.TaskAllocationId);
var Employee = await _userHelper.GetCurrentEmployeeAsync();
var comment = createComment.ToCommentFromCommentDto(tenantId, Employee.Id); var tenantId = GetTenantId();
var employee = await _userHelper.GetCurrentEmployeeAsync();
// Validate Task Allocation and associated WorkItem
var taskAllocation = await _context.TaskAllocations
.Include(t => t.WorkItem)
.FirstOrDefaultAsync(t => t.Id == createComment.TaskAllocationId);
if (taskAllocation == null || taskAllocation.WorkItem == null)
{
_logger.LogWarning("Invalid task allocation or work item not found.");
return BadRequest(ApiResponse<object>.ErrorResponse("No such task has been allocated.", "No such task has been allocated.", 400));
}
// Fetch WorkArea and Building (if available)
var workArea = await _context.WorkAreas
.Include(a => a.Floor)
.FirstOrDefaultAsync(a => a.Id == taskAllocation.WorkItem.WorkAreaId) ?? new WorkArea();
var buildingId = workArea.Floor?.BuildingId ?? Guid.Empty;
var building = await _context.Buildings.FirstOrDefaultAsync(b => b.Id == buildingId);
// Save comment
var comment = createComment.ToCommentFromCommentDto(tenantId, employee.Id);
_context.TaskComments.Add(comment); _context.TaskComments.Add(comment);
await _context.SaveChangesAsync(); await _context.SaveChangesAsync();
_logger.LogInfo("Comment saved with Id: {CommentId}", comment.Id);
// Process image uploads
var images = createComment.Images;
if (images != null && images.Any())
{
foreach (var image in images)
{
if (string.IsNullOrWhiteSpace(image.Base64Data))
{
_logger.LogWarning("Missing Base64 data in one of the images.");
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
}
// Clean base64 string
var base64 = image.Base64Data.Contains(",")
? image.Base64Data.Substring(image.Base64Data.IndexOf(",") + 1)
: image.Base64Data;
var fileType = _s3Service.GetContentTypeFromBase64(base64);
var fileName = _s3Service.GenerateFileName(fileType, tenantId, "task_comment");
var objectKey = $"tenant-{tenantId}/project-{building?.ProjectId}/Activity/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey);
_logger.LogInfo("Image uploaded to S3 with key: {ObjectKey}", objectKey);
var document = new Document
{
FileName = image.FileName ?? string.Empty,
ContentType = image.ContentType ?? fileType,
S3Key = objectKey,
Base64Data = image.Base64Data,
FileSize = image.FileSize,
UploadedAt = DateTime.UtcNow,
TenantId = tenantId
};
_context.Documents.Add(document);
var attachment = new TaskAttachment
{
DocumentId = document.Id,
ReferenceId = comment.Id
};
_context.TaskAttachments.Add(attachment);
}
await _context.SaveChangesAsync();
_logger.LogInfo("Documents and attachments saved for commentId: {CommentId}", comment.Id);
}
// Convert to view model and return response
var response = comment.ToCommentVMFromTaskComment();
_logger.LogInfo("Returning response for commentId: {CommentId}", comment.Id);
CommentVM response = comment.ToCommentVMFromTaskComment();
return Ok(ApiResponse<object>.SuccessResponse(response, "Comment saved successfully", 200)); return Ok(ApiResponse<object>.SuccessResponse(response, "Comment saved successfully", 200));
} }
[HttpGet("list")] [HttpGet("list")]
public async Task<IActionResult> GetTasksList([FromQuery] Guid projectId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null) public async Task<IActionResult> GetTasksList([FromQuery] Guid projectId, [FromQuery] string? dateFrom = null, [FromQuery] string? dateTo = null)
{ {
_logger.LogInfo("GetTasksList called for projectId: {ProjectId}, dateFrom: {DateFrom}, dateTo: {DateTo}", projectId, dateFrom ?? "", dateTo ?? "");
Guid tenantId = GetTenantId(); Guid tenantId = GetTenantId();
DateTime fromDate = new DateTime(); DateTime fromDate = new DateTime();
DateTime toDate = new DateTime(); DateTime toDate = new DateTime();
if (dateFrom != null && DateTime.TryParse(dateFrom, out fromDate) == false) // Parse and validate dateFrom
if (dateFrom != null && !DateTime.TryParse(dateFrom, out fromDate))
{ {
_logger.LogWarning("Invalid starting date provided: {DateFrom}", dateFrom);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid starting date.", "Invalid starting date.", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid starting date.", "Invalid starting date.", 400));
} }
if (dateTo != null && DateTime.TryParse(dateTo, out toDate) == false)
// Parse and validate dateTo
if (dateTo != null && !DateTime.TryParse(dateTo, out toDate))
{ {
_logger.LogWarning("Invalid ending date provided: {DateTo}", dateTo);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid ending date.", "Invalid ending date.", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid ending date.", "Invalid ending date.", 400));
} }
if (dateFrom == null) fromDate = DateTime.UtcNow.Date;
if (dateTo == null) toDate = fromDate.AddDays(1);
var jobroles = await _context.JobRoles.Where(r => r.TenantId == tenantId).ToListAsync(); // Set default date range if not provided
//var taskAllocations = await _context.TaskAllocations.Where(t => t.WorkItem.WorkArea.Floor.Building.ProjectId == projectId && t.AssignmentDate >= fromDate && t.AssignmentDate <= toDate && t.TenantId == tenantId).Include(t => t.WorkItemId).ToListAsync(); fromDate = dateFrom == null ? DateTime.UtcNow.Date : fromDate;
List<Building> buildings = await _context.Buildings.Where(b => b.ProjectId == projectId && b.TenantId == tenantId).ToListAsync(); toDate = dateTo == null ? fromDate.AddDays(1) : toDate;
List<Guid> idList = buildings.Select(b => b.Id).ToList();
List<Floor> floors = await _context.Floor.Where(f => idList.Contains(f.BuildingId) && f.TenantId == tenantId).ToListAsync(); // 1. Get all buildings under this project
idList = floors.Select(f => f.Id).ToList(); _logger.LogInfo("Fetching buildings for projectId: {ProjectId}", projectId);
var buildings = await _context.Buildings
.Where(b => b.ProjectId == projectId && b.TenantId == tenantId)
.ToListAsync();
List<WorkArea> workAreas = await _context.WorkAreas.Where(a => idList.Contains(a.FloorId) && a.TenantId == tenantId).ToListAsync(); var buildingIds = buildings.Select(b => b.Id).ToList();
idList = workAreas.Select(a => a.Id).ToList();
List<WorkItem> workItems = await _context.WorkItems.Where(i => idList.Contains(i.WorkAreaId) && i.TenantId == tenantId).Include(i => i.ActivityMaster).ToListAsync(); // 2. Get floors under the buildings
idList = workItems.Select(i => i.Id).ToList(); var floors = await _context.Floor
var activityIdList = workItems.Select(i => i.ActivityId).ToList(); .Where(f => buildingIds.Contains(f.BuildingId) && f.TenantId == tenantId)
.ToListAsync();
var floorIds = floors.Select(f => f.Id).ToList();
List<TaskAllocation> taskAllocations = await _context.TaskAllocations.Where(t => idList.Contains(t.WorkItemId) && t.AssignmentDate.Date >= fromDate.Date && t.AssignmentDate.Date <= toDate.Date && t.TenantId == tenantId).Include(t => t.WorkItem).Include(t => t.Employee).ToListAsync(); // 3. Get work areas under the floors
var taskIdList = taskAllocations.Select(t => t.Id).ToList(); var workAreas = await _context.WorkAreas
.Where(a => floorIds.Contains(a.FloorId) && a.TenantId == tenantId)
.ToListAsync();
var workAreaIds = workAreas.Select(a => a.Id).ToList();
List<TaskMembers> teamMembers = await _context.TaskMembers.Where(t => taskIdList.Contains(t.TaskAllocationId)).ToListAsync(); // 4. Get work items under the work areas
var employeeIdList = teamMembers.Select(e => e.EmployeeId).ToList(); var workItems = await _context.WorkItems
.Where(i => workAreaIds.Contains(i.WorkAreaId) && i.TenantId == tenantId)
.Include(i => i.ActivityMaster)
.ToListAsync();
var workItemIds = workItems.Select(i => i.Id).ToList();
_logger.LogInfo("Fetching task allocations between {FromDate} and {ToDate}", fromDate, toDate);
// 5. Get task allocations in the specified date range
var taskAllocations = await _context.TaskAllocations
.Include(t => t.Employee)
.Include(t => t.ReportedBy)
.Include(t => t.ApprovedBy)
.Include(t => t.WorkStatus)
.Include(t => t.WorkItem)
.Where(t => workItemIds.Contains(t.WorkItemId) &&
t.AssignmentDate.Date >= fromDate.Date &&
t.AssignmentDate.Date <= toDate.Date &&
t.TenantId == tenantId)
.ToListAsync();
var taskIds = taskAllocations.Select(t => t.Id).ToList();
// 6. Load team members
_logger.LogInfo("Loading task members and related employee data.");
var teamMembers = await _context.TaskMembers
.Include(t => t.Employee)
.Where(t => taskIds.Contains(t.TaskAllocationId))
.ToListAsync();
// 7. Load task comments
_logger.LogInfo("Fetching comments and attachments.");
var allComments = await _context.TaskComments
.Include(c => c.Employee)
.Where(c => taskIds.Contains(c.TaskAllocationId))
.ToListAsync();
var commentIds = allComments.Select(c => c.Id).ToList();
// 8. Load all attachments (task and comment)
var attachments = await _context.TaskAttachments
.Where(t => taskIds.Contains(t.ReferenceId) || commentIds.Contains(t.ReferenceId))
.ToListAsync();
var documentIds = attachments.Select(t => t.DocumentId).ToList();
// 9. Load actual documents from attachment references
var documents = await _context.Documents
.Where(d => documentIds.Contains(d.Id))
.ToListAsync();
var tasks = new List<ListTaskVM>();
_logger.LogInfo("Constructing task response data.");
List<Employee> employees = await _context.Employees.Where(e => employeeIdList.Contains(e.Id)).Include(e => e.JobRole).ToListAsync();
List<ListTaskVM> tasks = new List<ListTaskVM>();
//foreach (var workItem in workItems)
//{
foreach (var taskAllocation in taskAllocations) foreach (var taskAllocation in taskAllocations)
{ {
var response = taskAllocation.ToListTaskVMFromTaskAllocation(); var response = taskAllocation.ToListTaskVMFromTaskAllocation();
List<TaskComment> comments = await _context.TaskComments.Where(c => c.TaskAllocationId == taskAllocation.Id).ToListAsync(); // Attach documents to the main task
List<BasicEmployeeVM> team = new List<BasicEmployeeVM>(); var taskDocIds = attachments
List<TaskMembers> taskMembers = teamMembers.Where(m => m.TaskAllocationId == taskAllocation.Id).ToList(); .Where(a => a.ReferenceId == taskAllocation.Id)
.Select(a => a.DocumentId)
.ToList();
foreach (var taskMember in taskMembers) var taskDocs = documents
.Where(d => taskDocIds.Contains(d.Id))
.ToList();
response.ReportedPreSignedUrls = taskDocs
.Select(d => _s3Service.GeneratePreSignedUrlAsync(d.S3Key))
.ToList();
// Add team members
var taskMemberEntries = teamMembers
.Where(m => m.TaskAllocationId == taskAllocation.Id)
.ToList();
response.teamMembers = taskMemberEntries
.Select(m => m.Employee)
.Where(e => e != null)
.Select(e => e!.ToBasicEmployeeVMFromEmployee())
.ToList();
// Add comments with attachments
var commentVMs = new List<CommentVM>();
var taskComments = allComments
.Where(c => c.TaskAllocationId == taskAllocation.Id)
.ToList();
foreach (var comment in taskComments)
{ {
var teamMember = employees.Find(e => e.Id == taskMember.EmployeeId); var commentDocIds = attachments
if (teamMember != null) .Where(a => a.ReferenceId == comment.Id)
{ .Select(a => a.DocumentId)
team.Add(teamMember.ToBasicEmployeeVMFromEmployee()); .ToList();
}
var commentDocs = documents
.Where(d => commentDocIds.Contains(d.Id))
.ToList();
var commentVm = comment.ToCommentVMFromTaskComment();
commentVm.PreSignedUrls = commentDocs
.Select(d => _s3Service.GeneratePreSignedUrlAsync(d.S3Key))
.ToList();
commentVMs.Add(commentVm);
} }
List<CommentVM> commentVM = new List<CommentVM> { };
foreach (var comment in comments) response.comments = commentVMs;
// Checklists
var activityId = taskAllocation.WorkItem?.ActivityId ?? Guid.Empty;
var checkLists = await _context.ActivityCheckLists
.Where(c => c.ActivityId == activityId)
.ToListAsync();
var checkListMappings = await _context.CheckListMappings
.Where(c => c.TaskAllocationId == taskAllocation.Id)
.ToListAsync();
response.CheckList = checkLists.Select(check =>
{ {
commentVM.Add(comment.ToCommentVMFromTaskComment()); var isChecked = checkListMappings.Any(m => m.CheckListId == check.Id);
} return check.ToCheckListVMFromActivityCheckList(check.ActivityId, isChecked);
List<ActivityCheckList> checkLists = await _context.ActivityCheckLists.Where(x => x.ActivityId == (taskAllocation.WorkItem != null ? taskAllocation.WorkItem.ActivityId : Guid.Empty)).ToListAsync(); }).ToList();
List<CheckListMappings> checkListMappings = await _context.CheckListMappings.Where(c => c.TaskAllocationId == taskAllocation.Id).ToListAsync();
List<CheckListVM> checkList = new List<CheckListVM>();
foreach (var check in checkLists)
{
var checkListMapping = checkListMappings.Find(c => c.CheckListId == check.Id);
if (checkListMapping != null)
{
checkList.Add(check.ToCheckListVMFromActivityCheckList(check.ActivityId, true));
}
else
{
checkList.Add(check.ToCheckListVMFromActivityCheckList(check.ActivityId, false));
}
}
response.comments = commentVM;
response.teamMembers = team;
response.CheckList = checkList;
tasks.Add(response); tasks.Add(response);
} }
//} _logger.LogInfo("Task list constructed successfully. Returning {Count} tasks.", tasks.Count);
return Ok(ApiResponse<object>.SuccessResponse(tasks, "Success", 200)); return Ok(ApiResponse<object>.SuccessResponse(tasks, "Success", 200));
} }
[HttpGet("get/{taskId}")] [HttpGet("get/{taskId}")]
public async Task<IActionResult> GetTask(Guid taskId) public async Task<IActionResult> GetTask(Guid taskId)
{ {
if (taskId == Guid.Empty) return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", "Invalid data", 400)); _logger.LogInfo("GetTask called with taskId: {TaskId}", taskId);
var taskAllocation = await _context.TaskAllocations.Include(t => t.Tenant).Include(t => t.Employee).Include(t => t.WorkItem).FirstOrDefaultAsync(t => t.Id == taskId); // Validate input
if (taskAllocation != null && taskAllocation.Employee != null && taskAllocation.Tenant != null) if (taskId == Guid.Empty)
{ {
//var employee = await _context.Employees.FirstOrDefaultAsync(e => e.Id == taskAllocation.AssignedBy); _logger.LogWarning("Invalid taskId provided.");
string employeeName = System.String.Format("{0} {1}", taskAllocation.Employee.FirstName, taskAllocation.Employee.LastName); return BadRequest(ApiResponse<object>.ErrorResponse("Invalid data", "Invalid data", 400));
string tenantName = taskAllocation.Tenant.ContactName ?? string.Empty;
if (taskAllocation == null) return NotFound(ApiResponse<object>.ErrorResponse("Task Not Found", "Task not found", 404));
var taskVM = taskAllocation.TaskAllocationToTaskVM(employeeName);
var comments = await _context.TaskComments.Where(c => c.TaskAllocationId == taskAllocation.Id).ToListAsync();
var team = await _context.TaskMembers.Where(m => m.TaskAllocationId == taskAllocation.Id).Include(m => m.Employee).ToListAsync();
var teamMembers = new List<EmployeeVM> { };
foreach (var member in team)
{
var result = member.Employee != null ? member.Employee.ToEmployeeVMFromEmployee() : new EmployeeVM();
teamMembers.Add(result);
}
List<CommentVM> Comments = new List<CommentVM> { };
foreach (var comment in comments)
{
Comments.Add(comment.ToCommentVMFromTaskComment());
}
taskVM.Comments = Comments;
taskVM.TeamMembers = teamMembers;
return Ok(ApiResponse<object>.SuccessResponse(taskVM, "Success", 200));
} }
// Fetch Task Allocation with required related data
var taskAllocation = await _context.TaskAllocations
.Include(t => t.Tenant)
.Include(t => t.Employee)
.Include(t => t.ReportedBy)
.Include(t => t.ApprovedBy)
.Include(t => t.WorkItem)
.Include(t => t.WorkStatus)
.FirstOrDefaultAsync(t => t.Id == taskId);
return NotFound(ApiResponse<object>.ErrorResponse("Task Not Found", "Task not Found", 404)); if (taskAllocation == null)
{
_logger.LogWarning("Task not found for taskId: {TaskId}", taskId);
return NotFound(ApiResponse<object>.ErrorResponse("Task Not Found", "Task not found", 404));
}
if (taskAllocation.Employee == null || taskAllocation.Tenant == null)
{
_logger.LogWarning("Task found but missing Employee or Tenant data for taskId: {TaskId}", taskId);
return NotFound(ApiResponse<object>.ErrorResponse("Task Not Found", "Task not found", 404));
}
_logger.LogInfo("Task allocation found. Preparing response.");
var taskVM = taskAllocation.TaskAllocationToTaskVM();
// Fetch comments and attachments
_logger.LogInfo("Fetching comments and attachments for taskId: {TaskId}", taskId);
var comments = await _context.TaskComments
.Where(c => c.TaskAllocationId == taskId)
.Include(c => c.Employee)
.ToListAsync();
var commentIds = comments.Select(c => c.Id).ToList();
var taskAttachments = await _context.TaskAttachments
.Where(t => t.ReferenceId == taskId || commentIds.Contains(t.ReferenceId))
.ToListAsync();
var documentIds = taskAttachments.Select(t => t.DocumentId).Distinct().ToList();
var documents = await _context.Documents
.Where(d => documentIds.Contains(d.Id))
.ToListAsync();
// Fetch team members
_logger.LogInfo("Fetching team members for taskId: {TaskId}", taskId);
var team = await _context.TaskMembers
.Where(m => m.TaskAllocationId == taskId)
.Include(m => m.Employee)
.ToListAsync();
var teamMembers = team
.Where(m => m.Employee != null)
.Select(m => m.Employee!.ToBasicEmployeeVMFromEmployee())
.ToList();
taskVM.TeamMembers = teamMembers;
// Attach documents to the main task
_logger.LogInfo("Generating presigned URLs for task documents.");
var taskDocumentIds = taskAttachments
.Where(t => t.ReferenceId == taskId)
.Select(t => t.DocumentId)
.ToList();
var taskDocuments = documents
.Where(d => taskDocumentIds.Contains(d.Id))
.ToList();
taskVM.PreSignedUrls = taskDocuments
.Select(d => _s3Service.GeneratePreSignedUrlAsync(d.S3Key))
.ToList();
// Construct CommentVM list with document URLs
_logger.LogInfo("Preparing comment response data.");
var commentVMs = comments.Select(comment =>
{
var commentDocIds = taskAttachments
.Where(t => t.ReferenceId == comment.Id)
.Select(t => t.DocumentId)
.ToList();
var commentDocs = documents
.Where(d => commentDocIds.Contains(d.Id))
.ToList();
var commentVM = comment.ToCommentVMFromTaskComment();
commentVM.PreSignedUrls = commentDocs
.Select(d => _s3Service.GeneratePreSignedUrlAsync(d.S3Key))
.ToList();
return commentVM;
}).ToList();
taskVM.Comments = commentVMs;
_logger.LogInfo("Task details prepared successfully for taskId: {TaskId}", taskId);
return Ok(ApiResponse<object>.SuccessResponse(taskVM, "Success", 200));
}
/// <summary>
/// Approves a reported task after validation, updates status, and stores attachments/comments.
/// </summary>
/// <param name="approveTask">DTO containing task approval details.</param>
/// <returns>IActionResult indicating success or failure.</returns>
[HttpPost("approve")]
public async Task<IActionResult> ApproveTask(ApproveTaskDto approveTask)
{
Guid tenantId = _userHelper.GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
_logger.LogInfo("Employee {EmployeeId} is attempting to approve Task {TaskId}", loggedInEmployee.Id, approveTask.Id);
// Fetch task allocation with work item, only if it's reported
var taskAllocation = await _context.TaskAllocations
.Include(t => t.WorkItem)
.FirstOrDefaultAsync(t => t.Id == approveTask.Id && t.TenantId == tenantId && t.ReportedDate != null);
if (taskAllocation == null)
{
_logger.LogWarning("Task {TaskId} not found or not reported yet for Tenant {TenantId} by Employee {EmployeeId}",
approveTask.Id, tenantId, loggedInEmployee.Id);
return NotFound(ApiResponse<object>.ErrorResponse("Task not found", "Task not found", 404));
}
// Check for permission to approve tasks
var hasPermission = await _permissionServices.HasPermission(Approve_Task, loggedInEmployee.Id);
if (!hasPermission)
{
_logger.LogWarning("Employee {EmployeeId} attempted to approve Task {TaskId} without permission", loggedInEmployee.Id, approveTask.Id);
return StatusCode(403, ApiResponse<object>.ErrorResponse("You don't have access", "User not authorized to approve tasks", 403));
}
// Validation: Approved task count cannot exceed completed task count
if (taskAllocation.CompletedTask < approveTask.ApprovedTask)
{
_logger.LogWarning("Invalid approval attempt on Task {TaskId}: Approved tasks ({ApprovedTask}) > Completed tasks ({CompletedTask})",
approveTask.Id, approveTask.ApprovedTask, taskAllocation.CompletedTask);
return BadRequest(ApiResponse<object>.ErrorResponse("Approved tasks cannot be greater than completed tasks",
"Approved tasks cannot be greater than completed tasks", 400));
}
//// Update completed work in the associated work item, if it exists
//if (taskAllocation.WorkItem != null && taskAllocation.CompletedTask != approveTask.ApprovedTask)
//{
// if (taskAllocation.CompletedTask > 0)
// {
// taskAllocation.WorkItem.CompletedWork -= taskAllocation.CompletedTask;
// }
// taskAllocation.WorkItem.CompletedWork += approveTask.ApprovedTask;
//}
// Update task allocation details
taskAllocation.ApprovedById = loggedInEmployee.Id;
taskAllocation.ApprovedDate = DateTime.UtcNow;
taskAllocation.WorkStatusId = approveTask.WorkStatus;
taskAllocation.ReportedTask = approveTask.ApprovedTask;
// Add a comment (optional)
var comment = new TaskComment
{
TaskAllocationId = taskAllocation.Id,
CommentDate = DateTime.UtcNow,
Comment = approveTask.Comment ?? string.Empty,
CommentedBy = loggedInEmployee.Id,
TenantId = tenantId
};
_context.TaskComments.Add(comment);
// Handle image attachments, if any
if (approveTask.Images?.Count > 0)
{
var workAreaId = taskAllocation.WorkItem?.WorkAreaId;
var workArea = await _context.WorkAreas.Include(a => a.Floor)
.FirstOrDefaultAsync(a => a.Id == workAreaId) ?? new WorkArea();
var buildingId = workArea.Floor?.BuildingId;
var building = await _context.Buildings
.FirstOrDefaultAsync(b => b.Id == buildingId);
foreach (var image in approveTask.Images)
{
if (string.IsNullOrEmpty(image.Base64Data))
{
_logger.LogWarning("Image for Task {TaskId} is missing base64 data", approveTask.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("Base64 data is missing", "Base64 data is missing", 400));
}
var base64 = image.Base64Data.Contains(",") ? image.Base64Data[(image.Base64Data.IndexOf(",") + 1)..] : image.Base64Data;
var fileType = _s3Service.GetContentTypeFromBase64(base64);
var fileName = _s3Service.GenerateFileName(fileType, tenantId, "task_comment");
var objectKey = $"tenant-{tenantId}/project-{building?.ProjectId}/Activity/{fileName}";
await _s3Service.UploadFileAsync(base64, fileType, objectKey);
var document = new Document
{
FileName = fileName,
ContentType = image.ContentType ?? string.Empty,
S3Key = objectKey,
Base64Data = image.Base64Data,
FileSize = image.FileSize,
UploadedAt = DateTime.UtcNow,
TenantId = tenantId
};
_context.Documents.Add(document);
var attachment = new TaskAttachment
{
DocumentId = document.Id,
ReferenceId = comment.Id
};
_context.TaskAttachments.Add(attachment);
_logger.LogInfo("Attachment uploaded for Task {TaskId}: {FileName}", approveTask.Id, fileName);
}
}
// Commit all changes to the database
await _context.SaveChangesAsync();
_logger.LogInfo("Task {TaskId} successfully approved by Employee {EmployeeId}", approveTask.Id, loggedInEmployee.Id);
return Ok(ApiResponse<object>.SuccessResponse("Task has been approved", "Task has been approved", 200));
} }
} }
} }

View File

@ -3,8 +3,10 @@ using Marco.Pms.Model.Directory;
using Marco.Pms.Model.Dtos.Master; using Marco.Pms.Model.Dtos.Master;
using Marco.Pms.Model.Employees; using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Mapper; using Marco.Pms.Model.Mapper;
using Marco.Pms.Model.Master;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Master; using Marco.Pms.Model.ViewModels.Master;
using Marco.Pms.Services.Service;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service; using MarcoBMS.Services.Service;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -16,13 +18,19 @@ namespace Marco.Pms.Services.Helpers
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly UserHelper _userHelper; private readonly UserHelper _userHelper;
private readonly PermissionServices _permissionService;
private readonly Guid View_Master;
private readonly Guid Manage_Master;
public MasterHelper(ApplicationDbContext context, ILoggingService logger, UserHelper userHelper) public MasterHelper(ApplicationDbContext context, ILoggingService logger, UserHelper userHelper, PermissionServices permissionServices)
{ {
_context = context; _context = context;
_logger = logger; _logger = logger;
_userHelper = userHelper; _userHelper = userHelper;
_permissionService = permissionServices;
View_Master = Guid.Parse("5ffbafe0-7ab0-48b1-bb50-c1bf76b65f9d");
Manage_Master = Guid.Parse("588a8824-f924-4955-82d8-fc51956cf323");
} }
// -------------------------------- Contact Category -------------------------------- // -------------------------------- Contact Category --------------------------------
public async Task<ApiResponse<object>> CreateContactCategory(CreateContactCategoryDto contactCategoryDto) public async Task<ApiResponse<object>> CreateContactCategory(CreateContactCategoryDto contactCategoryDto)
@ -247,5 +255,215 @@ namespace Marco.Pms.Services.Helpers
return ApiResponse<object>.SuccessResponse(new { }, "Tag deleted successfully", 200); return ApiResponse<object>.SuccessResponse(new { }, "Tag deleted successfully", 200);
} }
// -------------------------------- Work Status --------------------------------
public async Task<ApiResponse<object>> GetWorkStatusList()
{
_logger.LogInfo("GetWorkStatusList called.");
try
{
// Step 1: Get tenant and logged-in employee info
Guid tenantId = _userHelper.GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Step 2: Check permission to view master data
bool hasViewPermission = await _permissionService.HasPermission(View_Master, loggedInEmployee.Id);
if (!hasViewPermission)
{
_logger.LogWarning("Access denied for employeeId: {EmployeeId}", loggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("You don't have access", "Don't have access to take action", 403);
}
// Step 3: Fetch work statuses for the tenant
var workStatusList = await _context.WorkStatusMasters
.Where(ws => ws.TenantId == tenantId)
.Select(ws => new
{
ws.Id,
ws.Name,
ws.Description,
ws.IsSystem
})
.ToListAsync();
_logger.LogInfo("{Count} work statuses fetched for tenantId: {TenantId}", workStatusList.Count, tenantId);
// Step 4: Return successful response
return ApiResponse<object>.SuccessResponse(
workStatusList,
$"{workStatusList.Count} work status records fetched successfully",
200
);
}
catch (Exception ex)
{
_logger.LogError("Error occurred while fetching work status list : {Error}", ex.Message);
return ApiResponse<object>.ErrorResponse("An error occurred", "Unable to fetch work status list", 500);
}
}
public async Task<ApiResponse<object>> CreateWorkStatus(CreateWorkStatusMasterDto createWorkStatusDto)
{
_logger.LogInfo("CreateWorkStatus called with Name: {Name}", createWorkStatusDto.Name ?? "");
try
{
// Step 1: Get tenant and logged-in employee
Guid tenantId = _userHelper.GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Step 2: Check if user has permission to manage master data
var hasManageMasterPermission = await _permissionService.HasPermission(Manage_Master, loggedInEmployee.Id);
if (!hasManageMasterPermission)
{
_logger.LogWarning("Access denied for employeeId: {EmployeeId}", loggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("You don't have access", "Don't have access to take action", 403);
}
// Step 3: Check if work status with the same name already exists
var existingWorkStatus = await _context.WorkStatusMasters
.FirstOrDefaultAsync(ws => ws.Name == createWorkStatusDto.Name && ws.TenantId == tenantId);
if (existingWorkStatus != null)
{
_logger.LogWarning("Work status already exists: {Name}", createWorkStatusDto.Name ?? "");
return ApiResponse<object>.ErrorResponse("Work status already exists", "Work status already exists", 400);
}
// Step 4: Create new WorkStatusMaster entry
var workStatus = new WorkStatusMaster
{
Name = createWorkStatusDto.Name?.Trim() ?? "",
Description = createWorkStatusDto.Description?.Trim() ?? "",
IsSystem = false,
TenantId = tenantId
};
_context.WorkStatusMasters.Add(workStatus);
await _context.SaveChangesAsync();
_logger.LogInfo("Work status created successfully: {Id}, Name: {Name}", workStatus.Id, workStatus.Name);
return ApiResponse<object>.SuccessResponse(workStatus, "Work status created successfully", 200);
}
catch (Exception ex)
{
_logger.LogError("Error occurred while creating work status : {Error}", ex.Message);
return ApiResponse<object>.ErrorResponse("An error occurred", "Unable to create work status", 500);
}
}
public async Task<ApiResponse<object>> UpdateWorkStatus(Guid id, UpdateWorkStatusMasterDto updateWorkStatusDto)
{
_logger.LogInfo("UpdateWorkStatus called for WorkStatus ID: {Id}, New Name: {Name}", id, updateWorkStatusDto.Name ?? "");
try
{
// Step 1: Validate input
if (id == Guid.Empty || id != updateWorkStatusDto.Id)
{
_logger.LogWarning("Invalid ID provided for update. Route ID: {RouteId}, DTO ID: {DtoId}", id, updateWorkStatusDto.Id);
return ApiResponse<object>.ErrorResponse("Invalid data provided", "The provided work status ID is invalid", 400);
}
// Step 2: Get tenant and logged-in employee
Guid tenantId = _userHelper.GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Step 3: Check permissions
var hasManageMasterPermission = await _permissionService.HasPermission(Manage_Master, loggedInEmployee.Id);
if (!hasManageMasterPermission)
{
_logger.LogWarning("Access denied. EmployeeId: {EmployeeId} does not have Manage Master permission.", loggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("Access denied", "You do not have permission to update this work status", 403);
}
// Step 4: Retrieve the work status record
var workStatus = await _context.WorkStatusMasters
.FirstOrDefaultAsync(ws => ws.Id == id && ws.TenantId == tenantId);
if (workStatus == null)
{
_logger.LogWarning("Work status not found for ID: {Id}", id);
return ApiResponse<object>.ErrorResponse("Work status not found", "No work status found with the provided ID", 404);
}
// Step 5: Check for duplicate name (optional)
var isDuplicate = await _context.WorkStatusMasters
.AnyAsync(ws => ws.Name == updateWorkStatusDto.Name && ws.Id != id && ws.TenantId == tenantId);
if (isDuplicate)
{
_logger.LogWarning("Duplicate work status name '{Name}' detected during update. ID: {Id}", updateWorkStatusDto.Name ?? "", id);
return ApiResponse<object>.ErrorResponse("Work status with the same name already exists", "Duplicate name", 400);
}
// Step 6: Update fields
workStatus.Name = updateWorkStatusDto.Name?.Trim() ?? "";
workStatus.Description = updateWorkStatusDto.Description?.Trim() ?? "";
await _context.SaveChangesAsync();
_logger.LogInfo("Work status updated successfully. ID: {Id}", id);
return ApiResponse<object>.SuccessResponse(workStatus, "Work status updated successfully", 200);
}
catch (Exception ex)
{
_logger.LogError("Error occurred while updating work status ID: {Id} : {Error}", id, ex.Message);
return ApiResponse<object>.ErrorResponse("An error occurred", "Unable to update the work status at this time", 500);
}
}
public async Task<ApiResponse<object>> DeleteWorkStatus(Guid id)
{
_logger.LogInfo("DeleteWorkStatus called for Id: {Id}", id);
try
{
// Step 1: Get current tenant and logged-in employee
Guid tenantId = _userHelper.GetTenantId();
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Step 2: Check permission to manage master data
var hasManageMasterPermission = await _permissionService.HasPermission(Manage_Master, loggedInEmployee.Id);
if (!hasManageMasterPermission)
{
_logger.LogWarning("Delete denied. EmployeeId: {EmployeeId} lacks Manage_Master permission.", loggedInEmployee.Id);
return ApiResponse<object>.ErrorResponse("You don't have access", "Access denied for deleting work status", 403);
}
// Step 3: Find the work status
var workStatus = await _context.WorkStatusMasters
.FirstOrDefaultAsync(ws => ws.Id == id && ws.TenantId == tenantId);
if (workStatus == null)
{
_logger.LogWarning("Work status not found for Id: {Id}", id);
return ApiResponse<object>.ErrorResponse("Work status not found", "Work status not found", 404);
}
// Step 4: Check for dependencies in TaskAllocations
bool hasDependency = await _context.TaskAllocations
.AnyAsync(ta => ta.TenantId == tenantId && ta.WorkStatusId == id);
if (hasDependency)
{
_logger.LogWarning("Cannot delete WorkStatus Id: {Id} due to existing task dependency", id);
return ApiResponse<object>.ErrorResponse(
"Work status has a dependency in assigned tasks and cannot be deleted",
"Deletion failed due to associated tasks",
400
);
}
// Step 5: Delete and persist
_context.WorkStatusMasters.Remove(workStatus);
await _context.SaveChangesAsync();
_logger.LogInfo("Work status deleted successfully. Id: {Id}", id);
return ApiResponse<object>.SuccessResponse(new { }, "Work status deleted successfully", 200);
}
catch (Exception ex)
{
_logger.LogError("Error occurred while deleting WorkStatus Id: {Id} : {Error}", id, ex.Message);
return ApiResponse<object>.ErrorResponse("An error occurred", "Unable to delete work status", 500);
}
}
} }
} }

View File

@ -0,0 +1,35 @@
using MarcoBMS.Services.Service;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
namespace Marco.Pms.Services.Hubs
{
[Authorize]
public class MarcoHub : Hub
{
private readonly ILoggingService _logger;
public MarcoHub(ILoggingService logger)
{
_logger = logger;
}
public async Task SendMessage(string user, string message)
{
_logger.LogInfo($"User: {user} Message : {message}");
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
_logger.LogInfo($"Connected successfully");
await Clients.All.SendAsync("Connected successfully");
}
// Optional: override OnDisconnectedAsync
public override async Task OnDisconnectedAsync(Exception? exception)
{
await base.OnDisconnectedAsync(exception);
_logger.LogInfo($"DIsonnected successfully");
await Clients.All.SendAsync("Disonnected successfully");
}
}
}

View File

@ -17,6 +17,7 @@
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.12" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.12" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.12" /> <PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.12" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.7" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.7" />
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.12"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.12">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@ -4,6 +4,7 @@ using Marco.Pms.Model.Authentication;
using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Helpers;
using Marco.Pms.Services.Hubs;
using Marco.Pms.Services.Service; using Marco.Pms.Services.Service;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Middleware; using MarcoBMS.Services.Middleware;
@ -59,7 +60,8 @@ builder.Services.AddCors(options =>
{ {
policy.AllowAnyOrigin() policy.AllowAnyOrigin()
.AllowAnyMethod() .AllowAnyMethod()
.AllowAnyHeader(); .AllowAnyHeader()
.WithExposedHeaders("Authorization");
}); });
}); });
@ -161,10 +163,28 @@ if (jwtSettings != null && jwtSettings.Key != null)
ValidAudience = jwtSettings.Audience, ValidAudience = jwtSettings.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key)) IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Key))
}; };
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
// Match your hub route here
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs/marco"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
}); });
builder.Services.AddSingleton(jwtSettings); builder.Services.AddSingleton(jwtSettings);
} }
builder.Services.AddSignalR();
builder.WebHost.ConfigureKestrel(options => builder.WebHost.ConfigureKestrel(options =>
{ {
options.AddServerHeader = false; // Disable the "Server" header options.AddServerHeader = false; // Disable the "Server" header
@ -207,7 +227,7 @@ app.UseHttpsRedirection();
app.UseAuthorization(); app.UseAuthorization();
app.MapHub<MarcoHub>("/hubs/marco");
app.MapControllers(); app.MapControllers();
app.Run(); app.Run();

View File

@ -28,53 +28,46 @@ namespace Marco.Pms.Services.Service
_s3Client = new AmazonS3Client(settings.AccessKey, settings.SecretKey, region); _s3Client = new AmazonS3Client(settings.AccessKey, settings.SecretKey, region);
} }
//public async Task<string> UploadFileAsync(string fileName, string contentType) //public async Task<string> UploadFileAsync(string fileName, string contentType)
public async Task<string> UploadFileAsync(string base64Data, Guid tenantId, string tag) public async Task UploadFileAsync(string base64, string fileType, string objectKey)
{ {
byte[] fileBytes; byte[] fileBytes;
//If base64 has a data URI prefix, strip it
var base64 = base64Data.Contains(",")
? base64Data.Substring(base64Data.IndexOf(",") + 1)
: base64Data;
var allowedFilesType = _configuration.GetSection("WhiteList:ContentType") var allowedFilesType = _configuration.GetSection("WhiteList:ContentType")
.GetChildren() .GetChildren()
.Select(x => x.Value) .Select(x => x.Value)
.ToList(); .ToList();
string fileType = GetContentTypeFromBase64(base64);
if (allowedFilesType != null && allowedFilesType.Contains(fileType)) if (allowedFilesType == null || !allowedFilesType.Contains(fileType))
{ {
string fileName = GenerateFileName(fileType, tenantId, tag); throw new InvalidOperationException("Unsupported file type.");
fileBytes = Convert.FromBase64String(base64);
using var fileStream = new MemoryStream(fileBytes);
// Generate a unique object key (you can customize this)
var objectKey = $"{fileName}";
var uploadRequest = new TransferUtilityUploadRequest
{
InputStream = fileStream,
Key = objectKey,
BucketName = _bucketName,
ContentType = fileType,
AutoCloseStream = true
};
try
{
var transferUtility = new TransferUtility(_s3Client);
await transferUtility.UploadAsync(uploadRequest);
_logger.LogInfo("File uploaded to Amazon S3");
return objectKey;
}
catch (Exception ex)
{
_logger.LogError("{error} while uploading file to S3", ex.Message);
return string.Empty;
}
} }
throw new InvalidOperationException("Unsupported file type.");
fileBytes = Convert.FromBase64String(base64);
using var fileStream = new MemoryStream(fileBytes);
var uploadRequest = new TransferUtilityUploadRequest
{
InputStream = fileStream,
Key = objectKey,
BucketName = _bucketName,
ContentType = fileType,
AutoCloseStream = true
};
try
{
var transferUtility = new TransferUtility(_s3Client);
await transferUtility.UploadAsync(uploadRequest);
_logger.LogInfo("File uploaded to Amazon S3");
}
catch (Exception ex)
{
_logger.LogError("{error} while uploading file to S3", ex.Message);
}
} }
public string GeneratePreSignedUrlAsync(string objectKey) public string GeneratePreSignedUrlAsync(string objectKey)
{ {

View File

@ -9,7 +9,7 @@
"Title": "Dev" "Title": "Dev"
}, },
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnectionString": "Server=147.93.98.152;User ID=devuser;Password=AppUser@123$;Database=MarcoBMSGuid" "DefaultConnectionString": "Server=147.93.98.152;User ID=devuser;Password=AppUser@123$;Database=MarcoBMS1"
}, },
"SmtpSettings": { "SmtpSettings": {
"SmtpServer": "smtp.gmail.com", "SmtpServer": "smtp.gmail.com",