Compare commits

..

35 Commits

Author SHA1 Message Date
a0a65fc08c Saving the mail logs in mongodb rather than my sql 2025-12-06 15:55:08 +05:30
9b8bb5d0cb Merge pull request 'SideMenu_Management' (#156) from SideMenu_Management into Purchase_Invoice_Management
Reviewed-on: #156
2025-12-06 10:02:20 +00:00
20df833c48 Added API to get side menu for mobile 2025-12-06 15:14:57 +05:30
8470223f98 Merge branch 'Finance_Dashboards' of https://git.marcoaiot.com/admin/marco.pms.api into SideMenu_Management 2025-12-06 14:13:53 +05:30
c5949606aa Changed the text to name 2025-12-06 12:49:46 +05:30
4c284f9904 Modified side menu APIs 2025-12-06 12:06:41 +05:30
852ddc7e02 Optimized the side menu APIs 2025-12-05 19:06:21 +05:30
6bcc67bb63 Added an API to get purchase invoice overview 2025-12-05 15:40:26 +05:30
fdb08fae89 Solved the migration from purchase invoice branch 2025-12-05 14:49:12 +05:30
94e2e4f18b Added an API to get collection overview 2025-12-05 14:42:15 +05:30
0960702f0c Added the new entites in invoice attachment type table 2025-12-04 14:32:50 +05:30
26ac59fa52 changed msg if invoice-payment exceed to tax profoma amount 2025-12-04 13:03:16 +05:30
1a3c030495 Added proform amount check in add payment API 2025-12-04 10:02:01 +05:30
447e915505 Added proforma related information in purchase inovice list VM 2025-12-03 15:43:14 +05:30
3caf944c50 Added globle search in purchase invoices (title, UID) 2025-12-03 14:59:42 +05:30
5d8a5e0cc8 Added an API to get todays attendance record for current logged-in-employee 2025-12-03 11:13:41 +05:30
a4714d5440 Added an API to deactivate or activate the purchase invoice 2025-12-01 12:37:35 +05:30
4b981b6c74 Added the purchase invoice related permissions 2025-12-01 12:07:16 +05:30
e92976049e Added an APIs get payment history list and add payment to purchase invoices 2025-12-01 11:01:20 +05:30
28deae6416 Added size and upload at in mapping for purchase invoice attachments 2025-12-01 10:26:26 +05:30
1746cf0300 Changed the condition to check the new attachments 2025-12-01 10:00:41 +05:30
9e7651f345 Corrected the logic of updating images and add file size in details View model 2025-12-01 09:49:41 +05:30
49da601092 Added filter by id as well in both project list API and basic Organization list API 2025-11-27 15:13:26 +05:30
0fe59223e2 Added an API to get list of delivery challan by purchase invoice ID 2025-11-27 14:42:38 +05:30
41feb58d45 Added An API to add delivery challan to purchase invoice 2025-11-27 12:28:21 +05:30
34c5ac9c25 Added an API to update purchase invoice 2025-11-26 19:30:22 +05:30
3dce559de2 Added an API to get details of the purchase invoice 2025-11-26 18:02:28 +05:30
886a32b3e3 Added an API to get list of purchase invoices wth filter 2025-11-26 15:00:44 +05:30
b6baff7d00 Added an API to get basic list of organizations 2025-11-26 12:47:50 +05:30
bbe36ed535 Added an API create the Purchase invoice 2025-11-26 12:07:11 +05:30
fa1c534ba8 Added Status In Purchase Invoice Details Table 2025-11-25 20:04:59 +05:30
d1f5240f8f Corrected the invoice attachment type APIs endpoint 2025-11-25 19:21:35 +05:30
c4653b557c Added an API to get a list of Invoice attachment types 2025-11-25 19:12:29 +05:30
8aace3e1d9 Added an API to get Purchase invoice status list API 2025-11-25 18:58:46 +05:30
a6177adb43 Added PurchaseInvoice Related Tables 2025-11-25 18:38:13 +05:30
75 changed files with 53041 additions and 1080 deletions

View File

@ -14,6 +14,7 @@ using Marco.Pms.Model.Master;
using Marco.Pms.Model.OrganizationModel; using Marco.Pms.Model.OrganizationModel;
using Marco.Pms.Model.PaymentGetway; using Marco.Pms.Model.PaymentGetway;
using Marco.Pms.Model.Projects; using Marco.Pms.Model.Projects;
using Marco.Pms.Model.PurchaseInvoice;
using Marco.Pms.Model.Roles; using Marco.Pms.Model.Roles;
using Marco.Pms.Model.ServiceProject; using Marco.Pms.Model.ServiceProject;
using Marco.Pms.Model.TenantModels; using Marco.Pms.Model.TenantModels;
@ -54,7 +55,7 @@ namespace Marco.Pms.DataAccess.Data
public DbSet<Document> Documents { get; set; } public DbSet<Document> Documents { get; set; }
public DbSet<MailingList> MailingList { get; set; } public DbSet<MailingList> MailingList { get; set; }
public DbSet<MailDetails> MailDetails { get; set; } public DbSet<MailDetails> MailDetails { get; set; }
public DbSet<MailLog> MailLogs { get; set; } //public DbSet<MailLog> MailLogs { get; set; }
public DbSet<OTPDetails> OTPDetails { get; set; } public DbSet<OTPDetails> OTPDetails { get; set; }
public DbSet<MPINDetails> MPINDetails { get; set; } public DbSet<MPINDetails> MPINDetails { get; set; }
public DbSet<FCMTokenMapping> FCMTokenMappings { get; set; } public DbSet<FCMTokenMapping> FCMTokenMappings { get; set; }
@ -238,6 +239,14 @@ namespace Marco.Pms.DataAccess.Data
#endregion #endregion
#region ======================================================= Purchase Invoice =======================================================
public DbSet<PurchaseInvoiceDetails> PurchaseInvoiceDetails { get; set; }
public DbSet<DeliveryChallanDetails> DeliveryChallanDetails { get; set; }
public DbSet<PurchaseInvoiceAttachment> PurchaseInvoiceAttachments { get; set; }
public DbSet<PurchaseInvoicePayment> PurchaseInvoicePayments { get; set; }
public DbSet<PurchaseInvoiceStatus> PurchaseInvoiceStatus { get; set; }
public DbSet<InvoiceAttachmentType> InvoiceAttachmentTypes { get; set; }
#endregion
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
@ -554,8 +563,102 @@ namespace Marco.Pms.DataAccess.Data
} }
); );
modelBuilder.Entity<InvoiceAttachmentType>().HasData(
new InvoiceAttachmentType
{
Id = Guid.Parse("ca294108-a586-4207-88c8-163b24305ddc"),
Name = "Delivery Challan",
Description = "A delivery challan is a formal document accompanying a shipment of goods that lists the items included and serves as proof of delivery upon receipt."
},
new InvoiceAttachmentType
{
Id = Guid.Parse("150ddd9b-4b8d-44ac-bae0-2e553c0f069a"),
Name = "E Way Bill",
Description = "An E-Way Bill (Electronic Way Bill) is a mandatory digital document generated on the GST portal to evidence and track the movement of goods valued over ₹50,000."
},
new InvoiceAttachmentType
{
Id = Guid.Parse("3ca08288-0a74-4850-9948-0783aa975b84"),
Name = "Tax Invoice",
Description = "A Tax Invoice is a mandatory legal document issued by a GST-registered supplier for taxable goods or services, enabling the buyer to claim Input Tax Credit (ITC)."
},
new InvoiceAttachmentType
{
Id = Guid.Parse("1fa20cff-b0ee-468e-9ea6-72d5aa144a3f"),
Name = "E-Invoice",
Description = "An E-Invoice (Electronic Invoice) is a system where B2B invoices are electronically authenticated by the GST Network (GSTN) to generate a unique Invoice Reference Number (IRN) and QR code."
},
new InvoiceAttachmentType
{
Id = Guid.Parse("31cd7533-3ffc-4e84-a0b4-db3b94d016b2"),
Name = "Proforma Invoice",
Description = "Proforma Invoice"
},
new InvoiceAttachmentType
{
Id = Guid.Parse("060c79a4-81c7-40a4-8cc3-56362ac9fad6"),
Name = "Sales Order",
Description = "Sales Order"
},
new InvoiceAttachmentType
{
Id = Guid.Parse("12773c2c-64e7-478c-af17-8471f943a5ed"),
Name = "Other",
Description = "Other"
} }
);
modelBuilder.Entity<PurchaseInvoiceStatus>().HasData(
new PurchaseInvoiceStatus
{
Id = Guid.Parse("8a5ef25e-3c9e-45de-add9-6b1c1df54381"),
Name = "Draft",
DisplayName = "Draft",
Description = "Draft Status in a Purchase Invoice indicates a preliminary, unfinalized document that is saved for review but has not yet been posted to the general ledger or affected your accounts/inventory.",
Color = "#8592a3"
},
new PurchaseInvoiceStatus
{
Id = Guid.Parse("16b10201-1651-465c-b2fd-236bdef86f95"),
Name = "Review Pending",
DisplayName = "Submit for Review",
Description = "Review Pending status in a Purchase Invoice indicates that the invoice has been submitted for validation but requires approval from an authorized person (like a manager or auditor) before it can be posted to the ledger or paid.",
Color = "#696cff"
},
new PurchaseInvoiceStatus
{
Id = Guid.Parse("a05f5f4a-bd9d-4028-af42-48ee0caa3e40"),
Name = "Rejected by Reviewer",
DisplayName = "Reject",
Description = "Rejected by Reviewer status indicates that the invoice failed the approval process due to errors, discrepancies, or policy violations and has been returned to the initiator or vendor for correction.",
Color = "#ff3e1d"
},
new PurchaseInvoiceStatus
{
Id = Guid.Parse("60027a54-3c23-4619-9f4e-6c20549b50a6"),
Name = "Approval Pending",
DisplayName = "Mark as Reviewed",
Description = "Approval Pending status in a Purchase Invoice indicates that the document has passed initial verification (matching and coding) and is now awaiting final financial authorization from a designated budget holder or signatory.",
Color = "#03c3ec"
},
new PurchaseInvoiceStatus
{
Id = Guid.Parse("58de9cef-811f-46a4-814d-0069b64d98a9"),
Name = "Rejected by Approver",
DisplayName = "Reject",
Description = "Rejected by Approver status in a Purchase Invoice indicates that the document successfully passed initial verification but was ultimately denied by the final authorizing signatory (such as a Manager or CFO) due to budget or validity concerns.",
Color = "#ff3e1d"
},
new PurchaseInvoiceStatus
{
Id = Guid.Parse("5b393371-dbcf-4a28-88a8-f406fa34e0d0"),
Name = "Approved",
DisplayName = "Mark as Approved",
Description = "Approved status indicates that the invoice has successfully cleared all necessary verification and authorization levels and is formally accepted by the company as a valid debt.",
Color = "#71dd37"
}
);
}
private static void ManageApplicationStructure(ModelBuilder modelBuilder) private static void ManageApplicationStructure(ModelBuilder modelBuilder)
{ {
// Configure ApplicationRole to Tenant relationship (if Tenant exists) // Configure ApplicationRole to Tenant relationship (if Tenant exists)
@ -1330,6 +1433,7 @@ namespace Marco.Pms.DataAccess.Data
new Module { Id = new Guid("2a231490-bcb1-4bdd-91f1-f25fb7f25b23"), Name = "Employee", Description = "Employee Module", Key = "0971c7fb-6ce1-458a-ae3f-8d3205893637" }, new Module { Id = new Guid("2a231490-bcb1-4bdd-91f1-f25fb7f25b23"), Name = "Employee", Description = "Employee Module", Key = "0971c7fb-6ce1-458a-ae3f-8d3205893637" },
new Module { Id = new Guid("c43db8c7-ab73-47f4-9d3b-f83e81357924"), Name = "Masters", Description = "Masters Module", Key = "504ec132-e6a9-422f-8f85-050602cfce05" }, new Module { Id = new Guid("c43db8c7-ab73-47f4-9d3b-f83e81357924"), Name = "Masters", Description = "Masters Module", Key = "504ec132-e6a9-422f-8f85-050602cfce05" },
new Module { Id = new Guid("f482a079-4dec-4f2d-9867-6baf2a4f23d9"), Name = "Tenant", Description = "Tenant Module", Key = "504ec132-e6a9-422f-8f85-050602cfce05" }, new Module { Id = new Guid("f482a079-4dec-4f2d-9867-6baf2a4f23d9"), Name = "Tenant", Description = "Tenant Module", Key = "504ec132-e6a9-422f-8f85-050602cfce05" },
new Module { Id = new Guid("74e7af50-d55f-4b59-a724-9847ceb7bc17"), Name = "Inventory", Description = "Inventory Module", Key = "504ec132-e6a9-422f-8f85-050602cfce05" },
new Module { Id = new Guid("0a79687a-86d7-430d-a2d7-8b8603cc76a1"), Name = "Finance", Description = "Finance Module", Key = "504ec132-e6a9-422f-8f85-050602cfce05" } new Module { Id = new Guid("0a79687a-86d7-430d-a2d7-8b8603cc76a1"), Name = "Finance", Description = "Finance Module", Key = "504ec132-e6a9-422f-8f85-050602cfce05" }
); );
@ -1353,6 +1457,9 @@ namespace Marco.Pms.DataAccess.Data
new Feature { Id = new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), Description = "Managing all directory related rights", Name = "Directory Management", ModuleId = new Guid("c43db8c7-ab73-47f4-9d3b-f83e81357924"), IsActive = true }, new Feature { Id = new Guid("39e66f81-efc6-446c-95bd-46bff6cfb606"), Description = "Managing all directory related rights", Name = "Directory Management", ModuleId = new Guid("c43db8c7-ab73-47f4-9d3b-f83e81357924"), IsActive = true },
new Feature { Id = new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914"), Description = "Managing all organization related rights", Name = "Organization Management", ModuleId = new Guid("c43db8c7-ab73-47f4-9d3b-f83e81357924"), IsActive = true }, new Feature { Id = new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914"), Description = "Managing all organization related rights", Name = "Organization Management", ModuleId = new Guid("c43db8c7-ab73-47f4-9d3b-f83e81357924"), IsActive = true },
// Inventory Module
new Feature { Id = new Guid("271cc47f-7b05-46c7-b5ae-ef0177ec3b60"), Description = "Managing all Purchase invoice related rights", Name = "Purchase Invoice Management", ModuleId = new Guid("74e7af50-d55f-4b59-a724-9847ceb7bc17"), IsActive = true },
// Tenant Module // Tenant Module
new Feature { Id = new Guid("2f3509b7-160d-410a-b9b6-daadd96c986d"), Description = "Managing all tenant related rights", Name = "Tenant Management", ModuleId = new Guid("f482a079-4dec-4f2d-9867-6baf2a4f23d9"), IsActive = true } new Feature { Id = new Guid("2f3509b7-160d-410a-b9b6-daadd96c986d"), Description = "Managing all tenant related rights", Name = "Tenant Management", ModuleId = new Guid("f482a079-4dec-4f2d-9867-6baf2a4f23d9"), IsActive = true }
); );
@ -1427,7 +1534,14 @@ namespace Marco.Pms.DataAccess.Data
// Organization Management Feature // Organization Management Feature
new FeaturePermission { Id = new Guid("068cb3c1-49c5-4746-9f29-1fce16e820ac"), FeatureId = new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914"), IsEnabled = true, Name = "Add Organization", Description = "Allow user to create new organization" }, new FeaturePermission { Id = new Guid("068cb3c1-49c5-4746-9f29-1fce16e820ac"), FeatureId = new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914"), IsEnabled = true, Name = "Add Organization", Description = "Allow user to create new organization" },
new FeaturePermission { Id = new Guid("c1ae1363-ab8a-4bd9-a9d1-8c2c6083873a"), FeatureId = new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914"), IsEnabled = true, Name = "Edit Organization", Description = "Allow the user to update the basic information of the organization" }, new FeaturePermission { Id = new Guid("c1ae1363-ab8a-4bd9-a9d1-8c2c6083873a"), FeatureId = new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914"), IsEnabled = true, Name = "Edit Organization", Description = "Allow the user to update the basic information of the organization" },
new FeaturePermission { Id = new Guid("7a6cf830-0008-4e03-b31d-0d050cb634f4"), FeatureId = new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914"), IsEnabled = true, Name = "View Organization", Description = "Allow the user to view information of the organization" } new FeaturePermission { Id = new Guid("7a6cf830-0008-4e03-b31d-0d050cb634f4"), FeatureId = new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914"), IsEnabled = true, Name = "View Organization", Description = "Allow the user to view information of the organization" },
// Purchase Invoice Management Feature
new FeaturePermission { Id = new Guid("91e09825-512a-465e-82ad-fa355b305585"), FeatureId = new Guid("271cc47f-7b05-46c7-b5ae-ef0177ec3b60"), IsEnabled = true, Name = "View Self Purchase Invoice", Description = "Allows the user to view only the purchase invoices they created." },
new FeaturePermission { Id = new Guid("d6ae78d3-a941-4cc4-8d0a-d40479be4211"), FeatureId = new Guid("271cc47f-7b05-46c7-b5ae-ef0177ec3b60"), IsEnabled = true, Name = "View All Purchase Invoice", Description = "Allows the user to view all purchase invoices across the entire organization." },
new FeaturePermission { Id = new Guid("68ff925d-8ebf-4034-a137-8d3317c56ca1"), FeatureId = new Guid("271cc47f-7b05-46c7-b5ae-ef0177ec3b60"), IsEnabled = true, Name = "Manage Purchase Invoice", Description = "Allows full control to create, edit, and process purchase invoices." },
new FeaturePermission { Id = new Guid("a4b77638-bf31-42bb-afd4-d5bbd15ccadc"), FeatureId = new Guid("271cc47f-7b05-46c7-b5ae-ef0177ec3b60"), IsEnabled = true, Name = "Delete Purchase Invoice", Description = "Allows the user to mark purchase invoices as inactive or void." },
new FeaturePermission { Id = new Guid("b24eba39-4a92-4f7a-b33b-b5308fbc48b9"), FeatureId = new Guid("271cc47f-7b05-46c7-b5ae-ef0177ec3b60"), IsEnabled = true, Name = "Add Delivery Challan", Description = "Allows the user to create delivery challans for purchase invoices." }
); );

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,423 @@
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_PurchaseInvoice_Related_Tables : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_AdvancePaymentTransactions_Projects_ProjectId",
table: "AdvancePaymentTransactions");
migrationBuilder.DropIndex(
name: "IX_AdvancePaymentTransactions_ProjectId",
table: "AdvancePaymentTransactions");
migrationBuilder.CreateTable(
name: "InvoiceAttachmentTypes",
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")
},
constraints: table =>
{
table.PrimaryKey("PK_InvoiceAttachmentTypes", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "PurchaseInvoiceDetails",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
UIDPrefix = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
UIDPostfix = table.Column<int>(type: "int", nullable: false),
Title = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Description = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ProjectId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
OrganizationId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
BillingAddress = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ShippingAddress = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
PurchaseOrderNumber = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
PurchaseOrderDate = table.Column<DateTime>(type: "datetime(6)", nullable: true),
SupplierId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
ProformaInvoiceNumber = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
ProformaInvoiceDate = table.Column<DateTime>(type: "datetime(6)", nullable: true),
ProformaInvoiceAmount = table.Column<double>(type: "double", nullable: true),
InvoiceNumber = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
InvoiceDate = table.Column<DateTime>(type: "datetime(6)", nullable: true),
EWayBillNumber = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
EWayBillDate = table.Column<DateTime>(type: "datetime(6)", nullable: true),
InvoiceReferenceNumber = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
AcknowledgmentNumber = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
AcknowledgmentDate = table.Column<DateTime>(type: "datetime(6)", nullable: true),
BaseAmount = table.Column<double>(type: "double", nullable: false),
TaxAmount = table.Column<double>(type: "double", nullable: false),
TransportCharges = table.Column<double>(type: "double", nullable: true),
TotalAmount = table.Column<double>(type: "double", nullable: false),
PaymentDueDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false),
CreatedById = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UpdatedById = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
UpdatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: true),
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
},
constraints: table =>
{
table.PrimaryKey("PK_PurchaseInvoiceDetails", x => x.Id);
table.ForeignKey(
name: "FK_PurchaseInvoiceDetails_Employees_CreatedById",
column: x => x.CreatedById,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PurchaseInvoiceDetails_Employees_UpdatedById",
column: x => x.UpdatedById,
principalTable: "Employees",
principalColumn: "Id");
table.ForeignKey(
name: "FK_PurchaseInvoiceDetails_Organizations_OrganizationId",
column: x => x.OrganizationId,
principalTable: "Organizations",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PurchaseInvoiceDetails_Organizations_SupplierId",
column: x => x.SupplierId,
principalTable: "Organizations",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PurchaseInvoiceDetails_Tenants_TenantId",
column: x => x.TenantId,
principalTable: "Tenants",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "PurchaseInvoiceStatus",
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"),
DisplayName = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Color = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Description = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_PurchaseInvoiceStatus", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "PurchaseInvoiceAttachments",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
PurchaseInvoiceId = 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"),
UploadedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
UploadedById = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
},
constraints: table =>
{
table.PrimaryKey("PK_PurchaseInvoiceAttachments", x => x.Id);
table.ForeignKey(
name: "FK_PurchaseInvoiceAttachments_Documents_DocumentId",
column: x => x.DocumentId,
principalTable: "Documents",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PurchaseInvoiceAttachments_Employees_UploadedById",
column: x => x.UploadedById,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PurchaseInvoiceAttachments_PurchaseInvoiceDetails_PurchaseIn~",
column: x => x.PurchaseInvoiceId,
principalTable: "PurchaseInvoiceDetails",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PurchaseInvoiceAttachments_Tenants_TenantId",
column: x => x.TenantId,
principalTable: "Tenants",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "PurchaseInvoicePayments",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
InvoiceId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
PaymentReceivedDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
TransactionId = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Amount = table.Column<double>(type: "double", nullable: false),
Comment = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
IsActive = table.Column<bool>(type: "tinyint(1)", nullable: false),
PaymentAdjustmentHeadId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
CreatedById = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
},
constraints: table =>
{
table.PrimaryKey("PK_PurchaseInvoicePayments", x => x.Id);
table.ForeignKey(
name: "FK_PurchaseInvoicePayments_Employees_CreatedById",
column: x => x.CreatedById,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PurchaseInvoicePayments_PaymentAdjustmentHeads_PaymentAdjust~",
column: x => x.PaymentAdjustmentHeadId,
principalTable: "PaymentAdjustmentHeads",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PurchaseInvoicePayments_PurchaseInvoiceDetails_InvoiceId",
column: x => x.InvoiceId,
principalTable: "PurchaseInvoiceDetails",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PurchaseInvoicePayments_Tenants_TenantId",
column: x => x.TenantId,
principalTable: "Tenants",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "DeliveryChallanDetails",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
DeliveryChallanNumber = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
DeliveryChallanDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
Description = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
PurchaseInvoiceId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
AttachmentId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false),
CreatedById = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci")
},
constraints: table =>
{
table.PrimaryKey("PK_DeliveryChallanDetails", x => x.Id);
table.ForeignKey(
name: "FK_DeliveryChallanDetails_Employees_CreatedById",
column: x => x.CreatedById,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_DeliveryChallanDetails_PurchaseInvoiceAttachments_Attachment~",
column: x => x.AttachmentId,
principalTable: "PurchaseInvoiceAttachments",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_DeliveryChallanDetails_PurchaseInvoiceDetails_PurchaseInvoic~",
column: x => x.PurchaseInvoiceId,
principalTable: "PurchaseInvoiceDetails",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_DeliveryChallanDetails_Tenants_TenantId",
column: x => x.TenantId,
principalTable: "Tenants",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.InsertData(
table: "InvoiceAttachmentTypes",
columns: new[] { "Id", "Description", "Name" },
values: new object[,]
{
{ new Guid("150ddd9b-4b8d-44ac-bae0-2e553c0f069a"), "An E-Way Bill (Electronic Way Bill) is a mandatory digital document generated on the GST portal to evidence and track the movement of goods valued over ₹50,000.", "E Way Bill" },
{ new Guid("1fa20cff-b0ee-468e-9ea6-72d5aa144a3f"), "An E-Invoice (Electronic Invoice) is a system where B2B invoices are electronically authenticated by the GST Network (GSTN) to generate a unique Invoice Reference Number (IRN) and QR code.", "E-Invoice" },
{ new Guid("3ca08288-0a74-4850-9948-0783aa975b84"), "A Tax Invoice is a mandatory legal document issued by a GST-registered supplier for taxable goods or services, enabling the buyer to claim Input Tax Credit (ITC).", "Tax Invoice" },
{ new Guid("ca294108-a586-4207-88c8-163b24305ddc"), "A delivery challan is a formal document accompanying a shipment of goods that lists the items included and serves as proof of delivery upon receipt.", "Delivery Challan" }
});
migrationBuilder.InsertData(
table: "PurchaseInvoiceStatus",
columns: new[] { "Id", "Color", "Description", "DisplayName", "Name" },
values: new object[,]
{
{ new Guid("16b10201-1651-465c-b2fd-236bdef86f95"), "#696cff", "Review Pending status in a Purchase Invoice indicates that the invoice has been submitted for validation but requires approval from an authorized person (like a manager or auditor) before it can be posted to the ledger or paid.", "Submit for Review", "Review Pending" },
{ new Guid("58de9cef-811f-46a4-814d-0069b64d98a9"), "#ff3e1d", "Rejected by Approver status in a Purchase Invoice indicates that the document successfully passed initial verification but was ultimately denied by the final authorizing signatory (such as a Manager or CFO) due to budget or validity concerns.", "Reject", "Rejected by Approver" },
{ new Guid("5b393371-dbcf-4a28-88a8-f406fa34e0d0"), "#71dd37", "Approved status indicates that the invoice has successfully cleared all necessary verification and authorization levels and is formally accepted by the company as a valid debt.", "Mark as Approved", "Approved" },
{ new Guid("60027a54-3c23-4619-9f4e-6c20549b50a6"), "#03c3ec", "Approval Pending status in a Purchase Invoice indicates that the document has passed initial verification (matching and coding) and is now awaiting final financial authorization from a designated budget holder or signatory.", "Mark as Reviewed", "Approval Pending" },
{ new Guid("8a5ef25e-3c9e-45de-add9-6b1c1df54381"), "#8592a3", "Draft Status in a Purchase Invoice indicates a preliminary, unfinalized document that is saved for review but has not yet been posted to the general ledger or affected your accounts/inventory.", "Draft", "Draft" },
{ new Guid("a05f5f4a-bd9d-4028-af42-48ee0caa3e40"), "#ff3e1d", "Rejected by Reviewer status indicates that the invoice failed the approval process due to errors, discrepancies, or policy violations and has been returned to the initiator or vendor for correction.", "Reject", "Rejected by Reviewer" }
});
migrationBuilder.CreateIndex(
name: "IX_DeliveryChallanDetails_AttachmentId",
table: "DeliveryChallanDetails",
column: "AttachmentId");
migrationBuilder.CreateIndex(
name: "IX_DeliveryChallanDetails_CreatedById",
table: "DeliveryChallanDetails",
column: "CreatedById");
migrationBuilder.CreateIndex(
name: "IX_DeliveryChallanDetails_PurchaseInvoiceId",
table: "DeliveryChallanDetails",
column: "PurchaseInvoiceId");
migrationBuilder.CreateIndex(
name: "IX_DeliveryChallanDetails_TenantId",
table: "DeliveryChallanDetails",
column: "TenantId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseInvoiceAttachments_DocumentId",
table: "PurchaseInvoiceAttachments",
column: "DocumentId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseInvoiceAttachments_PurchaseInvoiceId",
table: "PurchaseInvoiceAttachments",
column: "PurchaseInvoiceId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseInvoiceAttachments_TenantId",
table: "PurchaseInvoiceAttachments",
column: "TenantId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseInvoiceAttachments_UploadedById",
table: "PurchaseInvoiceAttachments",
column: "UploadedById");
migrationBuilder.CreateIndex(
name: "IX_PurchaseInvoiceDetails_CreatedById",
table: "PurchaseInvoiceDetails",
column: "CreatedById");
migrationBuilder.CreateIndex(
name: "IX_PurchaseInvoiceDetails_OrganizationId",
table: "PurchaseInvoiceDetails",
column: "OrganizationId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseInvoiceDetails_SupplierId",
table: "PurchaseInvoiceDetails",
column: "SupplierId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseInvoiceDetails_TenantId",
table: "PurchaseInvoiceDetails",
column: "TenantId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseInvoiceDetails_UpdatedById",
table: "PurchaseInvoiceDetails",
column: "UpdatedById");
migrationBuilder.CreateIndex(
name: "IX_PurchaseInvoicePayments_CreatedById",
table: "PurchaseInvoicePayments",
column: "CreatedById");
migrationBuilder.CreateIndex(
name: "IX_PurchaseInvoicePayments_InvoiceId",
table: "PurchaseInvoicePayments",
column: "InvoiceId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseInvoicePayments_PaymentAdjustmentHeadId",
table: "PurchaseInvoicePayments",
column: "PaymentAdjustmentHeadId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseInvoicePayments_TenantId",
table: "PurchaseInvoicePayments",
column: "TenantId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DeliveryChallanDetails");
migrationBuilder.DropTable(
name: "InvoiceAttachmentTypes");
migrationBuilder.DropTable(
name: "PurchaseInvoicePayments");
migrationBuilder.DropTable(
name: "PurchaseInvoiceStatus");
migrationBuilder.DropTable(
name: "PurchaseInvoiceAttachments");
migrationBuilder.DropTable(
name: "PurchaseInvoiceDetails");
migrationBuilder.CreateIndex(
name: "IX_AdvancePaymentTransactions_ProjectId",
table: "AdvancePaymentTransactions",
column: "ProjectId");
migrationBuilder.AddForeignKey(
name: "FK_AdvancePaymentTransactions_Projects_ProjectId",
table: "AdvancePaymentTransactions",
column: "ProjectId",
principalTable: "Projects",
principalColumn: "Id");
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,84 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Added_Status_In_PurchaseInvoiceDetails_Table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "StatusId",
table: "PurchaseInvoiceDetails",
type: "char(36)",
nullable: false,
defaultValue: new Guid("8a5ef25e-3c9e-45de-add9-6b1c1df54381"),
collation: "ascii_general_ci");
migrationBuilder.AddColumn<Guid>(
name: "InvoiceAttachmentTypeId",
table: "PurchaseInvoiceAttachments",
type: "char(36)",
nullable: false,
defaultValue: new Guid("3ca08288-0a74-4850-9948-0783aa975b84"),
collation: "ascii_general_ci");
migrationBuilder.CreateIndex(
name: "IX_PurchaseInvoiceDetails_StatusId",
table: "PurchaseInvoiceDetails",
column: "StatusId");
migrationBuilder.CreateIndex(
name: "IX_PurchaseInvoiceAttachments_InvoiceAttachmentTypeId",
table: "PurchaseInvoiceAttachments",
column: "InvoiceAttachmentTypeId");
migrationBuilder.AddForeignKey(
name: "FK_PurchaseInvoiceAttachments_InvoiceAttachmentTypes_InvoiceAtt~",
table: "PurchaseInvoiceAttachments",
column: "InvoiceAttachmentTypeId",
principalTable: "InvoiceAttachmentTypes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.AddForeignKey(
name: "FK_PurchaseInvoiceDetails_PurchaseInvoiceStatus_StatusId",
table: "PurchaseInvoiceDetails",
column: "StatusId",
principalTable: "PurchaseInvoiceStatus",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_PurchaseInvoiceAttachments_InvoiceAttachmentTypes_InvoiceAtt~",
table: "PurchaseInvoiceAttachments");
migrationBuilder.DropForeignKey(
name: "FK_PurchaseInvoiceDetails_PurchaseInvoiceStatus_StatusId",
table: "PurchaseInvoiceDetails");
migrationBuilder.DropIndex(
name: "IX_PurchaseInvoiceDetails_StatusId",
table: "PurchaseInvoiceDetails");
migrationBuilder.DropIndex(
name: "IX_PurchaseInvoiceAttachments_InvoiceAttachmentTypeId",
table: "PurchaseInvoiceAttachments");
migrationBuilder.DropColumn(
name: "StatusId",
table: "PurchaseInvoiceDetails");
migrationBuilder.DropColumn(
name: "InvoiceAttachmentTypeId",
table: "PurchaseInvoiceAttachments");
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,78 @@
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_Purchase_Invoice_Permissions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.InsertData(
table: "Modules",
columns: new[] { "Id", "Description", "Key", "Name" },
values: new object[] { new Guid("74e7af50-d55f-4b59-a724-9847ceb7bc17"), "Inventory Module", "504ec132-e6a9-422f-8f85-050602cfce05", "Inventory" });
migrationBuilder.InsertData(
table: "Features",
columns: new[] { "Id", "Description", "IsActive", "ModuleId", "Name" },
values: new object[] { new Guid("271cc47f-7b05-46c7-b5ae-ef0177ec3b60"), "Managing all Purchase invoice related rights", true, new Guid("74e7af50-d55f-4b59-a724-9847ceb7bc17"), "Purchase Invoice Management" });
migrationBuilder.InsertData(
table: "FeaturePermissions",
columns: new[] { "Id", "Description", "FeatureId", "IsEnabled", "Name" },
values: new object[,]
{
{ new Guid("68ff925d-8ebf-4034-a137-8d3317c56ca1"), "Allows full control to create, edit, and process purchase invoices.", new Guid("271cc47f-7b05-46c7-b5ae-ef0177ec3b60"), true, "Manage Purchase Invoice" },
{ new Guid("91e09825-512a-465e-82ad-fa355b305585"), "Allows the user to view only the purchase invoices they created.", new Guid("271cc47f-7b05-46c7-b5ae-ef0177ec3b60"), true, "View Self Purchase Invoice" },
{ new Guid("a4b77638-bf31-42bb-afd4-d5bbd15ccadc"), "Allows the user to mark purchase invoices as inactive or void.", new Guid("271cc47f-7b05-46c7-b5ae-ef0177ec3b60"), true, "Delete Purchase Invoice" },
{ new Guid("b24eba39-4a92-4f7a-b33b-b5308fbc48b9"), "Allows the user to create delivery challans for purchase invoices.", new Guid("271cc47f-7b05-46c7-b5ae-ef0177ec3b60"), true, "Add Delivery Challan" },
{ new Guid("d6ae78d3-a941-4cc4-8d0a-d40479be4211"), "Allows the user to view all purchase invoices across the entire organization.", new Guid("271cc47f-7b05-46c7-b5ae-ef0177ec3b60"), true, "View All Purchase Invoice" }
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: "FeaturePermissions",
keyColumn: "Id",
keyValue: new Guid("68ff925d-8ebf-4034-a137-8d3317c56ca1"));
migrationBuilder.DeleteData(
table: "FeaturePermissions",
keyColumn: "Id",
keyValue: new Guid("91e09825-512a-465e-82ad-fa355b305585"));
migrationBuilder.DeleteData(
table: "FeaturePermissions",
keyColumn: "Id",
keyValue: new Guid("a4b77638-bf31-42bb-afd4-d5bbd15ccadc"));
migrationBuilder.DeleteData(
table: "FeaturePermissions",
keyColumn: "Id",
keyValue: new Guid("b24eba39-4a92-4f7a-b33b-b5308fbc48b9"));
migrationBuilder.DeleteData(
table: "FeaturePermissions",
keyColumn: "Id",
keyValue: new Guid("d6ae78d3-a941-4cc4-8d0a-d40479be4211"));
migrationBuilder.DeleteData(
table: "Features",
keyColumn: "Id",
keyValue: new Guid("271cc47f-7b05-46c7-b5ae-ef0177ec3b60"));
migrationBuilder.DeleteData(
table: "Modules",
keyColumn: "Id",
keyValue: new Guid("74e7af50-d55f-4b59-a724-9847ceb7bc17"));
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,46 @@
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_Invoice_Attachment_Type_Table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.InsertData(
table: "InvoiceAttachmentTypes",
columns: new[] { "Id", "Description", "Name" },
values: new object[,]
{
{ new Guid("060c79a4-81c7-40a4-8cc3-56362ac9fad6"), "Sales Order", "Sales Order" },
{ new Guid("12773c2c-64e7-478c-af17-8471f943a5ed"), "Other", "Other" },
{ new Guid("31cd7533-3ffc-4e84-a0b4-db3b94d016b2"), "Proforma Invoice", "Proforma Invoice" }
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DeleteData(
table: "InvoiceAttachmentTypes",
keyColumn: "Id",
keyValue: new Guid("060c79a4-81c7-40a4-8cc3-56362ac9fad6"));
migrationBuilder.DeleteData(
table: "InvoiceAttachmentTypes",
keyColumn: "Id",
keyValue: new Guid("12773c2c-64e7-478c-af17-8471f943a5ed"));
migrationBuilder.DeleteData(
table: "InvoiceAttachmentTypes",
keyColumn: "Id",
keyValue: new Guid("31cd7533-3ffc-4e84-a0b4-db3b94d016b2"));
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,42 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Marco.Pms.DataAccess.Migrations
{
/// <inheritdoc />
public partial class Removed_MaiLogs_Table : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "MailLogs");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "MailLogs",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
Body = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
EmailId = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
EmployeeId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
ProjectId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
TenantId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
TimeStamp = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MailLogs", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
}
}

View File

@ -2042,6 +2042,46 @@ namespace Marco.Pms.DataAccess.Migrations
FeatureId = new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914"), FeatureId = new Guid("6d4c82d6-dbce-48ab-b8b8-f785f4d8c914"),
IsEnabled = true, IsEnabled = true,
Name = "View Organization" Name = "View Organization"
},
new
{
Id = new Guid("91e09825-512a-465e-82ad-fa355b305585"),
Description = "Allows the user to view only the purchase invoices they created.",
FeatureId = new Guid("271cc47f-7b05-46c7-b5ae-ef0177ec3b60"),
IsEnabled = true,
Name = "View Self Purchase Invoice"
},
new
{
Id = new Guid("d6ae78d3-a941-4cc4-8d0a-d40479be4211"),
Description = "Allows the user to view all purchase invoices across the entire organization.",
FeatureId = new Guid("271cc47f-7b05-46c7-b5ae-ef0177ec3b60"),
IsEnabled = true,
Name = "View All Purchase Invoice"
},
new
{
Id = new Guid("68ff925d-8ebf-4034-a137-8d3317c56ca1"),
Description = "Allows full control to create, edit, and process purchase invoices.",
FeatureId = new Guid("271cc47f-7b05-46c7-b5ae-ef0177ec3b60"),
IsEnabled = true,
Name = "Manage Purchase Invoice"
},
new
{
Id = new Guid("a4b77638-bf31-42bb-afd4-d5bbd15ccadc"),
Description = "Allows the user to mark purchase invoices as inactive or void.",
FeatureId = new Guid("271cc47f-7b05-46c7-b5ae-ef0177ec3b60"),
IsEnabled = true,
Name = "Delete Purchase Invoice"
},
new
{
Id = new Guid("b24eba39-4a92-4f7a-b33b-b5308fbc48b9"),
Description = "Allows the user to create delivery challans for purchase invoices.",
FeatureId = new Guid("271cc47f-7b05-46c7-b5ae-ef0177ec3b60"),
IsEnabled = true,
Name = "Add Delivery Challan"
}); });
}); });
@ -2141,8 +2181,6 @@ namespace Marco.Pms.DataAccess.Migrations
b.HasIndex("EmployeeId"); b.HasIndex("EmployeeId");
b.HasIndex("ProjectId");
b.HasIndex("TenantId"); b.HasIndex("TenantId");
b.ToTable("AdvancePaymentTransactions"); b.ToTable("AdvancePaymentTransactions");
@ -3133,37 +3171,6 @@ namespace Marco.Pms.DataAccess.Migrations
b.ToTable("MailDetails"); b.ToTable("MailDetails");
}); });
modelBuilder.Entity("Marco.Pms.Model.Mail.MailLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<string>("Body")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("EmailId")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid?>("EmployeeId")
.HasColumnType("char(36)");
b.Property<Guid>("ProjectId")
.HasColumnType("char(36)");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
b.Property<DateTime>("TimeStamp")
.HasColumnType("datetime(6)");
b.HasKey("Id");
b.ToTable("MailLogs");
});
modelBuilder.Entity("Marco.Pms.Model.Mail.MailingList", b => modelBuilder.Entity("Marco.Pms.Model.Mail.MailingList", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -3643,6 +3650,14 @@ namespace Marco.Pms.DataAccess.Migrations
Name = "Organization Management" Name = "Organization Management"
}, },
new new
{
Id = new Guid("271cc47f-7b05-46c7-b5ae-ef0177ec3b60"),
Description = "Managing all Purchase invoice related rights",
IsActive = true,
ModuleId = new Guid("74e7af50-d55f-4b59-a724-9847ceb7bc17"),
Name = "Purchase Invoice Management"
},
new
{ {
Id = new Guid("2f3509b7-160d-410a-b9b6-daadd96c986d"), Id = new Guid("2f3509b7-160d-410a-b9b6-daadd96c986d"),
Description = "Managing all tenant related rights", Description = "Managing all tenant related rights",
@ -3834,6 +3849,13 @@ namespace Marco.Pms.DataAccess.Migrations
Name = "Tenant" Name = "Tenant"
}, },
new new
{
Id = new Guid("74e7af50-d55f-4b59-a724-9847ceb7bc17"),
Description = "Inventory Module",
Key = "504ec132-e6a9-422f-8f85-050602cfce05",
Name = "Inventory"
},
new
{ {
Id = new Guid("0a79687a-86d7-430d-a2d7-8b8603cc76a1"), Id = new Guid("0a79687a-86d7-430d-a2d7-8b8603cc76a1"),
Description = "Finance Module", Description = "Finance Module",
@ -4964,6 +4986,408 @@ namespace Marco.Pms.DataAccess.Migrations
b.ToTable("WorkItems"); b.ToTable("WorkItems");
}); });
modelBuilder.Entity("Marco.Pms.Model.PurchaseInvoice.DeliveryChallanDetails", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<Guid>("AttachmentId")
.HasColumnType("char(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("CreatedById")
.HasColumnType("char(36)");
b.Property<DateTime>("DeliveryChallanDate")
.HasColumnType("datetime(6)");
b.Property<string>("DeliveryChallanNumber")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid>("PurchaseInvoiceId")
.HasColumnType("char(36)");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
b.HasKey("Id");
b.HasIndex("AttachmentId");
b.HasIndex("CreatedById");
b.HasIndex("PurchaseInvoiceId");
b.HasIndex("TenantId");
b.ToTable("DeliveryChallanDetails");
});
modelBuilder.Entity("Marco.Pms.Model.PurchaseInvoice.InvoiceAttachmentType", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("InvoiceAttachmentTypes");
b.HasData(
new
{
Id = new Guid("ca294108-a586-4207-88c8-163b24305ddc"),
Description = "A delivery challan is a formal document accompanying a shipment of goods that lists the items included and serves as proof of delivery upon receipt.",
Name = "Delivery Challan"
},
new
{
Id = new Guid("150ddd9b-4b8d-44ac-bae0-2e553c0f069a"),
Description = "An E-Way Bill (Electronic Way Bill) is a mandatory digital document generated on the GST portal to evidence and track the movement of goods valued over ₹50,000.",
Name = "E Way Bill"
},
new
{
Id = new Guid("3ca08288-0a74-4850-9948-0783aa975b84"),
Description = "A Tax Invoice is a mandatory legal document issued by a GST-registered supplier for taxable goods or services, enabling the buyer to claim Input Tax Credit (ITC).",
Name = "Tax Invoice"
},
new
{
Id = new Guid("1fa20cff-b0ee-468e-9ea6-72d5aa144a3f"),
Description = "An E-Invoice (Electronic Invoice) is a system where B2B invoices are electronically authenticated by the GST Network (GSTN) to generate a unique Invoice Reference Number (IRN) and QR code.",
Name = "E-Invoice"
},
new
{
Id = new Guid("31cd7533-3ffc-4e84-a0b4-db3b94d016b2"),
Description = "Proforma Invoice",
Name = "Proforma Invoice"
},
new
{
Id = new Guid("060c79a4-81c7-40a4-8cc3-56362ac9fad6"),
Description = "Sales Order",
Name = "Sales Order"
},
new
{
Id = new Guid("12773c2c-64e7-478c-af17-8471f943a5ed"),
Description = "Other",
Name = "Other"
});
});
modelBuilder.Entity("Marco.Pms.Model.PurchaseInvoice.PurchaseInvoiceAttachment", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<Guid>("DocumentId")
.HasColumnType("char(36)");
b.Property<Guid>("InvoiceAttachmentTypeId")
.HasColumnType("char(36)");
b.Property<Guid>("PurchaseInvoiceId")
.HasColumnType("char(36)");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
b.Property<DateTime>("UploadedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("UploadedById")
.HasColumnType("char(36)");
b.HasKey("Id");
b.HasIndex("DocumentId");
b.HasIndex("InvoiceAttachmentTypeId");
b.HasIndex("PurchaseInvoiceId");
b.HasIndex("TenantId");
b.HasIndex("UploadedById");
b.ToTable("PurchaseInvoiceAttachments");
});
modelBuilder.Entity("Marco.Pms.Model.PurchaseInvoice.PurchaseInvoiceDetails", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<DateTime?>("AcknowledgmentDate")
.HasColumnType("datetime(6)");
b.Property<string>("AcknowledgmentNumber")
.HasColumnType("longtext");
b.Property<double>("BaseAmount")
.HasColumnType("double");
b.Property<string>("BillingAddress")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("CreatedById")
.HasColumnType("char(36)");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime?>("EWayBillDate")
.HasColumnType("datetime(6)");
b.Property<string>("EWayBillNumber")
.HasColumnType("longtext");
b.Property<DateTime?>("InvoiceDate")
.HasColumnType("datetime(6)");
b.Property<string>("InvoiceNumber")
.HasColumnType("longtext");
b.Property<string>("InvoiceReferenceNumber")
.HasColumnType("longtext");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<Guid>("OrganizationId")
.HasColumnType("char(36)");
b.Property<DateTime>("PaymentDueDate")
.HasColumnType("datetime(6)");
b.Property<double?>("ProformaInvoiceAmount")
.HasColumnType("double");
b.Property<DateTime?>("ProformaInvoiceDate")
.HasColumnType("datetime(6)");
b.Property<string>("ProformaInvoiceNumber")
.HasColumnType("longtext");
b.Property<Guid>("ProjectId")
.HasColumnType("char(36)");
b.Property<DateTime?>("PurchaseOrderDate")
.HasColumnType("datetime(6)");
b.Property<string>("PurchaseOrderNumber")
.HasColumnType("longtext");
b.Property<string>("ShippingAddress")
.IsRequired()
.HasColumnType("longtext");
b.Property<Guid>("StatusId")
.HasColumnType("char(36)");
b.Property<Guid>("SupplierId")
.HasColumnType("char(36)");
b.Property<double>("TaxAmount")
.HasColumnType("double");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("longtext");
b.Property<double>("TotalAmount")
.HasColumnType("double");
b.Property<double?>("TransportCharges")
.HasColumnType("double");
b.Property<int>("UIDPostfix")
.HasColumnType("int");
b.Property<string>("UIDPrefix")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid?>("UpdatedById")
.HasColumnType("char(36)");
b.HasKey("Id");
b.HasIndex("CreatedById");
b.HasIndex("OrganizationId");
b.HasIndex("StatusId");
b.HasIndex("SupplierId");
b.HasIndex("TenantId");
b.HasIndex("UpdatedById");
b.ToTable("PurchaseInvoiceDetails");
});
modelBuilder.Entity("Marco.Pms.Model.PurchaseInvoice.PurchaseInvoicePayment", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<double>("Amount")
.HasColumnType("double");
b.Property<string>("Comment")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<Guid>("CreatedById")
.HasColumnType("char(36)");
b.Property<Guid>("InvoiceId")
.HasColumnType("char(36)");
b.Property<bool>("IsActive")
.HasColumnType("tinyint(1)");
b.Property<Guid>("PaymentAdjustmentHeadId")
.HasColumnType("char(36)");
b.Property<DateTime>("PaymentReceivedDate")
.HasColumnType("datetime(6)");
b.Property<Guid>("TenantId")
.HasColumnType("char(36)");
b.Property<string>("TransactionId")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("CreatedById");
b.HasIndex("InvoiceId");
b.HasIndex("PaymentAdjustmentHeadId");
b.HasIndex("TenantId");
b.ToTable("PurchaseInvoicePayments");
});
modelBuilder.Entity("Marco.Pms.Model.PurchaseInvoice.PurchaseInvoiceStatus", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("char(36)");
b.Property<string>("Color")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("DisplayName")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.ToTable("PurchaseInvoiceStatus");
b.HasData(
new
{
Id = new Guid("8a5ef25e-3c9e-45de-add9-6b1c1df54381"),
Color = "#8592a3",
Description = "Draft Status in a Purchase Invoice indicates a preliminary, unfinalized document that is saved for review but has not yet been posted to the general ledger or affected your accounts/inventory.",
DisplayName = "Draft",
Name = "Draft"
},
new
{
Id = new Guid("16b10201-1651-465c-b2fd-236bdef86f95"),
Color = "#696cff",
Description = "Review Pending status in a Purchase Invoice indicates that the invoice has been submitted for validation but requires approval from an authorized person (like a manager or auditor) before it can be posted to the ledger or paid.",
DisplayName = "Submit for Review",
Name = "Review Pending"
},
new
{
Id = new Guid("a05f5f4a-bd9d-4028-af42-48ee0caa3e40"),
Color = "#ff3e1d",
Description = "Rejected by Reviewer status indicates that the invoice failed the approval process due to errors, discrepancies, or policy violations and has been returned to the initiator or vendor for correction.",
DisplayName = "Reject",
Name = "Rejected by Reviewer"
},
new
{
Id = new Guid("60027a54-3c23-4619-9f4e-6c20549b50a6"),
Color = "#03c3ec",
Description = "Approval Pending status in a Purchase Invoice indicates that the document has passed initial verification (matching and coding) and is now awaiting final financial authorization from a designated budget holder or signatory.",
DisplayName = "Mark as Reviewed",
Name = "Approval Pending"
},
new
{
Id = new Guid("58de9cef-811f-46a4-814d-0069b64d98a9"),
Color = "#ff3e1d",
Description = "Rejected by Approver status in a Purchase Invoice indicates that the document successfully passed initial verification but was ultimately denied by the final authorizing signatory (such as a Manager or CFO) due to budget or validity concerns.",
DisplayName = "Reject",
Name = "Rejected by Approver"
},
new
{
Id = new Guid("5b393371-dbcf-4a28-88a8-f406fa34e0d0"),
Color = "#71dd37",
Description = "Approved status indicates that the invoice has successfully cleared all necessary verification and authorization levels and is formally accepted by the company as a valid debt.",
DisplayName = "Mark as Approved",
Name = "Approved"
});
});
modelBuilder.Entity("Marco.Pms.Model.Roles.ApplicationRole", b => modelBuilder.Entity("Marco.Pms.Model.Roles.ApplicationRole", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -7245,10 +7669,6 @@ namespace Marco.Pms.DataAccess.Migrations
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
.IsRequired(); .IsRequired();
b.HasOne("Marco.Pms.Model.Projects.Project", "Project")
.WithMany()
.HasForeignKey("ProjectId");
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant") b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant")
.WithMany() .WithMany()
.HasForeignKey("TenantId") .HasForeignKey("TenantId")
@ -7259,8 +7679,6 @@ namespace Marco.Pms.DataAccess.Migrations
b.Navigation("Employee"); b.Navigation("Employee");
b.Navigation("Project");
b.Navigation("Tenant"); b.Navigation("Tenant");
}); });
@ -8190,6 +8608,168 @@ namespace Marco.Pms.DataAccess.Migrations
b.Navigation("WorkCategoryMaster"); b.Navigation("WorkCategoryMaster");
}); });
modelBuilder.Entity("Marco.Pms.Model.PurchaseInvoice.DeliveryChallanDetails", b =>
{
b.HasOne("Marco.Pms.Model.PurchaseInvoice.PurchaseInvoiceAttachment", "Attachment")
.WithMany()
.HasForeignKey("AttachmentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.PurchaseInvoice.PurchaseInvoiceDetails", "PurchaseInvoice")
.WithMany()
.HasForeignKey("PurchaseInvoiceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant")
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Attachment");
b.Navigation("CreatedBy");
b.Navigation("PurchaseInvoice");
b.Navigation("Tenant");
});
modelBuilder.Entity("Marco.Pms.Model.PurchaseInvoice.PurchaseInvoiceAttachment", b =>
{
b.HasOne("Marco.Pms.Model.DocumentManager.Document", "Document")
.WithMany()
.HasForeignKey("DocumentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.PurchaseInvoice.InvoiceAttachmentType", "InvoiceAttachmentType")
.WithMany()
.HasForeignKey("InvoiceAttachmentTypeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.PurchaseInvoice.PurchaseInvoiceDetails", "PurchaseInvoice")
.WithMany()
.HasForeignKey("PurchaseInvoiceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant")
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.Employees.Employee", "UploadedBy")
.WithMany()
.HasForeignKey("UploadedById")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Document");
b.Navigation("InvoiceAttachmentType");
b.Navigation("PurchaseInvoice");
b.Navigation("Tenant");
b.Navigation("UploadedBy");
});
modelBuilder.Entity("Marco.Pms.Model.PurchaseInvoice.PurchaseInvoiceDetails", b =>
{
b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.OrganizationModel.Organization", "Organization")
.WithMany()
.HasForeignKey("OrganizationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.PurchaseInvoice.PurchaseInvoiceStatus", "Status")
.WithMany()
.HasForeignKey("StatusId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.OrganizationModel.Organization", "Supplier")
.WithMany()
.HasForeignKey("SupplierId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant")
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.Employees.Employee", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
b.Navigation("CreatedBy");
b.Navigation("Organization");
b.Navigation("Status");
b.Navigation("Supplier");
b.Navigation("Tenant");
b.Navigation("UpdatedBy");
});
modelBuilder.Entity("Marco.Pms.Model.PurchaseInvoice.PurchaseInvoicePayment", b =>
{
b.HasOne("Marco.Pms.Model.Employees.Employee", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.PurchaseInvoice.PurchaseInvoiceDetails", "Invoice")
.WithMany()
.HasForeignKey("InvoiceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.Collection.PaymentAdjustmentHead", "PaymentAdjustmentHead")
.WithMany()
.HasForeignKey("PaymentAdjustmentHeadId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", "Tenant")
.WithMany()
.HasForeignKey("TenantId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("CreatedBy");
b.Navigation("Invoice");
b.Navigation("PaymentAdjustmentHead");
b.Navigation("Tenant");
});
modelBuilder.Entity("Marco.Pms.Model.Roles.ApplicationRole", b => modelBuilder.Entity("Marco.Pms.Model.Roles.ApplicationRole", b =>
{ {
b.HasOne("Marco.Pms.Model.TenantModels.Tenant", null) b.HasOne("Marco.Pms.Model.TenantModels.Tenant", null)

View File

@ -0,0 +1,34 @@
using Marco.Pms.Model.Mail;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using MongoDB.Driver;
namespace Marco.Pms.Helpers.Utility
{
public class MailLogHelper
{
private readonly IMongoCollection<MailLog> _collection;
private readonly ILogger<MailLogHelper> _logger;
public MailLogHelper(IConfiguration configuration, ILogger<MailLogHelper> logger)
{
_logger = logger;
var connectionString = configuration["MongoDB:MailConnectionString"];
var mongoUrl = new MongoUrl(connectionString);
var client = new MongoClient(mongoUrl); // Your MongoDB connection string
var mongoDB = client.GetDatabase(mongoUrl.DatabaseName); // Your MongoDB Database name
_collection = mongoDB.GetCollection<MailLog>("MailLogs");
}
public async Task AddWebMenuItemAsync(List<MailLog> mailLogs)
{
try
{
await _collection.InsertManyAsync(mailLogs);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while adding Mail Logs.");
}
}
}
}

View File

@ -1,14 +1,14 @@
using Marco.Pms.Model.AppMenu; using Marco.Pms.Model.AppMenu;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
namespace Marco.Pms.CacheHelper namespace Marco.Pms.CacheHelper
{ {
public class SidebarMenuHelper public class SidebarMenuHelper
{ {
private readonly IMongoCollection<MenuSection> _collection; private readonly IMongoCollection<WebSideMenuItem> _webCollection;
private readonly IMongoCollection<MobileMenu> _mobileCollection;
private readonly ILogger<SidebarMenuHelper> _logger; private readonly ILogger<SidebarMenuHelper> _logger;
public SidebarMenuHelper(IConfiguration configuration, ILogger<SidebarMenuHelper> logger) public SidebarMenuHelper(IConfiguration configuration, ILogger<SidebarMenuHelper> logger)
@ -18,191 +18,18 @@ namespace Marco.Pms.CacheHelper
var mongoUrl = new MongoUrl(connectionString); var mongoUrl = new MongoUrl(connectionString);
var client = new MongoClient(mongoUrl); var client = new MongoClient(mongoUrl);
var database = client.GetDatabase(mongoUrl.DatabaseName); var database = client.GetDatabase(mongoUrl.DatabaseName);
_collection = database.GetCollection<MenuSection>("Menus"); _webCollection = database.GetCollection<WebSideMenuItem>("WebSideMenus");
_mobileCollection = database.GetCollection<MobileMenu>("MobileSideMenus");
} }
public async Task<MenuSection?> CreateMenuSectionAsync(MenuSection section) public async Task<List<WebSideMenuItem>> GetAllWebMenuSectionsAsync(Guid tenantId)
{ {
try try
{ {
await _collection.InsertOneAsync(section); var filter = Builders<WebSideMenuItem>.Filter.Eq(e => e.TenantId, tenantId);
return section;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while adding MenuSection.");
return null;
}
}
public async Task<MenuSection?> UpdateMenuSectionAsync(Guid sectionId, MenuSection updatedSection) var result = await _webCollection
{
try
{
var filter = Builders<MenuSection>.Filter.Eq(s => s.Id, sectionId);
var update = Builders<MenuSection>.Update
.Set(s => s.Header, updatedSection.Header)
.Set(s => s.Title, updatedSection.Title)
.Set(s => s.Items, updatedSection.Items);
var result = await _collection.UpdateOneAsync(filter, update);
if (result.ModifiedCount > 0)
{
return await _collection.Find(s => s.Id == sectionId).FirstOrDefaultAsync();
}
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating MenuSection.");
return null;
}
}
public async Task<MenuSection?> AddMenuItemAsync(Guid sectionId, MenuItem newItem)
{
try
{
newItem.Id = Guid.NewGuid();
var filter = Builders<MenuSection>.Filter.Eq(s => s.Id, sectionId);
var update = Builders<MenuSection>.Update.Push(s => s.Items, newItem);
var result = await _collection.UpdateOneAsync(filter, update);
if (result.ModifiedCount > 0)
{
return await _collection.Find(s => s.Id == sectionId).FirstOrDefaultAsync();
}
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error adding menu item.");
return null;
}
}
public async Task<MenuItem?> UpdateMenuItemAsync(Guid sectionId, Guid itemId, MenuItem updatedItem)
{
try
{
var filter = Builders<MenuSection>.Filter.And(
Builders<MenuSection>.Filter.Eq(s => s.Id, sectionId),
Builders<MenuSection>.Filter.ElemMatch(s => s.Items, i => i.Id == itemId)
);
var update = Builders<MenuSection>.Update
.Set("Items.$.Text", updatedItem.Text)
.Set("Items.$.Icon", updatedItem.Icon)
.Set("Items.$.Available", updatedItem.Available)
.Set("Items.$.Link", updatedItem.Link)
.Set("Items.$.PermissionIds", updatedItem.PermissionIds);
var result = await _collection.UpdateOneAsync(filter, update);
if (result.ModifiedCount > 0)
{
// Re-fetch section and return the updated item
var section = await _collection.Find(s => s.Id == sectionId).FirstOrDefaultAsync();
return section?.Items.FirstOrDefault(i => i.Id == itemId);
}
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating MenuItem.");
return null;
}
}
public async Task<MenuSection?> AddSubMenuItemAsync(Guid sectionId, Guid itemId, SubMenuItem newSubItem)
{
try
{
newSubItem.Id = Guid.NewGuid();
// Match the MenuSection and the specific MenuItem inside it
var filter = Builders<MenuSection>.Filter.And(
Builders<MenuSection>.Filter.Eq(s => s.Id, sectionId),
Builders<MenuSection>.Filter.ElemMatch(s => s.Items, i => i.Id == itemId)
);
// Use positional operator `$` to target matched MenuItem and push into its Submenu
var update = Builders<MenuSection>.Update.Push("Items.$.Submenu", newSubItem);
var result = await _collection.UpdateOneAsync(filter, update);
if (result.ModifiedCount > 0)
{
return await _collection.Find(s => s.Id == sectionId).FirstOrDefaultAsync();
}
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error adding submenu item.");
return null;
}
}
public async Task<SubMenuItem?> UpdateSubmenuItemAsync(Guid sectionId, Guid itemId, Guid subItemId, SubMenuItem updatedSub)
{
try
{
var filter = Builders<MenuSection>.Filter.Eq(s => s.Id, sectionId);
var arrayFilters = new List<ArrayFilterDefinition>
{
new BsonDocumentArrayFilterDefinition<BsonDocument>(
new BsonDocument("item._id", itemId.ToString())),
new BsonDocumentArrayFilterDefinition<BsonDocument>(
new BsonDocument("sub._id", subItemId.ToString()))
};
var update = Builders<MenuSection>.Update
.Set("Items.$[item].Submenu.$[sub].Text", updatedSub.Text)
.Set("Items.$[item].Submenu.$[sub].Available", updatedSub.Available)
.Set("Items.$[item].Submenu.$[sub].Link", updatedSub.Link)
.Set("Items.$[item].Submenu.$[sub].PermissionKeys", updatedSub.PermissionIds);
var options = new UpdateOptions { ArrayFilters = arrayFilters };
var result = await _collection.UpdateOneAsync(filter, update, options);
if (result.ModifiedCount == 0)
return null;
var updatedSection = await _collection.Find(x => x.Id == sectionId).FirstOrDefaultAsync();
var subItem = updatedSection?.Items
.FirstOrDefault(i => i.Id == itemId)?
.Submenu
.FirstOrDefault(s => s.Id == subItemId);
return subItem;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating SubMenuItem.");
return null;
}
}
public async Task<List<MenuSection>> GetAllMenuSectionsAsync(Guid tenantId)
{
var filter = Builders<MenuSection>.Filter.Eq(e => e.TenantId, tenantId);
var result = await _collection
.Find(filter) .Find(filter)
.ToListAsync(); .ToListAsync();
if (result.Any()) if (result.Any())
@ -211,13 +38,74 @@ namespace Marco.Pms.CacheHelper
} }
tenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26"); tenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26");
filter = Builders<MenuSection>.Filter.Eq(e => e.TenantId, tenantId); filter = Builders<WebSideMenuItem>.Filter.Eq(e => e.TenantId, tenantId);
result = await _collection result = await _webCollection
.Find(filter) .Find(filter)
.ToListAsync(); .ToListAsync();
return result; return result;
} }
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while fetching Web Menu Sections.");
return new List<WebSideMenuItem>();
}
}
public async Task<List<WebSideMenuItem>> AddWebMenuItemAsync(List<WebSideMenuItem> newItems)
{
try
{
await _webCollection.InsertManyAsync(newItems);
return newItems;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while adding Web Menu Section.");
return new List<WebSideMenuItem>();
}
}
public async Task<List<MobileMenu>> GetAllMobileMenuSectionsAsync(Guid tenantId)
{
try
{
var filter = Builders<MobileMenu>.Filter.Eq(e => e.TenantId, tenantId);
var result = await _mobileCollection
.Find(filter)
.ToListAsync();
if (result.Any())
{
return result;
}
tenantId = Guid.Parse("b3466e83-7e11-464c-b93a-daf047838b26");
filter = Builders<MobileMenu>.Filter.Eq(e => e.TenantId, tenantId);
result = await _mobileCollection
.Find(filter)
.ToListAsync();
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while fetching Web Menu Sections.");
return new List<MobileMenu>();
}
}
public async Task<List<MobileMenu>> AddMobileMenuItemAsync(List<MobileMenu> newItems)
{
try
{
await _mobileCollection.InsertManyAsync(newItems);
return newItems;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error occurred while adding Mobile Menu Section.");
return new List<MobileMenu>();
}
}
} }

View File

@ -0,0 +1,21 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace Marco.Pms.Model.AppMenu
{
public class MobileMenu
{
[BsonId]
[BsonRepresentation(BsonType.String)]
public Guid Id { get; set; }
public string? Name { get; set; }
public bool Available { get; set; }
public string? MobileLink { get; set; }
[BsonRepresentation(BsonType.String)]
public List<Guid> PermissionIds { get; set; } = new List<Guid>();
[BsonRepresentation(BsonType.String)]
public Guid TenantId { get; set; }
}
}

View File

@ -0,0 +1,19 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace Marco.Pms.Model.AppMenu
{
public class WebMenuSection
{
[BsonId]
[BsonRepresentation(BsonType.String)]
public Guid Id { get; set; } = Guid.NewGuid();
public string? Header { get; set; }
public string? Name { get; set; }
public List<WebSideMenuItem> Items { get; set; } = new List<WebSideMenuItem>();
[BsonRepresentation(BsonType.String)]
public Guid TenantId { get; set; }
}
}

View File

@ -0,0 +1,25 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace Marco.Pms.Model.AppMenu
{
public class WebSideMenuItem
{
[BsonId]
[BsonRepresentation(BsonType.String)]
public Guid Id { get; set; } = Guid.NewGuid();
[BsonRepresentation(BsonType.String)]
public Guid? ParentMenuId { get; set; }
public string? Name { get; set; }
public string? Icon { get; set; }
public bool Available { get; set; } = true;
public string Link { get; set; } = string.Empty;
[BsonRepresentation(BsonType.String)]
public List<Guid> PermissionIds { get; set; } = new List<Guid>();
[BsonRepresentation(BsonType.String)]
public Guid TenantId { get; set; }
}
}

View File

@ -1,17 +0,0 @@
namespace Marco.Pms.Model.Dtos.AppMenu
{
public class CreateMenuItemDto
{
public required string Text { get; set; }
public required string Icon { get; set; }
public bool Available { get; set; } = true;
public required string Link { get; set; }
public string? MobileLink { get; set; }
// Changed from string → List<string>
public List<string> PermissionIds { get; set; } = new List<string>();
public List<CreateSubMenuItemDto> Submenu { get; set; } = new List<CreateSubMenuItemDto>();
}
}

View File

@ -1,9 +0,0 @@
namespace Marco.Pms.Model.Dtos.AppMenu
{
public class CreateMenuSectionDto
{
public required string Header { get; set; }
public required string Title { get; set; }
public List<CreateMenuItemDto> Items { get; set; } = new List<CreateMenuItemDto>();
}
}

View File

@ -0,0 +1,13 @@
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace Marco.Pms.Model.Dtos.AppMenu
{
public class CreateMobileSideMenuItemDto
{
public string? Name { get; set; }
public bool Available { get; set; }
public string? MobileLink { get; set; }
public List<Guid> PermissionIds { get; set; } = new List<Guid>();
}
}

View File

@ -1,13 +0,0 @@
namespace Marco.Pms.Model.Dtos.AppMenu
{
public class CreateSubMenuItemDto
{
public required string Text { get; set; }
public bool Available { get; set; } = true;
public required string Link { get; set; } = string.Empty;
public string? MobileLink { get; set; }
// Changed from string → List<string>
public List<string> PermissionIds { get; set; } = new List<string>();
}
}

View File

@ -1,9 +1,9 @@
namespace Marco.Pms.Model.Dtos.AppMenu namespace Marco.Pms.Model.Dtos.AppMenu
{ {
public class UpdateMenuSectionDto public class CreateWebMenuSectionDto
{ {
public required Guid Id { get; set; }
public required string Header { get; set; } public required string Header { get; set; }
public required string Title { get; set; } public required string Title { get; set; }
public List<CreateWebSideMenuItemDto> Items { get; set; } = new List<CreateWebSideMenuItemDto>();
} }
} }

View File

@ -0,0 +1,15 @@
namespace Marco.Pms.Model.Dtos.AppMenu
{
public class CreateWebSideMenuItemDto
{
public Guid? Id { get; set; }
public Guid? ParentMenuId { get; set; }
public string? Name { get; set; }
public string? Icon { get; set; }
public bool Available { get; set; } = true;
public string Link { get; set; } = string.Empty;
// Changed from string → List<string>
public List<Guid> PermissionIds { get; set; } = new List<Guid>();
}
}

View File

@ -1,17 +0,0 @@
namespace Marco.Pms.Model.Dtos.AppMenu
{
public class UpdateMenuItemDto
{
public required Guid Id { get; set; }
public required string Text { get; set; }
public required string Icon { get; set; }
public bool Available { get; set; } = true;
public required string Link { get; set; }
public string? MobileLink { get; set; }
// Changed from string → List<string>
public List<string> PermissionIds { get; set; } = new List<string>();
}
}

View File

@ -1,16 +0,0 @@
namespace Marco.Pms.Model.Dtos.AppMenu
{
public class UpdateSubMenuItemDto
{
public Guid Id { get; set; }
public string? Text { get; set; }
public bool Available { get; set; } = true;
public string Link { get; set; } = string.Empty;
public string? MobileLink { get; set; }
// Changed from string → List<string>
public List<string> PermissionIds { get; set; } = new List<string>();
}
}

View File

@ -9,8 +9,8 @@ namespace Marco.Pms.Model.Dtos.Project
public Guid WorkAreaID { get; set; } public Guid WorkAreaID { get; set; }
public Guid WorkCategoryId { get; set; } public Guid WorkCategoryId { get; set; }
public Guid ActivityID { get; set; } public Guid ActivityID { get; set; }
public int PlannedWork { get; set; } public double PlannedWork { get; set; }
public int CompletedWork { get; set; } public double CompletedWork { get; set; }
public Guid? ParentTaskId { get; set; } public Guid? ParentTaskId { get; set; }
public string? Comment { get; set; } public string? Comment { get; set; }
} }

View File

@ -0,0 +1,11 @@
namespace Marco.Pms.Model.Dtos.PurchaseInvoice
{
public class DeliveryChallanDto
{
public required string DeliveryChallanNumber { get; set; }
public required DateTime DeliveryChallanDate { get; set; }
public required string Description { get; set; }
public required Guid PurchaseInvoiceId { get; set; }
public required InvoiceAttachmentDto Attachment { get; set; }
}
}

View File

@ -0,0 +1,14 @@
namespace Marco.Pms.Model.Dtos.PurchaseInvoice
{
public class InvoiceAttachmentDto
{
public Guid? DocumentId { get; set; }
public required Guid InvoiceAttachmentTypeId { get; set; }
public required string FileName { get; set; } // Name of the file (e.g., "image1.png")
public string? Base64Data { get; set; } // Base64-encoded string of the file
public string? ContentType { get; set; } // MIME type (e.g., "image/png", "application/pdf")
public long FileSize { get; set; } // File size in bytes
public string? Description { get; set; } // Optional: Description or purpose of the file
public required bool IsActive { get; set; }
}
}

View File

@ -0,0 +1,32 @@
namespace Marco.Pms.Model.Dtos.PurchaseInvoice
{
public class PurchaseInvoiceDto
{
public required string Title { get; set; }
public required string Description { get; set; }
public required Guid ProjectId { get; set; }
public required Guid OrganizationId { get; set; }
public required string BillingAddress { get; set; }
public required string ShippingAddress { get; set; }
public string? PurchaseOrderNumber { get; set; }
public DateTime? PurchaseOrderDate { get; set; }
public Guid? StatusId { get; set; }
public required Guid SupplierId { get; set; }
public string? ProformaInvoiceNumber { get; set; }
public DateTime? ProformaInvoiceDate { get; set; }
public double? ProformaInvoiceAmount { get; set; }
public string? InvoiceNumber { get; set; }
public DateTime? InvoiceDate { get; set; }
public string? EWayBillNumber { get; set; }
public DateTime? EWayBillDate { get; set; }
public string? InvoiceReferenceNumber { get; set; }
public string? AcknowledgmentNumber { get; set; }
public DateTime? AcknowledgmentDate { get; set; }
public required double BaseAmount { get; set; }
public required double TaxAmount { get; set; }
public double? TransportCharges { get; set; }
public required double TotalAmount { get; set; }
public DateTime? PaymentDueDate { get; set; } // Defaults to 40 days from the invoice date
public List<InvoiceAttachmentDto> Attachments { get; set; } = new List<InvoiceAttachmentDto>();
}
}

View File

@ -60,6 +60,11 @@
public static readonly Guid AddOrganization = Guid.Parse("068cb3c1-49c5-4746-9f29-1fce16e820ac"); public static readonly Guid AddOrganization = Guid.Parse("068cb3c1-49c5-4746-9f29-1fce16e820ac");
public static readonly Guid EditOrganization = Guid.Parse("c1ae1363-ab8a-4bd9-a9d1-8c2c6083873a"); public static readonly Guid EditOrganization = Guid.Parse("c1ae1363-ab8a-4bd9-a9d1-8c2c6083873a");
public static readonly Guid ViewOrganization = Guid.Parse("7a6cf830-0008-4e03-b31d-0d050cb634f4"); public static readonly Guid ViewOrganization = Guid.Parse("7a6cf830-0008-4e03-b31d-0d050cb634f4");
}
}
public static readonly Guid ViewSelfPurchaseInvoice = Guid.Parse("91e09825-512a-465e-82ad-fa355b305585");
public static readonly Guid ViewAllPurchaseInvoice = Guid.Parse("d6ae78d3-a941-4cc4-8d0a-d40479be4211");
public static readonly Guid ManagePurchaseInvoice = Guid.Parse("68ff925d-8ebf-4034-a137-8d3317c56ca1");
public static readonly Guid DeletePurchaseInvoice = Guid.Parse("a4b77638-bf31-42bb-afd4-d5bbd15ccadc");
public static readonly Guid AddDeliveryChallan = Guid.Parse("b24eba39-4a92-4f7a-b33b-b5308fbc48b9");
}
}

View File

@ -1,5 +1,4 @@
using Marco.Pms.Model.Employees; using Marco.Pms.Model.Employees;
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;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
@ -13,10 +12,6 @@ namespace Marco.Pms.Model.Expenses
public int FinanceUIdPostfix { get; set; } public int FinanceUIdPostfix { get; set; }
public string Title { get; set; } = default!; public string Title { get; set; } = default!;
public Guid? ProjectId { get; set; } public Guid? ProjectId { get; set; }
[ValidateNever]
[ForeignKey("ProjectId")]
public Project? Project { get; set; }
public Guid EmployeeId { get; set; } public Guid EmployeeId { get; set; }
[ValidateNever] [ValidateNever]

View File

@ -3,6 +3,8 @@
public class AdvanceFilter public class AdvanceFilter
{ {
// The dynamic filters from your JSON // The dynamic filters from your JSON
public DateDynamicFilter? DateFilter { get; set; }
public List<ListDynamicFilter>? Filters { get; set; }
public List<SortItem>? SortFilters { get; set; } public List<SortItem>? SortFilters { get; set; }
public List<SearchItem>? SearchFilters { get; set; } public List<SearchItem>? SearchFilters { get; set; }
public List<AdvanceItem>? AdvanceFilters { get; set; } public List<AdvanceItem>? AdvanceFilters { get; set; }

View File

@ -0,0 +1,9 @@
namespace Marco.Pms.Model.Filters
{
public class DateDynamicFilter
{
public string Column { get; set; } = string.Empty;
public DateTime StartValue { get; set; }
public DateTime EndValue { get; set; }
}
}

View File

@ -0,0 +1,8 @@
namespace Marco.Pms.Model.Filters
{
public class ListDynamicFilter
{
public string Column { get; set; } = string.Empty;
public List<Guid> Values { get; set; } = new List<Guid>();
}
}

View File

@ -0,0 +1,75 @@
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
namespace Marco.Pms.Model.PurchaseInvoice
{
/// <summary>
/// Represents a detail of a delivery challan in the Purchase Invoice system.
/// This class inherits from the TenantRelation class and adds specific properties related to a delivery challan.
/// </summary>
public class DeliveryChallanDetails : TenantRelation
{
/// <summary>
/// Gets or sets the unique identifier for the delivery challan.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Gets or sets the delivery challan number.
/// </summary>
public string DeliveryChallanNumber { get; set; } = default!;
/// <summary>
/// Gets or sets the date of the delivery challan.
/// </summary>
public DateTime DeliveryChallanDate { get; set; }
/// <summary>
/// Gets or sets the description of the delivery challan.
/// </summary>
public string Description { get; set; } = default!;
/// <summary>
/// Gets or sets the unique identifier of the associated PurchaseInvoice.
/// </summary>
public Guid PurchaseInvoiceId { get; set; }
/// <summary>
/// Gets or sets the associated PurchaseInvoice.
/// </summary>
[ValidateNever]
[ForeignKey("PurchaseInvoiceId")]
public PurchaseInvoiceDetails? PurchaseInvoice { get; set; }
/// <summary>
/// Gets or sets the unique identifier of the associated Attachment.
/// </summary>
public Guid AttachmentId { get; set; }
/// <summary>
/// Gets or sets the associated Attachment.
/// </summary>
[ValidateNever]
[ForeignKey("AttachmentId")]
public PurchaseInvoiceAttachment? Attachment { get; set; }
/// <summary>
/// Gets or sets the date and time the record was created.
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// Gets or sets the unique identifier of the user who created the record.
/// </summary>
public Guid CreatedById { get; set; }
/// <summary>
/// Gets or sets the user who created the record.
/// </summary>
[ValidateNever]
[ForeignKey("CreatedById")]
public Employee? CreatedBy { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace Marco.Pms.Model.PurchaseInvoice
{
public class InvoiceAttachmentType
{
public Guid Id { get; set; }
public string Name { get; set; } = default!;
public string Description { get; set; } = default!;
}
}

View File

@ -0,0 +1,73 @@
using Marco.Pms.Model.DocumentManager;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
namespace Marco.Pms.Model.PurchaseInvoice
{
/// <summary>
/// Represents an attachment for a purchase invoice.
/// </summary>
public class PurchaseInvoiceAttachment : TenantRelation
{
/// <summary>
/// Gets or sets the unique identifier for the attachment.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Gets or sets the unique identifier for the purchase invoice.
/// </summary>
public Guid PurchaseInvoiceId { get; set; }
/// <summary>
/// Gets or sets the purchase invoice associated with the attachment.
/// </summary>
[ValidateNever]
[ForeignKey("PurchaseInvoiceId")]
public PurchaseInvoiceDetails? PurchaseInvoice { get; set; }
/// <summary>
/// Gets or sets the unique identifier for the type of the invoice attachment.
/// </summary>
public Guid InvoiceAttachmentTypeId { get; set; }
/// <summary>
/// Gets or sets the type of the invoice attachment.
/// </summary>
[ValidateNever]
[ForeignKey("InvoiceAttachmentTypeId")]
public InvoiceAttachmentType? InvoiceAttachmentType { get; set; }
/// <summary>
/// Gets or sets the unique identifier for the document.
/// </summary>
public Guid DocumentId { get; set; }
/// <summary>
/// Gets or sets the document associated with the attachment.
/// </summary>
[ValidateNever]
[ForeignKey("DocumentId")]
public Document? Document { get; set; }
/// <summary>
/// Gets or sets the date and time when the attachment was uploaded.
/// </summary>
public DateTime UploadedAt { get; set; }
/// <summary>
/// Gets or sets the unique identifier of the employee who uploaded the attachment.
/// </summary>
public Guid UploadedById { get; set; }
/// <summary>
/// Gets or sets the employee who uploaded the attachment.
/// </summary>
[ValidateNever]
[ForeignKey("UploadedById")]
public Employee? UploadedBy { get; set; }
}
}

View File

@ -0,0 +1,217 @@
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.OrganizationModel;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
namespace Marco.Pms.Model.PurchaseInvoice
{
/// <summary>
/// The PurchaseInvoiceDetails class represents a detail in a purchase invoice.
/// It contains information about the detail such as its unique identifier, title, description, billing and shipping addresses,
/// purchase order number and date, supplier information, proforma invoice details, invoice details, e-way bill details,
/// invoice reference number, acknowledgment number and date, base amount, tax amount, transport charges, total amount, and payment due date.
/// </summary>
public class PurchaseInvoiceDetails : TenantRelation
{
/// <summary>
/// Gets or sets the unique identifier of the detail.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Gets or sets the prefix of the unique identifier for the detail.
/// </summary>
public string UIDPrefix { get; set; } = default!; // PUR/MMYY/
/// <summary>
/// Gets or sets the postfix of the unique identifier for the detail.
/// </summary>
public int UIDPostfix { get; set; } // 00001
/// <summary>
/// Gets or sets the title of the detail.
/// </summary>
public string Title { get; set; } = default!;
/// <summary>
/// Gets or sets the description of the detail.
/// </summary>
public string Description { get; set; } = default!;
/// <summary>
/// Gets or sets the unique identifier of the related project.
/// </summary>
public Guid ProjectId { get; set; }
/// <summary>
/// Gets or sets the unique identifier of the organization related to the detail.
/// </summary>
public Guid OrganizationId { get; set; }
/// <summary>
/// Gets or sets the related organization.
/// </summary>
[ValidateNever]
[ForeignKey("OrganizationId")]
public Organization? Organization { get; set; }
/// <summary>
/// Gets or sets the status of the detail.
/// </summary>
public Guid StatusId { get; set; }
/// <summary>
/// Gets or sets the status of the detail.
/// </summary>
[ValidateNever]
[ForeignKey("StatusId")]
public PurchaseInvoiceStatus? Status { get; set; }
/// <summary>
/// Gets or sets the billing address of the detail.
/// </summary>
public string BillingAddress { get; set; } = default!;
/// <summary>
/// Gets or sets the shipping address of the detail.
/// </summary>
public string ShippingAddress { get; set; } = default!;
/// <summary>
/// Gets or sets the purchase order number of the detail.
/// </summary>
public string? PurchaseOrderNumber { get; set; }
/// <summary>
/// Gets or sets the purchase order date of the detail.
/// </summary>
public DateTime? PurchaseOrderDate { get; set; }
/// <summary>
/// Gets or sets the unique identifier of the supplier related to the detail.
/// </summary>
public Guid SupplierId { get; set; }
/// <summary>
/// Gets or sets the related supplier.
/// </summary>
[ValidateNever]
[ForeignKey("SupplierId")]
public Organization? Supplier { get; set; }
/// <summary>
/// Gets or sets the proforma invoice number of the detail.
/// </summary>
public string? ProformaInvoiceNumber { get; set; }
/// <summary>
/// Gets or sets the proforma invoice date of the detail.
/// </summary>
public DateTime? ProformaInvoiceDate { get; set; }
/// <summary>
/// Gets or sets the proforma invoice amount of the detail.
/// </summary>
public double? ProformaInvoiceAmount { get; set; }
/// <summary>
/// Gets or sets the invoice number of the detail.
/// </summary>
public string? InvoiceNumber { get; set; }
/// <summary>
/// Gets or sets the invoice date of the detail.
/// </summary>
public DateTime? InvoiceDate { get; set; }
/// <summary>
/// Gets or sets the e-way bill number of the detail.
/// </summary>
public string? EWayBillNumber { get; set; }
/// <summary>
/// Gets or sets the e-way bill date of the detail.
/// </summary>
public DateTime? EWayBillDate { get; set; }
/// <summary>
/// Gets or sets the invoice reference number of the detail.
/// </summary>
public string? InvoiceReferenceNumber { get; set; }
/// <summary>
/// Gets or sets the acknowledgment number of the detail.
/// </summary>
public string? AcknowledgmentNumber { get; set; }
/// <summary>
/// Gets or sets the acknowledgment date of the detail.
/// </summary>
public DateTime? AcknowledgmentDate { get; set; }
/// <summary>
/// Gets or sets the base amount of the detail.
/// </summary>
public double BaseAmount { get; set; }
/// <summary>
/// Gets or sets the tax amount of the detail.
/// </summary>
public double TaxAmount { get; set; }
/// <summary>
/// Gets or sets the transport charges of the detail.
/// </summary>
public double? TransportCharges { get; set; }
/// <summary>
/// Gets or sets the total amount of the detail.
/// </summary>
public double TotalAmount { get; set; }
/// <summary>
/// The payment due date of the detail.
/// </summary>
public DateTime PaymentDueDate { get; set; } // Defaults to 40 days from the invoice date
/// <summary>
/// Gets or sets a value indicating whether the detail is active.
/// </summary>
public bool IsActive { get; set; }
/// <summary>
/// Gets or sets the unique identifier of the user who created the detail.
/// </summary>
public Guid CreatedById { get; set; }
/// <summary>
/// Gets or sets the user who created the detail.
/// </summary>
[ValidateNever]
[ForeignKey("CreatedById")]
public Employee? CreatedBy { get; set; }
/// <summary>
/// Gets or sets the date and time when the detail was created.
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// Gets or sets the unique identifier of the user who last updated the detail.
/// </summary>
public Guid? UpdatedById { get; set; }
/// <summary>
/// Gets or sets the user who last updated the detail.
/// </summary>
[ValidateNever]
[ForeignKey("UpdatedById")]
public Employee? UpdatedBy { get; set; }
/// <summary>
/// Gets or sets the date and time when the detail was last updated.
/// </summary>
public DateTime? UpdatedAt { get; set; }
}
}

View File

@ -0,0 +1,89 @@
using Marco.Pms.Model.Collection;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Utilities;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.ComponentModel.DataAnnotations.Schema;
namespace Marco.Pms.Model.PurchaseInvoice
{
/// <summary>
/// Represents a payment made against a purchase invoice.
/// It is a subclass of TenantRelation, indicating that it is a relation between a tenant and the Marco PMS system.
/// </summary>
public class PurchaseInvoicePayment : TenantRelation
{
/// <summary>
/// Gets or sets the unique identifier of the payment.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Gets or sets the unique identifier of the associated purchase invoice.
/// </summary>
public Guid InvoiceId { get; set; }
/// <summary>
/// Gets or sets the associated purchase invoice.
/// This is a navigation property that allows access to the associated PurchaseInvoiceDetails object.
/// </summary>
[ValidateNever]
[ForeignKey("InvoiceId")]
public PurchaseInvoiceDetails? Invoice { get; set; }
/// <summary>
/// Gets or sets the date the payment was received.
/// </summary>
public DateTime PaymentReceivedDate { get; set; }
/// <summary>
/// Gets or sets the transaction ID of the payment.
/// </summary>
public string TransactionId { get; set; } = default!;
/// <summary>
/// Gets or sets the amount of the payment.
/// </summary>
public double Amount { get; set; }
/// <summary>
/// Gets or sets a comment about the payment.
/// </summary>
public string Comment { get; set; } = default!;
/// <summary>
/// Gets or sets a value indicating whether the payment is active.
/// </summary>
public bool IsActive { get; set; } = true;
/// <summary>
/// Gets or sets the unique identifier of the payment adjustment head.
/// </summary>
public Guid PaymentAdjustmentHeadId { get; set; }
/// <summary>
/// Gets or sets the associated payment adjustment head.
/// This is a navigation property that allows access to the associated PaymentAdjustmentHead object.
/// </summary>
[ValidateNever]
[ForeignKey("PaymentAdjustmentHeadId")]
public PaymentAdjustmentHead? PaymentAdjustmentHead { get; set; }
/// <summary>
/// Gets or sets the date and time the record was created.
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// Gets or sets the unique identifier of the user who created the record.
/// </summary>
public Guid CreatedById { get; set; }
/// <summary>
/// Gets or sets the user who created the record.
/// This is a navigation property that allows access to the associated Employee object.
/// </summary>
[ValidateNever]
[ForeignKey("CreatedById")]
public Employee? CreatedBy { get; set; }
}
}

View File

@ -0,0 +1,11 @@
namespace Marco.Pms.Model.PurchaseInvoice
{
public class PurchaseInvoiceStatus
{
public Guid Id { get; set; }
public string Name { get; set; } = default!;
public string DisplayName { get; set; } = default!;
public string Color { get; set; } = default!;
public string Description { get; set; } = default!;
}
}

View File

@ -0,0 +1,11 @@
namespace Marco.Pms.Model.ViewModels.AppMenu
{
public class WebMenuSectionVM
{
public Guid Id { get; set; }
public string? Header { get; set; }
public string? Name { get; set; }
public List<WebSideMenuItemVM> Items { get; set; } = new List<WebSideMenuItemVM>();
}
}

View File

@ -1,13 +1,12 @@
namespace Marco.Pms.Model.ViewModels.DocumentManager namespace Marco.Pms.Model.ViewModels.AppMenu
{ {
public class MenuItemVM public class WebSideMenuItemVM
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public string? Name { get; set; } public string? Name { get; set; }
public string? Icon { get; set; } public string? Icon { get; set; }
public bool Available { get; set; } public bool Available { get; set; }
public string? Link { get; set; } public string? Link { get; set; }
public List<SubMenuItemVM> Submenu { get; set; } = new List<SubMenuItemVM>(); public List<WebSideMenuItemVM> Submenu { get; set; } = new List<WebSideMenuItemVM>();
} }
} }

View File

@ -1,6 +1,6 @@
namespace Marco.Pms.Model.ViewModels.DashBoard namespace Marco.Pms.Model.ViewModels.DashBoard
{ {
public class EmployeeAttendanceVM public class DashBoardEmployeeAttendanceVM
{ {
public string? FirstName { get; set; } public string? FirstName { get; set; }
public string? LastName { get; set; } public string? LastName { get; set; }

View File

@ -2,7 +2,7 @@
{ {
public class ProjectAttendanceVM public class ProjectAttendanceVM
{ {
public List<EmployeeAttendanceVM>? AttendanceTable { get; set; } public List<DashBoardEmployeeAttendanceVM>? AttendanceTable { get; set; }
public int CheckedInEmployee { get; set; } public int CheckedInEmployee { get; set; }
public int AssignedEmployee { get; set; } public int AssignedEmployee { get; set; }
} }

View File

@ -1,11 +0,0 @@
namespace Marco.Pms.Model.ViewModels.DocumentManager
{
public class MenuSectionVM
{
public Guid Id { get; set; }
public string? Header { get; set; }
public string? Name { get; set; }
public List<MenuItemVM> Items { get; set; } = new List<MenuItemVM>();
}
}

View File

@ -1,12 +0,0 @@
namespace Marco.Pms.Model.ViewModels.DocumentManager
{
public class SubMenuItemVM
{
public Guid Id { get; set; }
public string? Name { get; set; }
public bool Available { get; set; }
public string? Link { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace Marco.Pms.Model.ViewModels.PurchaseInvoice
{
public class BasicPurchaseInvoiceVM
{
public Guid Id { get; set; }
public string? Title { get; set; }
public string? PurchaseInvoiceUId { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using Marco.Pms.Model.ViewModels.Activities;
namespace Marco.Pms.Model.ViewModels.PurchaseInvoice
{
public class DeliveryChallanVM
{
public Guid Id { get; set; }
public string? DeliveryChallanNumber { get; set; }
public DateTime DeliveryChallanDate { get; set; }
public string? Description { get; set; }
public BasicPurchaseInvoiceVM? PurchaseInvoice { get; set; }
public PurchaseInvoiceAttachmentVM? Attachment { get; set; }
public DateTime CreatedAt { get; set; }
public BasicEmployeeVM? CreatedBy { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using Marco.Pms.Model.PurchaseInvoice;
namespace Marco.Pms.Model.ViewModels.PurchaseInvoice
{
public class PurchaseInvoiceAttachmentVM
{
public Guid DocumentId { get; set; }
public InvoiceAttachmentType? InvoiceAttachmentType { get; set; }
public string? FileName { get; set; }
public string? ContentType { get; set; }
public string? PreSignedUrl { get; set; }
public string? ThumbPreSignedUrl { get; set; }
public long FileSize { get; set; }
public DateTime UploadedAt { get; set; }
}
}

View File

@ -0,0 +1,44 @@
using Marco.Pms.Model.PurchaseInvoice;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.Organization;
using Marco.Pms.Model.ViewModels.Projects;
namespace Marco.Pms.Model.ViewModels.PurchaseInvoice
{
public class PurchaseInvoiceDetailsVM
{
public Guid Id { get; set; }
public string? PurchaseInvoiceUId { get; set; }
public string? Title { get; set; }
public string? Description { get; set; }
public BasicProjectVM? Project { get; set; }
public BasicOrganizationVm? Organization { get; set; }
public PurchaseInvoiceStatus? Status { get; set; }
public string? BillingAddress { get; set; }
public string? ShippingAddress { get; set; }
public string? PurchaseOrderNumber { get; set; }
public DateTime? PurchaseOrderDate { get; set; }
public BasicOrganizationVm? Supplier { get; set; }
public string? ProformaInvoiceNumber { get; set; }
public DateTime? ProformaInvoiceDate { get; set; }
public double? ProformaInvoiceAmount { get; set; }
public string? InvoiceNumber { get; set; }
public DateTime? InvoiceDate { get; set; }
public string? EWayBillNumber { get; set; }
public DateTime? EWayBillDate { get; set; }
public string? InvoiceReferenceNumber { get; set; }
public string? AcknowledgmentNumber { get; set; }
public DateTime? AcknowledgmentDate { get; set; }
public double BaseAmount { get; set; }
public double TaxAmount { get; set; }
public double? TransportCharges { get; set; }
public double TotalAmount { get; set; }
public DateTime PaymentDueDate { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
public BasicEmployeeVM? CreatedBy { get; set; }
public DateTime? UpdatedAt { get; set; }
public BasicEmployeeVM? UpdatedBy { get; set; }
public List<PurchaseInvoiceAttachmentVM>? Attachments { get; set; }
}
}

View File

@ -0,0 +1,21 @@
using Marco.Pms.Model.PurchaseInvoice;
using Marco.Pms.Model.ViewModels.Organization;
using Marco.Pms.Model.ViewModels.Projects;
namespace Marco.Pms.Model.ViewModels.PurchaseInvoice
{
public class PurchaseInvoiceListVM
{
public Guid Id { get; set; }
public string? Title { get; set; }
public string? Description { get; set; }
public string? PurchaseInvoiceUId { get; set; }
public string? ProformaInvoiceNumber { get; set; }
public DateTime? ProformaInvoiceDate { get; set; }
public double? ProformaInvoiceAmount { get; set; }
public BasicProjectVM? Project { get; set; }
public BasicOrganizationVm? Supplier { get; set; }
public PurchaseInvoiceStatus? Status { get; set; }
public double TotalAmount { get; set; }
}
}

File diff suppressed because it is too large Load Diff

View File

@ -88,7 +88,7 @@ namespace MarcoBMS.Services.Controllers
return BadRequest(ApiResponse<object>.ErrorResponse("Employee ID is required and must not be Empty.", "Employee ID is required and must not be empty.", 400)); return BadRequest(ApiResponse<object>.ErrorResponse("Employee ID is required and must not be Empty.", "Employee ID is required and must not be empty.", 400));
} }
Employee? employee = await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.Id == employeeId && e.TenantId == tenantId); Employee? employee = await _context.Employees.Include(e => e.JobRole).FirstOrDefaultAsync(e => e.Id == employeeId);
if (employee == null) if (employee == null)
{ {
_logger.LogWarning("Employee {EmployeeId} not found", employeeId); _logger.LogWarning("Employee {EmployeeId} not found", employeeId);

View File

@ -1,9 +1,15 @@
using Marco.Pms.DataAccess.Data; using AutoMapper;
using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Dtos.Attendance; using Marco.Pms.Model.Dtos.Attendance;
using Marco.Pms.Model.Entitlements; using Marco.Pms.Model.Entitlements;
using Marco.Pms.Model.Expenses; using Marco.Pms.Model.Expenses;
using Marco.Pms.Model.OrganizationModel;
using Marco.Pms.Model.Utilities; using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.AttendanceVM;
using Marco.Pms.Model.ViewModels.DashBoard; using Marco.Pms.Model.ViewModels.DashBoard;
using Marco.Pms.Model.ViewModels.Organization;
using Marco.Pms.Model.ViewModels.Projects;
using Marco.Pms.Services.Service; using Marco.Pms.Services.Service;
using Marco.Pms.Services.Service.ServiceInterfaces; using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
@ -20,12 +26,15 @@ namespace Marco.Pms.Services.Controllers
[ApiController] [ApiController]
public class DashboardController : ControllerBase public class DashboardController : ControllerBase
{ {
private readonly IDbContextFactory<ApplicationDbContext> _dbContextFactory;
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly UserHelper _userHelper; private readonly UserHelper _userHelper;
private readonly IProjectServices _projectServices; private readonly IProjectServices _projectServices;
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly PermissionServices _permissionServices;
private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IMapper _mapper;
public static readonly Guid ActiveId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731"); public static readonly Guid ActiveId = Guid.Parse("b74da4c2-d07e-46f2-9919-e75e49b12731");
private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8"); private static readonly Guid Draft = Guid.Parse("297e0d8f-f668-41b5-bfea-e03b354251c8");
private static readonly Guid Review = Guid.Parse("6537018f-f4e9-4cb3-a210-6c3b2da999d7"); private static readonly Guid Review = Guid.Parse("6537018f-f4e9-4cb3-a210-6c3b2da999d7");
@ -40,16 +49,19 @@ namespace Marco.Pms.Services.Controllers
IProjectServices projectServices, IProjectServices projectServices,
IServiceScopeFactory serviceScopeFactory, IServiceScopeFactory serviceScopeFactory,
ILoggingService logger, ILoggingService logger,
PermissionServices permissionServices) IMapper mapper,
IDbContextFactory<ApplicationDbContext> dbContextFactory)
{ {
_context = context; _context = context;
_userHelper = userHelper; _userHelper = userHelper;
_projectServices = projectServices; _projectServices = projectServices;
_logger = logger; _logger = logger;
_serviceScopeFactory = serviceScopeFactory; _serviceScopeFactory = serviceScopeFactory;
_permissionServices = permissionServices; _mapper = mapper;
tenantId = userHelper.GetTenantId(); tenantId = userHelper.GetTenantId();
_dbContextFactory = dbContextFactory;
} }
/// <summary> /// <summary>
/// Fetches project progression data (planned and completed tasks) in graph form for a tenant and specified (or all) projects over a date range. /// Fetches project progression data (planned and completed tasks) in graph form for a tenant and specified (or all) projects over a date range.
/// </summary> /// </summary>
@ -254,8 +266,10 @@ namespace Marco.Pms.Services.Controllers
if (projectId.HasValue) if (projectId.HasValue)
{ {
using var scope = _serviceScopeFactory.CreateScope();
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
// Security Check: Ensure the requested project is in the user's accessible list. // Security Check: Ensure the requested project is in the user's accessible list.
var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value); var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.Value);
if (!hasPermission) if (!hasPermission)
{ {
_logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId} (not active or not accessible).", loggedInEmployee.Id, projectId.Value); _logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId} (not active or not accessible).", loggedInEmployee.Id, projectId.Value);
@ -340,9 +354,11 @@ namespace Marco.Pms.Services.Controllers
if (projectId.HasValue) if (projectId.HasValue)
{ {
// --- Logic for a SINGLE Project --- // --- Logic for a SINGLE Project ---
using var scope = _serviceScopeFactory.CreateScope();
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
// 2a. Security Check: Verify permission for the specific project. // 2a. Security Check: Verify permission for the specific project.
var hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee, projectId.Value); var hasPermission = await _permission.HasProjectPermission(loggedInEmployee, projectId.Value);
if (!hasPermission) if (!hasPermission)
{ {
_logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, projectId.Value); _logger.LogWarning("Access DENIED for user {UserId} on project {ProjectId}.", loggedInEmployee.Id, projectId.Value);
@ -499,7 +515,7 @@ namespace Marco.Pms.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse( return Ok(ApiResponse<object>.SuccessResponse(
new ProjectAttendanceVM new ProjectAttendanceVM
{ {
AttendanceTable = new List<EmployeeAttendanceVM>(), AttendanceTable = new List<DashBoardEmployeeAttendanceVM>(),
CheckedInEmployee = 0, CheckedInEmployee = 0,
AssignedEmployee = 0 AssignedEmployee = 0
}, },
@ -523,7 +539,7 @@ namespace Marco.Pms.Services.Controllers
.Join(employees, .Join(employees,
attendance => attendance.EmployeeId, attendance => attendance.EmployeeId,
employee => employee.Id, employee => employee.Id,
(attendance, employee) => new EmployeeAttendanceVM (attendance, employee) => new DashBoardEmployeeAttendanceVM
{ {
FirstName = employee.FirstName, FirstName = employee.FirstName,
LastName = employee.LastName, LastName = employee.LastName,
@ -669,7 +685,11 @@ namespace Marco.Pms.Services.Controllers
// Step 3: Check if logged-in employee has permission for this project // Step 3: Check if logged-in employee has permission for this project
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
bool hasPermission = await _permissionServices.HasProjectPermission(loggedInEmployee!, projectId);
using var scope = _serviceScopeFactory.CreateScope();
var _permission = scope.ServiceProvider.GetRequiredService<PermissionServices>();
bool hasPermission = await _permission.HasProjectPermission(loggedInEmployee!, projectId);
if (!hasPermission) if (!hasPermission)
{ {
_logger.LogWarning("Unauthorized access by EmployeeId: {EmployeeId} to ProjectId: {ProjectId}", loggedInEmployee.Id, projectId); _logger.LogWarning("Unauthorized access by EmployeeId: {EmployeeId} to ProjectId: {ProjectId}", loggedInEmployee.Id, projectId);
@ -747,7 +767,6 @@ namespace Marco.Pms.Services.Controllers
return Ok(ApiResponse<object>.SuccessResponse(sortedResult, $"{sortedResult.Count} records fetched for attendance overview", 200)); return Ok(ApiResponse<object>.SuccessResponse(sortedResult, $"{sortedResult.Count} records fetched for attendance overview", 200));
} }
[HttpGet("expense/monthly")] [HttpGet("expense/monthly")]
public async Task<IActionResult> GetExpenseReportByProjectsAsync([FromQuery] Guid? projectId, [FromQuery] Guid? categoryId, [FromQuery] int months) public async Task<IActionResult> GetExpenseReportByProjectsAsync([FromQuery] Guid? projectId, [FromQuery] Guid? categoryId, [FromQuery] int months)
{ {
@ -1074,5 +1093,546 @@ namespace Marco.Pms.Services.Controllers
ApiResponse<object>.ErrorResponse("An error occurred while fetching pending expenses.", "An error occurred while fetching pending expenses.", 500)); // [Error Response] ApiResponse<object>.ErrorResponse("An error occurred while fetching pending expenses.", "An error occurred while fetching pending expenses.", 500)); // [Error Response]
} }
} }
/// <summary>
/// Retrieves today's attendance details for a specific employee on a given project,
/// defaulting to the currently logged-in employee when no employeeId is provided.
/// Includes related project and employee information for UI display.
/// </summary>
/// <param name="projectId">The project identifier whose attendance is requested.</param>
/// <param name="employeeId">
/// Optional employee identifier. When null, the currently logged-in employee is used.
/// </param>
/// <returns>
/// 200 OK with an <see cref="EmployeeAttendanceVM"/> payload on success, or a standardized
/// error envelope on validation or processing failure.
/// </returns>
[HttpGet("get/attendance/employee/{projectId}")]
public async Task<IActionResult> GetAttendanceByEmployeeAsync(Guid projectId, [FromQuery] Guid? employeeId)
{
// TenantId is assumed to come from a base controller, HttpContext, or similar.
if (tenantId == Guid.Empty)
{
_logger.LogWarning("GetAttendanceByEmployeeAsync called with empty TenantId. ProjectId={ProjectId}", projectId);
return BadRequest(
ApiResponse<object>.ErrorResponse("Invalid tenant information.", "TenantId is empty in GetAttendanceByEmployeeAsync.", 400));
}
if (projectId == Guid.Empty)
{
_logger.LogWarning("GetAttendanceByEmployeeAsync called with empty ProjectId. TenantId={TenantId}", tenantId);
return BadRequest(
ApiResponse<object>.ErrorResponse("Project reference is required.", "ProjectId is empty in GetAttendanceByEmployeeAsync.", 400));
}
// Resolve the currently logged-in employee (e.g., from token or session).
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var attendanceEmployeeId = employeeId ?? loggedInEmployee.Id;
try
{
// Step 1: Ensure employee is allocated to the project for this tenant.
var projectAllocation = await _context.ProjectAllocations
.Include(pa => pa.Employee)!.ThenInclude(e => e!.JobRole)
.Include(pa => pa.Employee)!.ThenInclude(e => e!.Organization)
.Include(pa => pa.Project)
.FirstOrDefaultAsync(pa =>
pa.ProjectId == projectId &&
pa.EmployeeId == attendanceEmployeeId &&
pa.IsActive &&
pa.TenantId == tenantId);
if (projectAllocation == null)
{
_logger.LogWarning(
"GetAttendanceByEmployeeAsync failed: Employee not allocated to project. TenantId={TenantId}, ProjectId={ProjectId}, EmployeeId={EmployeeId}, RequestedById={RequestedById}",
tenantId, projectId, attendanceEmployeeId, loggedInEmployee.Id);
return BadRequest(ApiResponse<object>.ErrorResponse("The employee is not allocated to the selected project.", "Project allocation not found for given ProjectId, EmployeeId, and TenantId.",
400));
}
// Step 2: Fetch today's attendance (if any) for the selected employee and project.
var today = DateTime.UtcNow.Date; // Prefer UTC for server-side comparisons.
var attendance = await _context.Attendes
.Include(a => a.Approver)
.Include(a => a.RequestedBy)
.FirstOrDefaultAsync(a =>
a.TenantId == tenantId &&
a.EmployeeId == attendanceEmployeeId &&
a.ProjectID == projectId &&
a.AttendanceDate.Date == today);
// Step 3: Map to view model with defensive null handling.
var attendanceVm = new EmployeeAttendanceVM
{
Id = attendance?.Id ?? Guid.Empty,
EmployeeAvatar = null, // Can be filled from a file service or CDN later.
EmployeeId = projectAllocation.EmployeeId,
FirstName = projectAllocation.Employee?.FirstName,
OrganizationName = projectAllocation.Employee?.Organization?.Name,
LastName = projectAllocation.Employee?.LastName,
JobRoleName = projectAllocation.Employee?.JobRole?.Name,
ProjectId = projectId,
ProjectName = projectAllocation.Project?.Name,
CheckInTime = attendance?.InTime,
CheckOutTime = attendance?.OutTime,
Activity = attendance?.Activity ?? ATTENDANCE_MARK_TYPE.CHECK_IN,
ApprovedAt = attendance?.ApprovedAt,
Approver = attendance == null
? null
: _mapper.Map<BasicEmployeeVM>(attendance.Approver),
RequestedAt = attendance?.RequestedAt,
RequestedBy = attendance == null
? null
: _mapper.Map<BasicEmployeeVM>(attendance.RequestedBy)
};
_logger.LogInfo("GetAttendanceByEmployeeAsync completed successfully. TenantId={TenantId}, ProjectId={ProjectId}, EmployeeId={EmployeeId}, HasAttendance={HasAttendance}",
tenantId, projectId, attendanceEmployeeId, attendance != null);
return Ok(ApiResponse<object>.SuccessResponse(attendanceVm, "Attendance fetched successfully.", 200));
}
catch (OperationCanceledException)
{
_logger.LogWarning("GetAttendanceByEmployeeAsync was canceled. TenantId={TenantId}, ProjectId={ProjectId}, EmployeeId={EmployeeId}",
tenantId, projectId, attendanceEmployeeId);
return StatusCode(499, ApiResponse<object>.ErrorResponse("The request was canceled.", "GetAttendanceByEmployeeAsync was canceled by the client.", 499));
}
catch (Exception ex)
{
_logger.LogError(ex, "GetAttendanceByEmployeeAsync failed with an unexpected error. TenantId={TenantId}, ProjectId={ProjectId}, EmployeeId={EmployeeId}",
tenantId, projectId, attendanceEmployeeId);
return StatusCode(500, ApiResponse<object>.ErrorResponse("An error occurred while fetching attendance.", "Unhandled exception in GetAttendanceByEmployeeAsync.", 500));
}
}
/// <summary>
/// Returns a high-level collection overview (aging buckets, due vs collected, top client)
/// for invoices of the current tenant, optionally filtered by project.
/// </summary>
/// <param name="projectId">Optional project identifier to filter invoices.</param>
/// <returns>Standardized API response with collection KPIs.</returns>
[HttpGet("collection-overview")]
public async Task<IActionResult> GetCollectionOverviewAsync([FromQuery] Guid? projectId)
{
// Correlation ID pattern for distributed tracing (if you use one)
var correlationId = HttpContext.TraceIdentifier;
if (tenantId == Guid.Empty)
{
_logger.LogWarning("Invalid request: TenantId is empty on progression endpoint");
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid TenantId", "Provided Invalid TenantId", 400));
}
_logger.LogInfo("Started GetCollectionOverviewAsync. CorrelationId: {CorrelationId}, ProjectId: {ProjectId}", correlationId, projectId ?? Guid.Empty);
try
{
// Validate and identify current employee/tenant context
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Base invoice query for this tenant; AsNoTracking for read-only performance [web:1][web:5]
var invoiceQuery = _context.Invoices
.Where(i => i.TenantId == tenantId && i.IsActive)
.Include(i => i.BilledTo)
.AsNoTracking();
// Fetch infra and service projects in parallel using factory-created contexts
// NOTE: Avoid Task.Run over async IO where possible. Here each uses its own context instance. [web:6][web:15]
var infraProjectTask = GetInfraProjectsAsync(tenantId);
var serviceProjectTask = GetServiceProjectsAsync(tenantId);
await Task.WhenAll(infraProjectTask, serviceProjectTask);
var projects = infraProjectTask.Result
.Union(serviceProjectTask.Result)
.ToList();
// Optional project filter: validate existence in cached list first
if (projectId.HasValue)
{
var project = projects.FirstOrDefault(p => p.Id == projectId.Value);
if (project == null)
{
_logger.LogWarning(
"Project {ProjectId} not found for tenant {TenantId} in GetCollectionOverviewAsync. CorrelationId: {CorrelationId}",
projectId, tenantId, correlationId);
return StatusCode(
StatusCodes.Status404NotFound,
ApiResponse<object>.ErrorResponse(
"Project Not Found",
"The requested project does not exist or is not associated with the current tenant.",
StatusCodes.Status404NotFound));
}
invoiceQuery = invoiceQuery.Where(i => i.ProjectId == projectId.Value);
}
var invoices = await invoiceQuery.ToListAsync();
if (invoices.Count == 0)
{
_logger.LogInfo(
"No invoices found for tenant {TenantId} in GetCollectionOverviewAsync. CorrelationId: {CorrelationId}",
tenantId, correlationId);
// Return an empty but valid overview instead of 404 endpoint is conceptually valid
var emptyResponse = new
{
TotalDueAmount = 0d,
TotalCollectedAmount = 0d,
TotalValue = 0d,
PendingPercentage = 0d,
CollectedPercentage = 0d,
Bucket0To30Invoices = 0,
Bucket30To60Invoices = 0,
Bucket60To90Invoices = 0,
Bucket90PlusInvoices = 0,
Bucket0To30Amount = 0d,
Bucket30To60Amount = 0d,
Bucket60To90Amount = 0d,
Bucket90PlusAmount = 0d,
TopClientBalance = 0d,
TopClient = new BasicOrganizationVm()
};
return Ok(ApiResponse<object>.SuccessResponse(emptyResponse, "No invoices found for the current tenant and filters; returning empty collection overview.", 200));
}
var invoiceIds = invoices.Select(i => i.Id).ToList();
// Pre-aggregate payments per invoice in the DB where possible [web:1][web:17]
var paymentGroups = await _context.ReceivedInvoicePayments
.AsNoTracking()
.Where(p => invoiceIds.Contains(p.InvoiceId) && p.TenantId == tenantId)
.GroupBy(p => p.InvoiceId)
.Select(g => new
{
InvoiceId = g.Key,
PaidAmount = g.Sum(p => p.Amount)
})
.ToListAsync();
// Create a lookup to avoid repeated LINQ Where on each iteration
var paymentsLookup = paymentGroups.ToDictionary(p => p.InvoiceId, p => p.PaidAmount);
double totalDueAmount = 0;
var today = DateTime.UtcNow.Date; // use UTC for consistency [web:17]
var bucketOneInvoices = 0;
double bucketOneAmount = 0;
var bucketTwoInvoices = 0;
double bucketTwoAmount = 0;
var bucketThreeInvoices = 0;
double bucketThreeAmount = 0;
var bucketFourInvoices = 0;
double bucketFourAmount = 0;
// Main aging calculation loop
foreach (var invoice in invoices)
{
var total = invoice.BasicAmount + invoice.TaxAmount;
var paid = paymentsLookup.TryGetValue(invoice.Id, out var paidAmount)
? paidAmount
: 0d;
var balance = total - paid;
// Skip fully paid or explicitly completed invoices
if (balance <= 0 || invoice.MarkAsCompleted)
continue;
totalDueAmount += balance;
// Only consider invoices with expected payment date up to today for aging
var expectedDate = invoice.ExceptedPaymentDate.Date;
if (expectedDate > today)
continue;
var days = (today - expectedDate).Days;
if (days <= 30 && days > 0)
{
bucketOneInvoices++;
bucketOneAmount += balance;
}
else if (days > 30 && days <= 60)
{
bucketTwoInvoices++;
bucketTwoAmount += balance;
}
else if (days > 60 && days <= 90)
{
bucketThreeInvoices++;
bucketThreeAmount += balance;
}
else if (days > 90)
{
bucketFourInvoices++;
bucketFourAmount += balance;
}
}
var totalCollectedAmount = paymentGroups.Sum(p => p.PaidAmount);
var totalValue = totalDueAmount + totalCollectedAmount;
var pendingPercentage = totalValue > 0 ? (totalDueAmount / totalValue) * 100 : 0;
var collectedPercentage = totalValue > 0 ? (totalCollectedAmount / totalValue) * 100 : 0;
// Determine top client by outstanding balance
double topClientBalance = 0;
Organization topClient = new Organization();
var groupedByClient = invoices
.Where(i => i.BilledToId.HasValue && i.BilledTo != null)
.GroupBy(i => i.BilledToId);
foreach (var group in groupedByClient)
{
var clientInvoiceIds = group.Select(i => i.Id).ToList();
var totalForClient = group.Sum(i => i.BasicAmount + i.TaxAmount);
var paidForClient = paymentGroups
.Where(pg => clientInvoiceIds.Contains(pg.InvoiceId))
.Sum(pg => pg.PaidAmount);
var clientBalance = totalForClient - paidForClient;
if (clientBalance > topClientBalance)
{
topClientBalance = clientBalance;
topClient = group.First()!.BilledTo!;
}
}
BasicOrganizationVm topClientVm = new BasicOrganizationVm();
if (topClient != null)
{
topClientVm = new BasicOrganizationVm
{
Id = topClient.Id,
Name = topClient.Name,
Email = topClient.Email,
ContactPerson = topClient.ContactPerson,
ContactNumber = topClient.ContactNumber,
Address = topClient.Address,
GSTNumber = topClient.GSTNumber,
SPRID = topClient.SPRID
};
}
var response = new
{
TotalDueAmount = totalDueAmount,
TotalCollectedAmount = totalCollectedAmount,
TotalValue = totalValue,
PendingPercentage = Math.Round(pendingPercentage, 2),
CollectedPercentage = Math.Round(collectedPercentage, 2),
Bucket0To30Invoices = bucketOneInvoices,
Bucket30To60Invoices = bucketTwoInvoices,
Bucket60To90Invoices = bucketThreeInvoices,
Bucket90PlusInvoices = bucketFourInvoices,
Bucket0To30Amount = bucketOneAmount,
Bucket30To60Amount = bucketTwoAmount,
Bucket60To90Amount = bucketThreeAmount,
Bucket90PlusAmount = bucketFourAmount,
TopClientBalance = topClientBalance,
TopClient = topClientVm
};
_logger.LogInfo("Successfully completed GetCollectionOverviewAsync for tenant {TenantId}. CorrelationId: {CorrelationId}, TotalInvoices: {InvoiceCount}, TotalValue: {TotalValue}",
tenantId, correlationId, invoices.Count, totalValue);
return Ok(ApiResponse<object>.SuccessResponse(response, "Collection overview fetched successfully.", 200));
}
catch (Exception ex)
{
// Centralized logging for unhandled exceptions with context, no sensitive data [web:1][web:5][web:10]
_logger.LogError(ex, "Unhandled exception in GetCollectionOverviewAsync. CorrelationId: {CorrelationId}", correlationId);
// Generic but consistent error payload; let global exception handler standardize if you use ProblemDetails [web:10][web:13][web:16]
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error",
"An unexpected error occurred while generating the collection overview. Please try again or contact support with the correlation identifier.", 500));
}
}
[HttpGet("purchase-invoice-overview")]
public async Task<IActionResult> GetPurchaseInvoiceOverview()
{
// Correlation id for tracing this request across services/logs.
var correlationId = HttpContext.TraceIdentifier;
_logger.LogInfo("GetPurchaseInvoiceOverview started. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId);
// Basic guard: invalid tenant.
if (tenantId == Guid.Empty)
{
_logger.LogWarning("GetPurchaseInvoiceOverview rejected due to empty TenantId. CorrelationId: {CorrelationId}", correlationId);
return BadRequest(ApiResponse<object>.ErrorResponse("Invalid TenantId", "The tenant identifier provided is invalid or missing.", 400));
}
try
{
// Fetch current employee context (if needed for authorization/audit).
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Run project queries in parallel to reduce latency.
var infraProjectTask = GetInfraProjectsAsync(tenantId);
var serviceProjectTask = GetServiceProjectsAsync(tenantId);
await Task.WhenAll(infraProjectTask, serviceProjectTask);
var projects = infraProjectTask.Result
.Union(serviceProjectTask.Result)
.ToList();
_logger.LogDebug("GetPurchaseInvoiceOverview loaded projects. Count: {ProjectCount}, TenantId: {TenantId}, CorrelationId: {CorrelationId}", projects.Count, tenantId, correlationId);
// Query purchase invoices for the tenant.
var purchaseInvoices = await _context.PurchaseInvoiceDetails
.Include(pid => pid.Supplier)
.Include(pid => pid.Status)
.AsNoTracking()
.Where(pid => pid.TenantId == tenantId && pid.IsActive)
.ToListAsync();
_logger.LogInfo("GetPurchaseInvoiceOverview loaded invoices. InvoiceCount: {InvoiceCount}, TenantId: {TenantId}, CorrelationId: {CorrelationId}",
purchaseInvoices.Count, tenantId, correlationId);
if (!purchaseInvoices.Any())
{
// No invoices is not an error; return an empty but well-structured overview.
_logger.LogInfo("GetPurchaseInvoiceOverview: No active purchase invoices found. TenantId: {TenantId}, CorrelationId: {CorrelationId}", tenantId, correlationId);
var emptyResponse = new
{
TotalInvoices = 0,
TotalValue = 0m,
AverageValue = 0m,
StatusBreakdown = Array.Empty<object>(),
ProjectBreakdown = Array.Empty<object>(),
TopSupplier = (object?)null
};
return Ok(ApiResponse<object>.SuccessResponse(
emptyResponse,
"No active purchase invoices found for the specified tenant.",
StatusCodes.Status200OK));
}
var totalInvoices = purchaseInvoices.Count;
var totalValue = purchaseInvoices.Sum(pid => pid.BaseAmount);
// Guard against divide-by-zero (in case BaseAmount is all zero).
var averageValue = totalInvoices > 0
? totalValue / totalInvoices
: 0;
// Status-wise aggregation
var statusBreakdown = purchaseInvoices
.Where(pid => pid.Status != null)
.GroupBy(pid => pid.StatusId)
.Select(g => new
{
Id = g.Key,
Name = g.First().Status!.DisplayName,
Count = g.Count(),
TotalValue = g.Sum(pid => pid.BaseAmount),
Percentage = totalValue > 0
? Math.Round(g.Sum(pid => pid.BaseAmount) / totalValue * 100, 2)
: 0
})
.OrderByDescending(x => x.TotalValue)
.ToList();
// Project-wise aggregation (top 3 by value)
var projectBreakdown = purchaseInvoices
.GroupBy(pid => pid.ProjectId)
.Select(g => new
{
Id = g.Key,
Name = projects.FirstOrDefault(p => p.Id == g.Key)?.Name ?? "Unknown Project",
Count = g.Count(),
TotalValue = g.Sum(pid => pid.BaseAmount),
Percentage = totalValue > 0
? Math.Round(g.Sum(pid => pid.BaseAmount) / totalValue * 100, 2)
: 0
})
.OrderByDescending(pid => pid.TotalValue)
.Take(3)
.ToList();
// Top supplier by total value
var supplierBreakdown = purchaseInvoices
.Where(pid => pid.Supplier != null)
.GroupBy(pid => pid.SupplierId)
.Select(g => new
{
Id = g.Key,
Name = g.First().Supplier!.Name,
Count = g.Count(),
TotalValue = g.Sum(pid => pid.BaseAmount),
Percentage = totalValue > 0
? Math.Round(g.Sum(pid => pid.BaseAmount) / totalValue * 100, 2)
: 0
})
.OrderByDescending(pid => pid.TotalValue)
.FirstOrDefault();
var response = new
{
TotalInvoices = totalInvoices,
TotalValue = Math.Round(totalValue, 2),
AverageValue = Math.Round(averageValue, 2),
StatusBreakdown = statusBreakdown,
ProjectBreakdown = projectBreakdown,
TopSupplier = supplierBreakdown
};
_logger.LogInfo("GetPurchaseInvoiceOverview completed successfully. TenantId: {TenantId}, TotalInvoices: {TotalInvoices}, TotalValue: {TotalValue}, CorrelationId: {CorrelationId}",
tenantId, totalInvoices, totalValue, correlationId);
return Ok(ApiResponse<object>.SuccessResponse(response, "Purchase invoice overview retrieved successfully.", 200));
}
catch (Exception ex)
{
// Capture complete context for diagnostics, but ensure no sensitive data is logged.
_logger.LogError(ex, "Error occurred while processing GetPurchaseInvoiceOverview. TenantId: {TenantId}, CorrelationId: {CorrelationId}",
tenantId, correlationId);
// Do not expose internal details to clients. Return a generic 500 response.
return StatusCode(500, ApiResponse<object>.ErrorResponse("Internal Server Error", "An unexpected error occurred while processing the purchase invoice overview.", 500));
}
}
/// <summary>
/// Gets infrastructure projects for a tenant as a lightweight view model.
/// </summary>
private async Task<List<BasicProjectVM>> GetInfraProjectsAsync(Guid tenantId)
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Projects
.AsNoTracking()
.Where(p => p.TenantId == tenantId)
.Select(p => new BasicProjectVM { Id = p.Id, Name = p.Name })
.ToListAsync();
}
/// <summary>
/// Gets service projects for a tenant as a lightweight view model.
/// </summary>
private async Task<List<BasicProjectVM>> GetServiceProjectsAsync(Guid tenantId)
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ServiceProjects
.AsNoTracking()
.Where(sp => sp.TenantId == tenantId)
.Select(sp => new BasicProjectVM { Id = sp.Id, Name = sp.Name })
.ToListAsync();
}
} }
} }

View File

@ -70,6 +70,49 @@ namespace Marco.Pms.Services.Controllers
#endregion #endregion
#region =================================================================== Purchase Invoice Status APIs ===================================================================
/// <summary>
/// Retrieves the purchase invoice status.
/// </summary>
/// <param name="ct">The cancellation token.</param>
/// <returns>The HTTP response containing the purchase invoice status.</returns>
[HttpGet("purchase-invoice-status/list")]
public async Task<IActionResult> GetPurchaseInvoiceStatus(CancellationToken ct)
{
// Get the currently logged-in employee.
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Retrieve the purchase invoice status asynchronously.
var response = await _masterService.GetPurchaseInvoiceStatusAsync(loggedInEmployee, ct);
// Return the HTTP response with the purchase invoice status.
return StatusCode(response.StatusCode, response);
}
#endregion
#region =================================================================== Invoice Attachment Type APIs ===================================================================
/// <summary>
/// Retrieves the invoice attachment types.
/// </summary>
/// <param name="ct">The cancellation token.</param>
/// <returns>The HTTP response containing the invoice attachment types.</returns>
[HttpGet("invoice-attachment-type/list")]
public async Task<IActionResult> GetInvoiceAttachmentType(CancellationToken ct)
{
// Get the currently logged-in employee.
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Retrieve the invoice attachment types asynchronously.
var response = await _masterService.GetInvoiceAttachmentTypeAsync(loggedInEmployee, ct);
// Return the HTTP response with the invoice attachment types.
return StatusCode(response.StatusCode, response);
}
#endregion
#region =================================================================== Currency APIs =================================================================== #region =================================================================== Currency APIs ===================================================================
[HttpGet("currencies/list")] [HttpGet("currencies/list")]

View File

@ -45,6 +45,14 @@ namespace Marco.Pms.Services.Controllers
return StatusCode(response.StatusCode, response); return StatusCode(response.StatusCode, response);
} }
[HttpGet("list/basic")]
public async Task<IActionResult> GetOrganizationBasicList([FromQuery] Guid? id, [FromQuery] string? searchString, CancellationToken ct, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _organizationService.GetOrganizationBasicListAsync(id, searchString, pageNumber, pageSize, loggedInEmployee, ct);
return StatusCode(response.StatusCode, response);
}
[HttpGet("details/{id}")] [HttpGet("details/{id}")]
public async Task<IActionResult> GetOrganizationDetails(Guid id) public async Task<IActionResult> GetOrganizationDetails(Guid id)
{ {

View File

@ -41,11 +41,11 @@ namespace MarcoBMS.Services.Controllers
#region =================================================================== Project Get APIs =================================================================== #region =================================================================== Project Get APIs ===================================================================
[HttpGet("list/basic/all")] [HttpGet("list/basic/all")]
public async Task<IActionResult> GetBothProjectBasicList([FromQuery] string? searchString) public async Task<IActionResult> GetBothProjectBasicList([FromQuery] Guid? id, [FromQuery] string? searchString)
{ {
// Get the current user // Get the current user
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync(); var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _projectServices.GetBothProjectBasicListAsync(searchString, loggedInEmployee, tenantId); var response = await _projectServices.GetBothProjectBasicListAsync(id, searchString, loggedInEmployee, tenantId);
return StatusCode(response.StatusCode, response); return StatusCode(response.StatusCode, response);
} }

View File

@ -0,0 +1,218 @@
using AutoMapper;
using Marco.Pms.Model.Dtos.Collection;
using Marco.Pms.Model.Dtos.PurchaseInvoice;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Helpers;
using Microsoft.AspNetCore.JsonPatch;
using Microsoft.AspNetCore.Mvc;
namespace Marco.Pms.Services.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class PurchaseInvoiceController : ControllerBase
{
private readonly UserHelper _userHelper;
private readonly IPurchaseInvoiceService _purchaseInvoiceService;
private readonly ISignalRService _signalR;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly Guid tenantId;
public PurchaseInvoiceController(UserHelper userHelper, IPurchaseInvoiceService purchaseInvoiceService, ISignalRService signalR, IServiceScopeFactory serviceScopeFactory)
{
_userHelper = userHelper;
_purchaseInvoiceService = purchaseInvoiceService;
tenantId = _userHelper.GetTenantId();
_signalR = signalR;
_serviceScopeFactory = serviceScopeFactory;
}
#region =================================================================== Purchase Invoice Functions ===================================================================
/// <summary>
/// Retrieves a list of purchase invoices based on search string, filter, activity status, page size, and page number.
/// </summary>
/// <param name="searchString">Optional search string to filter invoices by.</param>
/// <param name="filter">Optional filter to apply to the invoices.</param>
/// <param name="isActive">Optional flag to filter invoices by activity status.</param>
/// <param name="pageSize">The number of invoices to display per page.</param>
/// <param name="pageNumber">The requested page number (1-based).</param>
/// <param name="cancellationToken">Token to propagate notification that operations should be canceled.</param>
/// <returns>A HTTP 200 OK response with a list of purchase invoices or an appropriate HTTP error code.</returns>
[HttpGet("list")]
public async Task<IActionResult> GetPurchaseInvoiceList([FromQuery] string? searchString, [FromQuery] string? filter, CancellationToken cancellationToken, [FromQuery] bool isActive = true,
[FromQuery] int pageSize = 20, [FromQuery] int pageNumber = 1)
{
// Get the currently logged-in employee
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Retrieve the purchase invoice list using the service
var response = await _purchaseInvoiceService.GetPurchaseInvoiceListAsync(searchString, filter, isActive, pageSize, pageNumber, loggedInEmployee, tenantId, cancellationToken);
// Return the response with the appropriate HTTP status code
return StatusCode(response.StatusCode, response);
}
[HttpGet("details/{id}")]
public async Task<IActionResult> GetPurchaseInvoiceDetails(Guid id, CancellationToken cancellationToken)
{
// Get the currently logged-in employee
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Retrieve the purchase invoice details using the service
var response = await _purchaseInvoiceService.GetPurchaseInvoiceDetailsAsync(id, loggedInEmployee, tenantId, cancellationToken);
// Return the response with the appropriate HTTP status code
return StatusCode(response.StatusCode, response);
}
/// <summary>
/// Creates a purchase invoice.
/// </summary>
/// <param name="model">The purchase invoice model.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>The HTTP response for the creation of the purchase invoice.</returns>
[HttpPost("create")]
public async Task<IActionResult> CreatePurchaseInvoice([FromBody] PurchaseInvoiceDto model, CancellationToken ct)
{
// Get the currently logged-in employee
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Create a purchase invoice using the purchase invoice service
var response = await _purchaseInvoiceService.CreatePurchaseInvoiceAsync(model, loggedInEmployee, tenantId, ct);
// If the creation is successful, send a notification to the SignalR service
if (response.Success)
{
var notification = new
{
LoggedInUserId = loggedInEmployee.Id,
Keyword = "Purchase_Invoice",
Response = response.Data
};
await _signalR.SendNotificationAsync(notification);
}
// Return the HTTP response
return StatusCode(response.StatusCode, response);
}
[HttpPatch("edit/{id}")]
public async Task<IActionResult> EditPurchaseInvoice(Guid id, [FromBody] JsonPatchDocument<PurchaseInvoiceDto> patchDoc, CancellationToken ct)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var existingPurchaseInvoice = await _purchaseInvoiceService.GetPurchaseInvoiceByIdAsync(id, tenantId, ct);
if (existingPurchaseInvoice == null)
return NotFound(ApiResponse<object>.ErrorResponse("Invalid purchase invoice ID", "Invalid purchase invoice ID", 404));
using var scope = _serviceScopeFactory.CreateScope();
var mapper = scope.ServiceProvider.GetRequiredService<IMapper>();
var modelToPatch = mapper.Map<PurchaseInvoiceDto>(existingPurchaseInvoice);
// Apply the JSON Patch document to the DTO and check model state validity
patchDoc.ApplyTo(modelToPatch, ModelState);
if (!ModelState.IsValid)
{
return BadRequest(ApiResponse<object>.ErrorResponse("Validation failed", "Provided patch document values are invalid", 400));
}
var response = await _purchaseInvoiceService.UpdatePurchaseInvoiceAsync(id, existingPurchaseInvoice, modelToPatch, loggedInEmployee, tenantId, ct);
return StatusCode(response.StatusCode, response);
}
[HttpDelete("delete/{id}")]
public async Task<IActionResult> DeletePurchaseInvoice(Guid id, CancellationToken ct, [FromQuery] bool isActive = false)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _purchaseInvoiceService.DeletePurchaseInvoiceAsync(id, isActive, loggedInEmployee, tenantId, ct);
if (response.Success)
{
var notification = new
{
LoggedInUserId = loggedInEmployee.Id,
Keyword = "Purchase_Invoice",
Response = response.Data
};
await _signalR.SendNotificationAsync(notification);
}
return StatusCode(response.StatusCode, response);
}
#endregion
#region =================================================================== Delivery Challan Functions ===================================================================
[HttpGet("delivery-challan/list/{purchaseInvoiceId}")]
public async Task<IActionResult> GetDeliveryChallans(Guid purchaseInvoiceId, CancellationToken cancellationToken)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _purchaseInvoiceService.GetDeliveryChallansAsync(purchaseInvoiceId, loggedInEmployee, tenantId, cancellationToken);
return StatusCode(response.StatusCode, response);
}
/// <summary>
/// Adds a delivery challan.
/// </summary>
/// <param name="model">The delivery challan model.</param>
/// <param name="ct">The cancellation token.</param>
/// <returns>The HTTP response for adding the delivery challan.</returns>
[HttpPost("delivery-challan/create")]
public async Task<IActionResult> AddDeliveryChallan([FromBody] DeliveryChallanDto model, CancellationToken ct)
{
// Get the currently logged-in employee
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
// Add the delivery challan using the purchase invoice service
var response = await _purchaseInvoiceService.AddDeliveryChallanAsync(model, loggedInEmployee, tenantId, ct);
// If the addition is successful, send a notification to the SignalR service
if (response.Success)
{
var notification = new
{
LoggedInUserId = loggedInEmployee.Id,
Keyword = "Delivery_Challan",
Response = response.Data
};
await _signalR.SendNotificationAsync(notification);
}
// Return the HTTP response
return StatusCode(response.StatusCode, response);
}
#endregion
#region =================================================================== Purchase Invoice History Functions ===================================================================
[HttpGet("payment-history/list/{purchaseInvoiceId}")]
public async Task<IActionResult> GetPurchaseInvoiceHistoryList(Guid purchaseInvoiceId, CancellationToken cancellationToken)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _purchaseInvoiceService.GetPurchaseInvoiceHistoryListAsync(purchaseInvoiceId, loggedInEmployee, tenantId, cancellationToken);
return StatusCode(response.StatusCode, response);
}
[HttpPost("add/payment")]
public async Task<IActionResult> AddPurchaseInvoicePayment([FromBody] ReceivedInvoicePaymentDto model, CancellationToken ct)
{
var loggedInEmployee = await _userHelper.GetCurrentEmployeeAsync();
var response = await _purchaseInvoiceService.AddPurchaseInvoicePaymentAsync(model, loggedInEmployee, tenantId, ct);
if (response.Success)
{
var notification = new
{
LoggedInUserId = loggedInEmployee.Id,
Keyword = "Purchase_Invoice_Payment",
Response = response.Data
};
await _signalR.SendNotificationAsync(notification);
}
// Return the HTTP response
return StatusCode(response.StatusCode, response);
}
#endregion
}
}

View File

@ -384,7 +384,10 @@ namespace Marco.Pms.Services.Controllers
response.CreatedBy = createdBy; response.CreatedBy = createdBy;
response.CurrentPlan = _mapper.Map<SubscriptionPlanDetailsVM>(currentPlan); response.CurrentPlan = _mapper.Map<SubscriptionPlanDetailsVM>(currentPlan);
response.CurrentPlan.PaymentDetail = paymentsDetails.FirstOrDefault(pd => currentPlan != null && pd.Id == currentPlan.PaymentDetailId); if (currentPlan != null)
{
response.CurrentPlan.PaymentDetail = paymentsDetails.FirstOrDefault(pd => pd.Id == currentPlan.PaymentDetailId);
}
response.CurrentPlanFeatures = await _featureDetailsHelper.GetFeatureDetails(currentPlan?.Plan?.FeaturesId ?? Guid.Empty); response.CurrentPlanFeatures = await _featureDetailsHelper.GetFeatureDetails(currentPlan?.Plan?.FeaturesId ?? Guid.Empty);
// Map subscription history plans to DTO // Map subscription history plans to DTO

View File

@ -1,4 +1,5 @@
using Marco.Pms.Model.Filters; using Marco.Pms.Model.Filters;
using System.Data;
using System.Linq.Dynamic.Core; using System.Linq.Dynamic.Core;
namespace Marco.Pms.Services.Extensions namespace Marco.Pms.Services.Extensions
@ -69,9 +70,16 @@ namespace Marco.Pms.Services.Extensions
return query; return query;
} }
/// <summary>
/// Applies search filters to the given IQueryable.
/// </summary>
/// <typeparam name="T">The type of the elements in the IQueryable.</typeparam>
/// <param name="query">The IQueryable to apply the filters to.</param>
/// <param name="searchFilters">The list of search filters to apply.</param>
/// <returns>The filtered IQueryable.</returns>
public static IQueryable<T> ApplySearchFilters<T>(this IQueryable<T> query, List<SearchItem> searchFilters) public static IQueryable<T> ApplySearchFilters<T>(this IQueryable<T> query, List<SearchItem> searchFilters)
{ {
// 1. Apply Search Filters (Contains/Text search) // Apply search filters to the query
if (searchFilters.Any()) if (searchFilters.Any())
{ {
foreach (var search in searchFilters) foreach (var search in searchFilters)
@ -86,10 +94,70 @@ namespace Marco.Pms.Services.Extensions
return query; return query;
} }
/// <summary>
/// Applies group by filters to the given IQueryable.
/// </summary>
/// <typeparam name="T">The type of the elements in the IQueryable.</typeparam>
/// <param name="query">The IQueryable to apply the filters to.</param>
/// <param name="groupByColumn">The column to group by.</param>
/// <returns>The grouped IQueryable.</returns>
public static IQueryable<T> ApplyGroupByFilters<T>(this IQueryable<T> query, string groupByColumn) public static IQueryable<T> ApplyGroupByFilters<T>(this IQueryable<T> query, string groupByColumn)
{ {
// Group the query by the specified column and reshape the result to { Key: "Value", Items: [...] }
query.GroupBy(groupByColumn, "it") query.GroupBy(groupByColumn, "it")
.Select("new (Key, it as Items)"); // Reshape to { Key: "Value", Items: [...] } .Select("new (Key, it as Items)");
return query;
}
/// <summary>
/// Applies list filters to the given IQueryable.
/// </summary>
/// <typeparam name="T">The type of the elements in the IQueryable.</typeparam>
/// <param name="query">The IQueryable to apply the filters to.</param>
/// <param name="filters">The list of filters to apply.</param>
/// <returns>The filtered IQueryable.</returns>
public static IQueryable<T> ApplyListFilters<T>(this IQueryable<T> query, List<ListDynamicFilter> filters)
{
// Check if there are any filters
if (filters == null || !filters.Any()) return query;
// Apply filters to the query
foreach (var filter in filters)
{
// Skip if column is empty or values are null or empty
if (string.IsNullOrWhiteSpace(filter.Column) || filter.Values == null || !filter.Values.Any()) continue;
// Apply filter to the query
query = query.Where($"@0.Contains({filter.Column})", filter.Values);
}
// Return the filtered query
return query;
}
/// <summary>
/// Applies date filters to the given IQueryable.
/// </summary>
/// <typeparam name="T">The type of the elements in the IQueryable.</typeparam>
/// <param name="query">The IQueryable to apply the filters to.</param>
/// <param name="dateFilter">The date filter to apply.</param>
/// <returns>The filtered IQueryable.</returns>
public static IQueryable<T> ApplyDateFilter<T>(this IQueryable<T> query, DateDynamicFilter dateFilter)
{
// Check if date filter is null or column is empty
if (dateFilter == null || string.IsNullOrWhiteSpace(dateFilter.Column)) return query;
// Convert start and end values to date
var startValue = dateFilter.StartValue.Date;
var endValue = dateFilter.EndValue.Date.AddDays(1);
// Apply a filter to include items with a date greater than or equal to the start value
query = query.Where($"{dateFilter.Column} >= @0", startValue);
// Apply a filter to include items with a date less than or equal to the end value
query = query.Where($"{dateFilter.Column} < @0", endValue);
// Return the filtered IQueryable
return query; return query;
} }
} }

View File

@ -116,7 +116,7 @@ namespace Marco.Pms.Services.Helpers
using var context = _dbContextFactory.CreateDbContext(); using var context = _dbContextFactory.CreateDbContext();
return await context.ProjectAllocations return await context.ProjectAllocations
.AsNoTracking() .AsNoTracking()
.CountAsync(pa => pa.ProjectId == project.Id && pa.IsActive); // Server-side count is efficient .CountAsync(pa => pa.ProjectId == project.Id && pa.TenantId == project.TenantId && pa.IsActive); // Server-side count is efficient
}); });
// This task fetches the entire infrastructure hierarchy and performs aggregations in the database. // This task fetches the entire infrastructure hierarchy and performs aggregations in the database.
@ -127,26 +127,26 @@ namespace Marco.Pms.Services.Helpers
// 1. Fetch all hierarchical data using projections. // 1. Fetch all hierarchical data using projections.
// This is still a chain, but it's inside one task and much faster due to projections. // This is still a chain, but it's inside one task and much faster due to projections.
var buildings = await context.Buildings.AsNoTracking() var buildings = await context.Buildings.AsNoTracking()
.Where(b => b.ProjectId == project.Id) .Where(b => b.ProjectId == project.Id && b.TenantId == project.TenantId)
.Select(b => new { b.Id, b.ProjectId, b.Name, b.Description }) .Select(b => new { b.Id, b.ProjectId, b.Name, b.Description })
.ToListAsync(); .ToListAsync();
var buildingIds = buildings.Select(b => b.Id).ToList(); var buildingIds = buildings.Select(b => b.Id).ToList();
var floors = await context.Floor.AsNoTracking() var floors = await context.Floor.AsNoTracking()
.Where(f => buildingIds.Contains(f.BuildingId)) .Where(f => buildingIds.Contains(f.BuildingId) && f.TenantId == project.TenantId)
.Select(f => new { f.Id, f.BuildingId, f.FloorName }) .Select(f => new { f.Id, f.BuildingId, f.FloorName })
.ToListAsync(); .ToListAsync();
var floorIds = floors.Select(f => f.Id).ToList(); var floorIds = floors.Select(f => f.Id).ToList();
var workAreas = await context.WorkAreas.AsNoTracking() var workAreas = await context.WorkAreas.AsNoTracking()
.Where(wa => floorIds.Contains(wa.FloorId)) .Where(wa => floorIds.Contains(wa.FloorId) && wa.TenantId == project.TenantId)
.Select(wa => new { wa.Id, wa.FloorId, wa.AreaName }) .Select(wa => new { wa.Id, wa.FloorId, wa.AreaName })
.ToListAsync(); .ToListAsync();
var workAreaIds = workAreas.Select(wa => wa.Id).ToList(); var workAreaIds = workAreas.Select(wa => wa.Id).ToList();
// 2. THE KEY OPTIMIZATION: Aggregate work items in the database. // 2. THE KEY OPTIMIZATION: Aggregate work items in the database.
var workSummaries = await context.WorkItems.AsNoTracking() var workSummaries = await context.WorkItems.AsNoTracking()
.Where(wi => workAreaIds.Contains(wi.WorkAreaId)) .Where(wi => workAreaIds.Contains(wi.WorkAreaId) && wi.TenantId == project.TenantId)
.GroupBy(wi => wi.WorkAreaId) // Group by parent on the DB server .GroupBy(wi => wi.WorkAreaId) // Group by parent on the DB server
.Select(g => new // Let the DB do the SUM .Select(g => new // Let the DB do the SUM
{ {
@ -281,6 +281,7 @@ namespace Marco.Pms.Services.Helpers
var projectStatusIds = projects.Select(p => p.ProjectStatusId).Distinct().ToList(); var projectStatusIds = projects.Select(p => p.ProjectStatusId).Distinct().ToList();
var promotorIds = projects.Select(p => p.PromoterId).Distinct().ToList(); var promotorIds = projects.Select(p => p.PromoterId).Distinct().ToList();
var pmcsIds = projects.Select(p => p.PMCId).Distinct().ToList(); var pmcsIds = projects.Select(p => p.PMCId).Distinct().ToList();
var tenantIds = projects.Select(p => p.TenantId).Distinct().ToList();
// --- Step 1: Fetch all required data in maximum parallel --- // --- Step 1: Fetch all required data in maximum parallel ---
// Each task uses its own DbContext and selects only the required columns (projection). // Each task uses its own DbContext and selects only the required columns (projection).
@ -320,7 +321,7 @@ namespace Marco.Pms.Services.Helpers
// Server-side aggregation and projection into a dictionary // Server-side aggregation and projection into a dictionary
return await context.ProjectAllocations return await context.ProjectAllocations
.AsNoTracking() .AsNoTracking()
.Where(pa => projectIds.Contains(pa.ProjectId) && pa.IsActive) .Where(pa => projectIds.Contains(pa.ProjectId) && tenantIds.Contains(pa.TenantId) && pa.IsActive)
.GroupBy(pa => pa.ProjectId) .GroupBy(pa => pa.ProjectId)
.Select(g => new { ProjectId = g.Key, Count = g.Count() }) .Select(g => new { ProjectId = g.Key, Count = g.Count() })
.ToDictionaryAsync(x => x.ProjectId, x => x.Count); .ToDictionaryAsync(x => x.ProjectId, x => x.Count);
@ -331,7 +332,7 @@ namespace Marco.Pms.Services.Helpers
using var context = _dbContextFactory.CreateDbContext(); using var context = _dbContextFactory.CreateDbContext();
return await context.Buildings return await context.Buildings
.AsNoTracking() .AsNoTracking()
.Where(b => projectIds.Contains(b.ProjectId)) .Where(b => projectIds.Contains(b.ProjectId) && tenantIds.Contains(b.TenantId))
.Select(b => new { b.Id, b.ProjectId, b.Name, b.Description }) // Projection .Select(b => new { b.Id, b.ProjectId, b.Name, b.Description }) // Projection
.ToListAsync(); .ToListAsync();
}); });
@ -345,7 +346,7 @@ namespace Marco.Pms.Services.Helpers
using var context = _dbContextFactory.CreateDbContext(); using var context = _dbContextFactory.CreateDbContext();
return await context.Floor return await context.Floor
.AsNoTracking() .AsNoTracking()
.Where(f => buildingIds.Contains(f.BuildingId)) .Where(f => buildingIds.Contains(f.BuildingId) && tenantIds.Contains(f.TenantId))
.Select(f => new { f.Id, f.BuildingId, f.FloorName }) // Projection .Select(f => new { f.Id, f.BuildingId, f.FloorName }) // Projection
.ToListAsync(); .ToListAsync();
}); });
@ -359,7 +360,7 @@ namespace Marco.Pms.Services.Helpers
using var context = _dbContextFactory.CreateDbContext(); using var context = _dbContextFactory.CreateDbContext();
return await context.WorkAreas return await context.WorkAreas
.AsNoTracking() .AsNoTracking()
.Where(wa => floorIds.Contains(wa.FloorId)) .Where(wa => floorIds.Contains(wa.FloorId) && tenantIds.Contains(wa.TenantId))
.Select(wa => new { wa.Id, wa.FloorId, wa.AreaName }) // Projection .Select(wa => new { wa.Id, wa.FloorId, wa.AreaName }) // Projection
.ToListAsync(); .ToListAsync();
}); });
@ -376,7 +377,7 @@ namespace Marco.Pms.Services.Helpers
// Let the DB do the SUM. This is much faster and transfers less data. // Let the DB do the SUM. This is much faster and transfers less data.
return await context.WorkItems return await context.WorkItems
.AsNoTracking() .AsNoTracking()
.Where(wi => workAreaIds.Contains(wi.WorkAreaId)) .Where(wi => workAreaIds.Contains(wi.WorkAreaId) && tenantIds.Contains(wi.TenantId))
.GroupBy(wi => wi.WorkAreaId) .GroupBy(wi => wi.WorkAreaId)
.Select(g => new .Select(g => new
{ {
@ -808,7 +809,7 @@ namespace Marco.Pms.Services.Helpers
} }
Task<List<string>> getPermissionIdsTask = Task.Run(async () => Task<List<string>> getPermissionIdsTask = Task.Run(async () =>
{ {
using var context = _dbContextFactory.CreateDbContext(); await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.RolePermissionMappings return await context.RolePermissionMappings
.Where(rp => roleIds.Contains(rp.ApplicationRoleId)) .Where(rp => roleIds.Contains(rp.ApplicationRoleId))

View File

@ -1,4 +1,5 @@
using Marco.Pms.DataAccess.Data; using Marco.Pms.DataAccess.Data;
using Marco.Pms.Helpers.Utility;
using Marco.Pms.Model.Dtos.Attendance; using Marco.Pms.Model.Dtos.Attendance;
using Marco.Pms.Model.Employees; using Marco.Pms.Model.Employees;
using Marco.Pms.Model.Mail; using Marco.Pms.Model.Mail;
@ -17,12 +18,14 @@ namespace Marco.Pms.Services.Helpers
private readonly IEmailSender _emailSender; private readonly IEmailSender _emailSender;
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly CacheUpdateHelper _cache; private readonly CacheUpdateHelper _cache;
public ReportHelper(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, CacheUpdateHelper cache) private readonly MailLogHelper _mailLogger;
public ReportHelper(ApplicationDbContext context, IEmailSender emailSender, ILoggingService logger, CacheUpdateHelper cache, MailLogHelper mailLogger)
{ {
_context = context; _context = context;
_emailSender = emailSender; _emailSender = emailSender;
_logger = logger; _logger = logger;
_cache = cache; _cache = cache;
_mailLogger = mailLogger;
} }
public async Task<ProjectStatisticReport?> GetDailyProjectReportWithOutTenant(Guid projectId, DateTime reportDate) public async Task<ProjectStatisticReport?> GetDailyProjectReportWithOutTenant(Guid projectId, DateTime reportDate)
@ -599,7 +602,8 @@ namespace Marco.Pms.Services.Helpers
TenantId = tenantId TenantId = tenantId
}).ToList(); }).ToList();
_context.MailLogs.AddRange(mailLogs); //_context.MailLogs.AddRange(mailLogs);
await _mailLogger.AddWebMenuItemAsync(mailLogs);
try try
{ {

View File

@ -14,6 +14,7 @@ using Marco.Pms.Model.Dtos.Expenses.Masters;
using Marco.Pms.Model.Dtos.Master; using Marco.Pms.Model.Dtos.Master;
using Marco.Pms.Model.Dtos.Organization; using Marco.Pms.Model.Dtos.Organization;
using Marco.Pms.Model.Dtos.Project; using Marco.Pms.Model.Dtos.Project;
using Marco.Pms.Model.Dtos.PurchaseInvoice;
using Marco.Pms.Model.Dtos.ServiceProject; using Marco.Pms.Model.Dtos.ServiceProject;
using Marco.Pms.Model.Dtos.Tenant; using Marco.Pms.Model.Dtos.Tenant;
using Marco.Pms.Model.Employees; using Marco.Pms.Model.Employees;
@ -28,10 +29,12 @@ using Marco.Pms.Model.MongoDBModels.Masters;
using Marco.Pms.Model.MongoDBModels.Project; using Marco.Pms.Model.MongoDBModels.Project;
using Marco.Pms.Model.OrganizationModel; using Marco.Pms.Model.OrganizationModel;
using Marco.Pms.Model.Projects; using Marco.Pms.Model.Projects;
using Marco.Pms.Model.PurchaseInvoice;
using Marco.Pms.Model.ServiceProject; using Marco.Pms.Model.ServiceProject;
using Marco.Pms.Model.TenantModels; using Marco.Pms.Model.TenantModels;
using Marco.Pms.Model.TenantModels.MongoDBModel; using Marco.Pms.Model.TenantModels.MongoDBModel;
using Marco.Pms.Model.ViewModels.Activities; using Marco.Pms.Model.ViewModels.Activities;
using Marco.Pms.Model.ViewModels.AppMenu;
using Marco.Pms.Model.ViewModels.Collection; using Marco.Pms.Model.ViewModels.Collection;
using Marco.Pms.Model.ViewModels.Directory; using Marco.Pms.Model.ViewModels.Directory;
using Marco.Pms.Model.ViewModels.DocumentManager; using Marco.Pms.Model.ViewModels.DocumentManager;
@ -42,6 +45,7 @@ using Marco.Pms.Model.ViewModels.Expenses.Masters;
using Marco.Pms.Model.ViewModels.Master; using Marco.Pms.Model.ViewModels.Master;
using Marco.Pms.Model.ViewModels.Organization; using Marco.Pms.Model.ViewModels.Organization;
using Marco.Pms.Model.ViewModels.Projects; using Marco.Pms.Model.ViewModels.Projects;
using Marco.Pms.Model.ViewModels.PurchaseInvoice;
using Marco.Pms.Model.ViewModels.ServiceProject; using Marco.Pms.Model.ViewModels.ServiceProject;
using Marco.Pms.Model.ViewModels.Tenant; using Marco.Pms.Model.ViewModels.Tenant;
@ -560,26 +564,25 @@ namespace Marco.Pms.Services.MappingProfiles
#endregion #endregion
#region ======================================================= AppMenu ======================================================= #region ======================================================= AppMenu =======================================================
CreateMap<CreateMenuSectionDto, MenuSection>(); CreateMap<CreateWebMenuSectionDto, WebMenuSection>();
CreateMap<UpdateMenuSectionDto, MenuSection>();
CreateMap<MenuSection, MenuSectionVM>()
.ForMember(
dest => dest.Name,
opt => opt.MapFrom(src => src.Title));
CreateMap<CreateMenuItemDto, MenuItem>(); CreateMap<WebMenuSection, WebMenuSectionVM>()
CreateMap<UpdateMenuItemDto, MenuItem>();
CreateMap<MenuItem, MenuItemVM>()
.ForMember( .ForMember(
dest => dest.Name, dest => dest.Items,
opt => opt.MapFrom(src => src.Text)); opt => opt.MapFrom(src => new List<WebSideMenuItem>()));
CreateMap<CreateSubMenuItemDto, SubMenuItem>();
CreateMap<UpdateSubMenuItemDto, SubMenuItem>(); CreateMap<CreateWebSideMenuItemDto, WebSideMenuItem>()
CreateMap<SubMenuItem, SubMenuItemVM>()
.ForMember( .ForMember(
dest => dest.Name, dest => dest.Id,
opt => opt.MapFrom(src => src.Text)); opt => opt.MapFrom(src => src.Id.HasValue ? src.Id.Value : Guid.NewGuid()));
CreateMap<WebSideMenuItem, WebSideMenuItemVM>();
CreateMap<CreateMobileSideMenuItemDto, MobileMenu>();
CreateMap<MobileMenu, MenuSectionApplicationVM>();
#endregion #endregion
#region ======================================================= Directory ======================================================= #region ======================================================= Directory =======================================================
@ -618,6 +621,58 @@ namespace Marco.Pms.Services.MappingProfiles
CreateMap<UpdateContactNoteDto, ContactNote>(); CreateMap<UpdateContactNoteDto, ContactNote>();
#endregion #endregion
#region ======================================================= Purchase Invoice =======================================================
CreateMap<PurchaseInvoiceDto, PurchaseInvoiceDetails>()
.ForMember(
dest => dest.PaymentDueDate,
opt => opt.MapFrom(src => src.PaymentDueDate.HasValue ? src.PaymentDueDate : DateTime.UtcNow.AddDays(40)));
CreateMap<PurchaseInvoiceDetails, PurchaseInvoiceDto>();
CreateMap<PurchaseInvoiceDetails, PurchaseInvoiceListVM>()
.ForMember(
dest => dest.PurchaseInvoiceUId,
opt => opt.MapFrom(src => $"{src.UIDPrefix}/{src.UIDPostfix:D5}"));
CreateMap<PurchaseInvoiceDetails, BasicPurchaseInvoiceVM>()
.ForMember(
dest => dest.PurchaseInvoiceUId,
opt => opt.MapFrom(src => $"{src.UIDPrefix}/{src.UIDPostfix:D5}"));
CreateMap<PurchaseInvoiceDetails, PurchaseInvoiceDetailsVM>()
.ForMember(
dest => dest.PurchaseInvoiceUId,
opt => opt.MapFrom(src => $"{src.UIDPrefix}/{src.UIDPostfix:D5}"));
CreateMap<PurchaseInvoiceAttachment, PurchaseInvoiceAttachmentVM>()
.ForMember(
dest => dest.DocumentId,
opt => opt.MapFrom(src => src.DocumentId))
.ForMember(
dest => dest.FileName,
opt => opt.MapFrom(src => src.Document != null ? src.Document.FileName : null))
.ForMember(
dest => dest.ContentType,
opt => opt.MapFrom(src => src.Document != null ? src.Document.ContentType : null))
.ForMember(
dest => dest.FileSize,
opt => opt.MapFrom(src => src.Document != null ? src.Document.FileSize : 0))
.ForMember(
dest => dest.UploadedAt,
opt => opt.MapFrom(src => src.Document != null ? src.Document.UploadedAt : DateTime.UtcNow));
CreateMap<DeliveryChallanDto, DeliveryChallanDetails>()
.ForMember(
dest => dest.Attachment,
opt => opt.Ignore());
CreateMap<DeliveryChallanDetails, DeliveryChallanVM>();
CreateMap<ReceivedInvoicePaymentDto, PurchaseInvoicePayment>();
CreateMap<PurchaseInvoicePayment, ReceivedInvoicePaymentVM>();
#endregion
} }
} }
} }

View File

@ -192,6 +192,7 @@ builder.Services.AddScoped<IAesEncryption, AesEncryption>();
builder.Services.AddScoped<IOrganizationService, OrganizationService>(); builder.Services.AddScoped<IOrganizationService, OrganizationService>();
builder.Services.AddScoped<ITenantService, TenantService>(); builder.Services.AddScoped<ITenantService, TenantService>();
builder.Services.AddScoped<IServiceProject, ServiceProjectService>(); builder.Services.AddScoped<IServiceProject, ServiceProjectService>();
builder.Services.AddScoped<IPurchaseInvoiceService, PurchaseInvoiceService>();
#endregion #endregion
#region Helpers #region Helpers
@ -211,6 +212,7 @@ builder.Services.AddScoped<EmployeeCache>();
builder.Services.AddScoped<ReportCache>(); builder.Services.AddScoped<ReportCache>();
builder.Services.AddScoped<ExpenseCache>(); builder.Services.AddScoped<ExpenseCache>();
builder.Services.AddScoped<SidebarMenuHelper>(); builder.Services.AddScoped<SidebarMenuHelper>();
builder.Services.AddScoped<MailLogHelper>();
#endregion #endregion
// Singleton services (one instance for the app's lifetime) // Singleton services (one instance for the app's lifetime)

View File

@ -3644,7 +3644,6 @@ namespace Marco.Pms.Services.Service
{ {
// Fetch advance payment transactions for specified employee, including related navigation properties // Fetch advance payment transactions for specified employee, including related navigation properties
var transactions = await _context.AdvancePaymentTransactions var transactions = await _context.AdvancePaymentTransactions
.Include(apt => apt.Project)
.Include(apt => apt.Employee).ThenInclude(e => e!.JobRole) .Include(apt => apt.Employee).ThenInclude(e => e!.JobRole)
.Include(apt => apt.CreatedBy).ThenInclude(e => e!.JobRole) .Include(apt => apt.CreatedBy).ThenInclude(e => e!.JobRole)
.Where(apt => apt.EmployeeId == employeeId && apt.TenantId == tenantId && apt.IsActive) .Where(apt => apt.EmployeeId == employeeId && apt.TenantId == tenantId && apt.IsActive)
@ -3658,11 +3657,31 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.ErrorResponse("No advance payment transactions found.", null, 404); return ApiResponse<object>.ErrorResponse("No advance payment transactions found.", null, 404);
} }
var projectIds = transactions.Select(pr => pr.ProjectId).ToList();
var infraProjectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.Projects.Where(p => projectIds.Contains(p.Id) && p.TenantId == tenantId).Select(p => _mapper.Map<BasicProjectVM>(p)).ToListAsync();
});
var serviceProjectTask = Task.Run(async () =>
{
await using var context = await _dbContextFactory.CreateDbContextAsync();
return await context.ServiceProjects.Where(sp => projectIds.Contains(sp.Id) && sp.TenantId == tenantId).Select(sp => _mapper.Map<BasicProjectVM>(sp)).ToListAsync();
});
await Task.WhenAll(infraProjectTask, serviceProjectTask);
var projects = infraProjectTask.Result;
projects.AddRange(serviceProjectTask.Result);
// Map transactions to view model with formatted FinanceUId // Map transactions to view model with formatted FinanceUId
var response = transactions.Select(transaction => var response = transactions.Select(transaction =>
{ {
var result = _mapper.Map<AdvancePaymentTransactionVM>(transaction); var result = _mapper.Map<AdvancePaymentTransactionVM>(transaction);
result.FinanceUId = $"{transaction.FinanceUIdPrefix}/{transaction.FinanceUIdPostfix:D5}"; result.FinanceUId = $"{transaction.FinanceUIdPrefix}/{transaction.FinanceUIdPostfix:D5}";
result.Project = projects.Where(p => p.Id == transaction.ProjectId).Select(p => _mapper.Map<BasicProjectVM>(p)).FirstOrDefault();
return result; return result;
}).ToList(); }).ToList();

View File

@ -187,6 +187,118 @@ namespace Marco.Pms.Services.Service
} }
#endregion #endregion
#region =================================================================== Purchase Invoice Status APIs ===================================================================
/// <summary>
/// Asynchronously retrieves a list of all available Purchase Invoice Statuses.
/// </summary>
/// <param name="loggedInEmployee">The employee context (used for logging or future permission checks).</param>
/// <param name="cancellationToken">Token to observe while waiting for the task to complete.</param>
/// <returns>A unified API response containing the list of status DTOs.</returns>
public async Task<ApiResponse<object>> GetPurchaseInvoiceStatusAsync(Employee loggedInEmployee, CancellationToken cancellationToken = default)
{
// 1. Structural Logging: Capture the context of who is performing the action
_logger.LogInfo("Initiating fetch of Purchase Invoice Statuses for User ID: {UserId}", loggedInEmployee.Id);
try
{
// 2. Performance & Security: Use AsNoTracking, Select specifically (Projection), and handle CancellationToken
var invoiceStatuses = await _context.PurchaseInvoiceStatus
.AsNoTracking() // Critical for read-only queries to avoid overhead of change tracker
.OrderBy(status => status.Name)
.ToListAsync(cancellationToken); // Respect request cancellation
// 3. Logging Success: Log the count for monitoring purposes
_logger.LogInfo("Successfully fetched {Count} Purchase Invoice Status records.", invoiceStatuses.Count);
// 4. Strongly Typed Return: Return specific DTOs, not 'object'
return ApiResponse<object>.SuccessResponse(
invoiceStatuses,
$"{invoiceStatuses.Count} record(s) of Purchase invoice status fetched successfully",
200
);
}
catch (OperationCanceledException)
{
// Handle cases where the user cancels the request (browser closed/timeout)
_logger.LogWarning("Purchase Invoice Status fetch was cancelled by the user.");
return ApiResponse<object>.ErrorResponse(
"Request was cancelled",
"Operation cancelled.",
499 // Client Closed Request
);
}
catch (Exception ex)
{
// 5. Security & Error Handling: Log full stack trace securely, return generic message to client
_logger.LogError(ex, "Critical error occurred while fetching Purchase Invoice Statuses for User ID: {UserId}", loggedInEmployee.Id);
// NEVER return 'ex.Message' directly to the client in production (security risk)
return ApiResponse<object>.ErrorResponse(
"An internal error occurred while processing your request. Please contact support.",
"Internal Server Error",
500
);
}
}
#endregion
#region =================================================================== Invoice Attachment Type APIs ===================================================================
/// <summary>
/// Asynchronously retrieves the list of valid Invoice Attachment Types.
/// </summary>
/// <param name="loggedInEmployee">The current user context for audit logging.</param>
/// <param name="cancellationToken">Token to propagate notification that operations should be canceled.</param>
/// <returns>A standardized API response containing a list of attachment type DTOs.</returns>
public async Task<ApiResponse<object>> GetInvoiceAttachmentTypeAsync(Employee loggedInEmployee, CancellationToken cancellationToken = default)
{
// 1. Structured Logging: Audit WHO is requesting the data
_logger.LogInfo("Initiating fetch of Invoice Attachment Types. Requested by User ID: {UserId}", loggedInEmployee.Id);
try
{
// 2. Database Optimization:
// - AsNoTracking(): Crucial for read-only lists. Bypasses change tracking overhead.
// - Select(): Projects directly to DTO. Fetches ONLY needed columns (SQL optimization).
var attachmentTypes = await _context.InvoiceAttachmentTypes
.AsNoTracking()
.OrderBy(type => type.Name)
.ToListAsync(cancellationToken); // 3. Pass the token to EF Core
_logger.LogInfo("Successfully retrieved {Count} Invoice Attachment Types.", attachmentTypes.Count);
// 4. Strong Typing: Return IEnumerable<Dto> instead of 'object'
return ApiResponse<object>.SuccessResponse(
attachmentTypes,
$"{attachmentTypes.Count} record(s) of invoice attachment type fetched successfully",
200
);
}
catch (OperationCanceledException)
{
// Handle request cancellation (e.g., user navigated away)
_logger.LogWarning("Invoice Attachment Type fetch operation was canceled by the client.");
return ApiResponse<object>.ErrorResponse("Operation canceled", "Request canceled by user", 499);
}
catch (Exception ex)
{
// 5. Security & Error Handling:
// Log the real error with stack trace internally.
_logger.LogError(ex, "Critical error fetching Invoice Attachment Types for User ID: {UserId}", loggedInEmployee.Id);
// Return a sanitized message to the client. Never expose raw SQL errors or Stack Traces.
return ApiResponse<object>.ErrorResponse(
"An unexpected error occurred while processing your request.",
"Internal Server Error",
500
);
}
}
#endregion
#region =================================================================== Currency APIs =================================================================== #region =================================================================== Currency APIs ===================================================================
public async Task<ApiResponse<object>> GetCurrencyAsync(Employee loggedInEmployee, Guid tenantId) public async Task<ApiResponse<object>> GetCurrencyAsync(Employee loggedInEmployee, Guid tenantId)

View File

@ -1,4 +1,5 @@
using AutoMapper; using AutoMapper;
using AutoMapper.QueryableExtensions;
using Marco.Pms.DataAccess.Data; using Marco.Pms.DataAccess.Data;
using Marco.Pms.Model.Dtos.Organization; using Marco.Pms.Model.Dtos.Organization;
using Marco.Pms.Model.Employees; using Marco.Pms.Model.Employees;
@ -13,6 +14,7 @@ using Marco.Pms.Services.Helpers;
using Marco.Pms.Services.Service.ServiceInterfaces; using Marco.Pms.Services.Service.ServiceInterfaces;
using MarcoBMS.Services.Service; using MarcoBMS.Services.Service;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using System.Linq.Dynamic.Core;
namespace Marco.Pms.Services.Service namespace Marco.Pms.Services.Service
{ {
@ -150,6 +152,92 @@ namespace Marco.Pms.Services.Service
return ApiResponse<object>.SuccessResponse(response, "Successfully fetched the organization list", 200); return ApiResponse<object>.SuccessResponse(response, "Successfully fetched the organization list", 200);
} }
/// <summary>
/// Retrieves a paginated, searchable list of organizations.
/// Optimized for performance using DB Projections and AsNoTracking.
/// </summary>
/// <param name="searchString">Optional keyword to filter organizations by name.</param>
/// <param name="pageNumber">The requested page number (1-based).</param>
/// <param name="pageSize">The number of records per page (Max 50).</param>
/// <param name="loggedInEmployee">The current user context for security filtering.</param>
/// <param name="ct">Cancellation token to cancel operations if the client disconnects.</param>
/// <returns>A paginated list of BasicOrganizationVm.</returns>
public async Task<ApiResponse<object>> GetOrganizationBasicListAsync(Guid? id, string? searchString, int pageNumber, int pageSize, Employee loggedInEmployee, CancellationToken ct = default)
{
try
{
// 1. VALIDATION
_logger.LogInfo("Fetching Organization list. Page: {Page}, Size: {Size}, Search: {Search}, User: {UserId}",
pageNumber, pageSize, searchString ?? "<empty>", loggedInEmployee.Id);
// 2. QUERY BUILDING
// Use AsNoTracking() for read-only scenarios to reduce overhead.
var query = _context.Organizations.AsNoTracking()
.Where(o => o.IsActive);
// 3. SECURITY FILTER (Multi-Tenancy)
// Enterprise Rule: Always filter by the logged-in user's Tenant/Permissions.
// Assuming loggedInEmployee has a TenantId or OrganizationId
// query = query.Where(o => o.TenantId == loggedInEmployee.TenantId);
// 4. DYNAMIC FILTERING
if (!string.IsNullOrWhiteSpace(searchString))
{
var searchTrimmed = searchString.Trim();
query = query.Where(o => o.Name.Contains(searchTrimmed));
}
if (id.HasValue)
{
query = query.Where(o => o.Id == id.Value);
}
// 5. COUNT TOTALS (Efficiently)
// Count the total records matching the filter BEFORE applying pagination
var totalCount = await query.CountAsync(ct);
// 6. FETCH DATA (With Projection)
// CRITICAL OPTIMIZATION: Use .ProjectTo or .Select BEFORE .ToListAsync.
// This ensures SQL only fetches the columns needed for BasicOrganizationVm,
// rather than fetching the whole Entity and discarding data in memory.
var items = await query
.OrderBy(o => o.Name)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ProjectTo<BasicOrganizationVm>(_mapper.ConfigurationProvider) // Requires AutoMapper.QueryableExtensions
.ToListAsync(ct);
// 7. PREPARE RESPONSE
var totalPages = (int)Math.Ceiling((double)totalCount / pageSize);
var pagedResult = new
{
CurrentPage = pageNumber,
PageSize = pageSize,
TotalPages = totalPages,
TotalCount = totalCount,
HasPrevious = pageNumber > 1,
HasNext = pageNumber < totalPages,
Data = items
};
_logger.LogInfo("Successfully fetched {Count} organizations out of {Total}.", items.Count, totalCount);
return ApiResponse<object>.SuccessResponse(pagedResult, "Organization list fetched successfully.", 200);
}
catch (OperationCanceledException)
{
// Handle client disconnection gracefully
_logger.LogWarning("Organization list fetch was cancelled by the client.");
return ApiResponse<object>.ErrorResponse("Request Cancelled", "The operation was cancelled.", 499);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fetch organization list. User: {UserId}, Error: {Message}", loggedInEmployee.Id, ex.Message);
return ApiResponse<object>.ErrorResponse("Data Fetch Failed", "An unexpected error occurred while retrieving the organization list.", 500);
}
}
public async Task<ApiResponse<object>> GetOrganizationDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId) public async Task<ApiResponse<object>> GetOrganizationDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId)
{ {
_logger.LogDebug("Started fetching details for OrganizationId: {OrganizationId}", id); _logger.LogDebug("Started fetching details for OrganizationId: {OrganizationId}", id);

View File

@ -4,6 +4,7 @@ using Marco.Pms.Model.Entitlements;
using Marco.Pms.Services.Helpers; using Marco.Pms.Services.Helpers;
using MarcoBMS.Services.Helpers; using MarcoBMS.Services.Helpers;
using MarcoBMS.Services.Service; using MarcoBMS.Services.Service;
using Microsoft.CodeAnalysis;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Marco.Pms.Services.Service namespace Marco.Pms.Services.Service
@ -11,15 +12,13 @@ namespace Marco.Pms.Services.Service
public class PermissionServices public class PermissionServices
{ {
private readonly ApplicationDbContext _context; private readonly ApplicationDbContext _context;
private readonly RolesHelper _rolesHelper;
private readonly CacheUpdateHelper _cache; private readonly CacheUpdateHelper _cache;
private readonly ILoggingService _logger; private readonly ILoggingService _logger;
private readonly Guid tenantId; private readonly Guid tenantId;
public PermissionServices(ApplicationDbContext context, RolesHelper rolesHelper, CacheUpdateHelper cache, ILoggingService logger, UserHelper userHelper) public PermissionServices(ApplicationDbContext context, CacheUpdateHelper cache, ILoggingService logger, UserHelper userHelper)
{ {
_context = context; _context = context;
_rolesHelper = rolesHelper;
_cache = cache; _cache = cache;
_logger = logger; _logger = logger;
tenantId = userHelper.GetTenantId(); tenantId = userHelper.GetTenantId();
@ -34,72 +33,23 @@ namespace Marco.Pms.Services.Service
/// <returns>True if the user has the permission, otherwise false.</returns> /// <returns>True if the user has the permission, otherwise false.</returns>
public async Task<bool> HasPermission(Guid featurePermissionId, Guid employeeId, Guid? projectId = null) public async Task<bool> HasPermission(Guid featurePermissionId, Guid employeeId, Guid? projectId = null)
{ {
// 1. Try fetching permissions from cache (fast-path lookup). var featurePermissionIds = await GetPermissionIdsByEmployeeId(employeeId, projectId);
var featurePermissionIds = await _cache.GetPermissions(employeeId, tenantId);
// If not found in cache, fallback to database (slower).
if (featurePermissionIds == null)
{
var featurePermissions = await _rolesHelper.GetFeaturePermissionByEmployeeId(employeeId, tenantId);
featurePermissionIds = featurePermissions.Select(fp => fp.Id).ToList();
}
// 2. Handle project-level permission overrides if a project is specified.
if (projectId.HasValue)
{
// Fetch permissions explicitly assigned to this employee in the project.
var projectLevelPermissionIds = await _context.ProjectLevelPermissionMappings
.AsNoTracking()
.Where(pl => pl.ProjectId == projectId.Value && pl.EmployeeId == employeeId)
.Select(pl => pl.PermissionId)
.ToListAsync();
if (projectLevelPermissionIds?.Any() ?? false)
{
// Define modules where project-level overrides apply.
var projectLevelModuleIds = new HashSet<Guid>
{
Guid.Parse("53176ebf-c75d-42e5-839f-4508ffac3def"),
Guid.Parse("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"),
Guid.Parse("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"),
Guid.Parse("a8cf4331-8f04-4961-8360-a3f7c3cc7462")
};
// Get all feature permissions under those modules where the user didn't have explicit project-level grants.
var allOverriddenPermissions = await _context.FeaturePermissions
.AsNoTracking()
.Where(fp => projectLevelModuleIds.Contains(fp.FeatureId) &&
!projectLevelPermissionIds.Contains(fp.Id))
.Select(fp => fp.Id)
.ToListAsync();
// Apply overrides:
// - Remove global permissions overridden by project-level rules.
// - Add explicit project-level permissions.
featurePermissionIds = featurePermissionIds
.Except(allOverriddenPermissions) // Remove overridden
.Concat(projectLevelPermissionIds) // Add project-level
.Distinct() // Ensure no duplicates
.ToList();
}
}
// 3. Final check: does the employee have the requested permission?
return featurePermissionIds.Contains(featurePermissionId); return featurePermissionIds.Contains(featurePermissionId);
} }
public async Task<bool> HasPermissionAny(List<Guid> featurePermissionIds, Guid employeeId) public async Task<bool> HasPermissionAny(List<Guid> featurePermissionIds, Guid employeeId)
{ {
var allFeaturePermissionIds = await _cache.GetPermissions(employeeId, tenantId); var allFeaturePermissionIds = await GetPermissionIdsByEmployeeId(employeeId);
if (allFeaturePermissionIds == null)
{
List<FeaturePermission> featurePermission = await _rolesHelper.GetFeaturePermissionByEmployeeId(employeeId, tenantId);
allFeaturePermissionIds = featurePermission.Select(fp => fp.Id).ToList();
}
var hasPermission = featurePermissionIds.Any(f => allFeaturePermissionIds.Contains(f)); var hasPermission = featurePermissionIds.Any(f => allFeaturePermissionIds.Contains(f));
return hasPermission; return hasPermission;
} }
public bool HasPermissionAny(List<Guid> realPermissionIds, List<Guid> toCheckPermissionIds, Guid employeeId)
{
var hasPermission = toCheckPermissionIds.Any(f => realPermissionIds.Contains(f));
return hasPermission;
}
public async Task<bool> HasProjectPermission(Employee LoggedInEmployee, Guid projectId) public async Task<bool> HasProjectPermission(Employee LoggedInEmployee, Guid projectId)
{ {
var employeeId = LoggedInEmployee.Id; var employeeId = LoggedInEmployee.Id;
@ -199,5 +149,164 @@ namespace Marco.Pms.Services.Service
return false; return false;
} }
} }
/// <summary>
/// Retrieves permission IDs for an employee, supporting both global role-based permissions
/// and project-specific overrides with cache-first strategy.
/// </summary>
/// <param name="employeeId">The ID of the employee to fetch permissions for.</param>
/// <param name="projectId">Optional project ID for project-level permission overrides.</param>
/// <returns>List of unique permission IDs the employee has access to.</returns>
/// <exception cref="ArgumentException">Thrown when employeeId or tenantId is empty.</exception>
public async Task<List<Guid>> GetPermissionIdsByEmployeeId(Guid employeeId, Guid? projectId = null)
{
// Input validation
if (employeeId == Guid.Empty)
{
_logger.LogWarning("EmployeeId cannot be empty.");
return new List<Guid>();
}
if (tenantId == Guid.Empty)
{
_logger.LogWarning("TenantId cannot be empty.");
return new List<Guid>();
}
_logger.LogDebug(
"GetPermissionIdsByEmployeeId started. EmployeeId: {EmployeeId}, ProjectId: {ProjectId}, TenantId: {TenantId}",
employeeId, projectId ?? Guid.Empty, tenantId);
try
{
// Phase 1: Cache-first lookup for role-based permissions (fast path)
var featurePermissionIds = await _cache.GetPermissions(employeeId, tenantId);
var permissionsFromCache = featurePermissionIds != null;
_logger.LogDebug(
"Permission lookup from cache: {CacheHit}, InitialPermissions: {PermissionCount}, EmployeeId: {EmployeeId}",
permissionsFromCache, featurePermissionIds?.Count ?? 0, employeeId);
// Phase 2: Database fallback if cache miss
if (featurePermissionIds == null)
{
_logger.LogDebug(
"Cache miss detected, falling back to database lookup. EmployeeId: {EmployeeId}, TenantId: {TenantId}",
employeeId, tenantId);
var roleIds = await _context.EmployeeRoleMappings
.Where(erm => erm.EmployeeId == employeeId && erm.TenantId == tenantId)
.Select(erm => erm.RoleId)
.ToListAsync();
if (!roleIds.Any())
{
_logger.LogDebug(
"No roles found for employee. EmployeeId: {EmployeeId}, TenantId: {TenantId}",
employeeId, tenantId);
return new List<Guid>();
}
featurePermissionIds = await _context.RolePermissionMappings
.Where(rpm => roleIds.Contains(rpm.ApplicationRoleId))
.Select(rpm => rpm.FeaturePermissionId)
.Distinct()
.ToListAsync();
// The cache service might also need its own context, or you can pass the data directly.
// Assuming AddApplicationRole takes the data, not a context.
await _cache.AddApplicationRole(employeeId, roleIds, tenantId);
_logger.LogInfo("Successfully queued cache update for EmployeeId: {EmployeeId}", employeeId);
_logger.LogDebug(
"Loaded {RoleCount} roles → {PermissionCount} permissions from database. EmployeeId: {EmployeeId}",
roleIds.Count, featurePermissionIds.Count, employeeId);
}
// Early return for global permissions (no project context)
if (!projectId.HasValue)
{
_logger.LogDebug(
"Returning global permissions. Count: {PermissionCount}, EmployeeId: {EmployeeId}",
featurePermissionIds.Count, employeeId);
return featurePermissionIds;
}
// Phase 3: Apply project-level permission overrides
_logger.LogDebug(
"Applying project-level overrides. ProjectId: {ProjectId}, EmployeeId: {EmployeeId}",
projectId.Value, employeeId);
var projectLevelPermissionIds = await _context.ProjectLevelPermissionMappings
.AsNoTracking()
.Where(pl => pl.ProjectId == projectId.Value &&
pl.EmployeeId == employeeId)
.Select(pl => pl.PermissionId)
.Distinct()
.ToListAsync();
if (!projectLevelPermissionIds.Any())
{
_logger.LogDebug(
"No project-level permissions found. ProjectId: {ProjectId}, EmployeeId: {EmployeeId}",
projectId.Value, employeeId);
return featurePermissionIds;
}
// Phase 4: Override logic for specific project modules
var projectOverrideModules = new HashSet<Guid>
{
// Hard-coded module IDs for project-level override scope
// TODO: Consider moving to configuration or database lookup
Guid.Parse("53176ebf-c75d-42e5-839f-4508ffac3def"), // Module: Projects
Guid.Parse("9d4b5489-2079-40b9-bd77-6e1bf90bc19f"), // Module: Expenses
Guid.Parse("52c9cf54-1eb2-44d2-81bb-524cf29c0a94"), // Module: Invoices
Guid.Parse("a8cf4331-8f04-4961-8360-a3f7c3cc7462") // Module: Documents
};
// Find permissions in override modules that employee lacks at project level
var overriddenPermissions = await _context.FeaturePermissions
.AsNoTracking()
.Where(fp => projectOverrideModules.Contains(fp.FeatureId) &&
!projectLevelPermissionIds.Contains(fp.Id))
.Select(fp => fp.Id)
.ToListAsync();
// Apply override rules:
// 1. Remove global permissions overridden by project context
// 2. Add explicit project-level grants
// 3. Ensure uniqueness
var finalPermissions = featurePermissionIds
.Except(overriddenPermissions) // Remove overridden global perms
.Concat(projectLevelPermissionIds) // Add project-specific grants
.Distinct() // Deduplicate
.ToList();
_logger.LogDebug(
"Project override applied. Before: {BeforeCount}, After: {AfterCount}, Added: {AddedCount}, Removed: {RemovedCount}, EmployeeId: {EmployeeId}",
featurePermissionIds.Count, finalPermissions.Count,
projectLevelPermissionIds.Count, overriddenPermissions.Count, employeeId);
return finalPermissions;
}
catch (OperationCanceledException)
{
_logger.LogWarning("GetPermissionIdsByEmployeeId cancelled. EmployeeId: {EmployeeId}", employeeId);
return new List<Guid>();
}
catch (DbUpdateException ex)
{
_logger.LogError(ex, "Database error in GetPermissionIdsByEmployeeId. EmployeeId: {EmployeeId}, TenantId: {TenantId}", employeeId, tenantId);
return new List<Guid>();
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error in GetPermissionIdsByEmployeeId. EmployeeId: {EmployeeId}, TenantId: {TenantId}", employeeId, tenantId);
return new List<Guid>();
}
}
} }
} }

View File

@ -61,7 +61,7 @@ namespace Marco.Pms.Services.Service
/// <param name="loggedInEmployee">Authenticated employee requesting the data.</param> /// <param name="loggedInEmployee">Authenticated employee requesting the data.</param>
/// <param name="tenantId">Tenant identifier to ensure multi-tenant data isolation.</param> /// <param name="tenantId">Tenant identifier to ensure multi-tenant data isolation.</param>
/// <returns>Returns an ApiResponse containing the distinct combined list of basic project view models or an error response.</returns> /// <returns>Returns an ApiResponse containing the distinct combined list of basic project view models or an error response.</returns>
public async Task<ApiResponse<object>> GetBothProjectBasicListAsync(string? searchString, Employee loggedInEmployee, Guid tenantId) public async Task<ApiResponse<object>> GetBothProjectBasicListAsync(Guid? id, string? searchString, Employee loggedInEmployee, Guid tenantId)
{ {
if (tenantId == Guid.Empty) if (tenantId == Guid.Empty)
{ {
@ -88,6 +88,10 @@ namespace Marco.Pms.Services.Service
.Where(p => p.Name.ToLower().Contains(normalized) || .Where(p => p.Name.ToLower().Contains(normalized) ||
(!string.IsNullOrWhiteSpace(p.ShortName) && p.ShortName.ToLower().Contains(normalized))); (!string.IsNullOrWhiteSpace(p.ShortName) && p.ShortName.ToLower().Contains(normalized)));
} }
if (id.HasValue)
{
infraProjectsQuery = infraProjectsQuery.Where(p => p.Id == id.Value);
}
var infraProjects = await infraProjectsQuery.ToListAsync(); var infraProjects = await infraProjectsQuery.ToListAsync();
return infraProjects.Select(p => _mapper.Map<BasicProjectVM>(p)).ToList(); return infraProjects.Select(p => _mapper.Map<BasicProjectVM>(p)).ToList();
@ -108,6 +112,11 @@ namespace Marco.Pms.Services.Service
(!string.IsNullOrWhiteSpace(sp.ShortName) && sp.ShortName.ToLower().Contains(normalized))); (!string.IsNullOrWhiteSpace(sp.ShortName) && sp.ShortName.ToLower().Contains(normalized)));
} }
if (id.HasValue)
{
serviceProjectsQuery = serviceProjectsQuery.Where(sp => sp.Id == id.Value);
}
var serviceProjects = await serviceProjectsQuery.ToListAsync(); var serviceProjects = await serviceProjectsQuery.ToListAsync();
return serviceProjects.Select(sp => _mapper.Map<BasicProjectVM>(sp)).ToList(); return serviceProjects.Select(sp => _mapper.Map<BasicProjectVM>(sp)).ToList();
}); });

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,14 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
Task<ApiResponse<object>> GetRecurringPaymentStatusAsync(Employee loggedInEmployee, Guid tenantId); Task<ApiResponse<object>> GetRecurringPaymentStatusAsync(Employee loggedInEmployee, Guid tenantId);
#endregion #endregion
#region =================================================================== Purchase Invoice Status APIs ===================================================================
Task<ApiResponse<object>> GetPurchaseInvoiceStatusAsync(Employee loggedInEmployee, CancellationToken cancellationToken);
#endregion
#region =================================================================== Invoice Attachment Type APIs ===================================================================
Task<ApiResponse<object>> GetInvoiceAttachmentTypeAsync(Employee loggedInEmployee, CancellationToken cancellationToken);
#endregion
#region =================================================================== Currency APIs =================================================================== #region =================================================================== Currency APIs ===================================================================
Task<ApiResponse<object>> GetCurrencyAsync(Employee loggedInEmployee, Guid tenantId); Task<ApiResponse<object>> GetCurrencyAsync(Employee loggedInEmployee, Guid tenantId);

View File

@ -8,6 +8,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
{ {
#region =================================================================== Get Functions =================================================================== #region =================================================================== Get Functions ===================================================================
Task<ApiResponse<object>> GetOrganizarionListAsync(string? searchString, long? sprid, bool active, int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId); Task<ApiResponse<object>> GetOrganizarionListAsync(string? searchString, long? sprid, bool active, int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId);
Task<ApiResponse<object>> GetOrganizationBasicListAsync(Guid? id, string? searchString, int pageNumber, int pageSize, Employee loggedInEmployee, CancellationToken ct);
Task<ApiResponse<object>> GetOrganizationDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId); Task<ApiResponse<object>> GetOrganizationDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId);
Task<ApiResponse<object>> GetOrganizationHierarchyListAsync(Guid employeeId, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId); Task<ApiResponse<object>> GetOrganizationHierarchyListAsync(Guid employeeId, Employee loggedInEmployee, Guid tenantId, Guid loggedOrganizationId);
#endregion #endregion

View File

@ -10,7 +10,7 @@ namespace Marco.Pms.Services.Service.ServiceInterfaces
{ {
public interface IProjectServices public interface IProjectServices
{ {
Task<ApiResponse<object>> GetBothProjectBasicListAsync(string? searchString, Employee loggedInEmployee, Guid tenantId); Task<ApiResponse<object>> GetBothProjectBasicListAsync(Guid? id, string? searchString, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> GetAllProjectsBasicAsync(bool provideAll, Employee loggedInEmployee, Guid tenantId); Task<ApiResponse<object>> GetAllProjectsBasicAsync(bool provideAll, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> GetAllProjectsAsync(string? searchString, int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId); Task<ApiResponse<object>> GetAllProjectsAsync(string? searchString, int pageNumber, int pageSize, Employee loggedInEmployee, Guid tenantId);
Task<ApiResponse<object>> GetProjectAsync(Guid id, Employee loggedInEmployee, Guid tenantId); Task<ApiResponse<object>> GetProjectAsync(Guid id, Employee loggedInEmployee, Guid tenantId);

View File

@ -0,0 +1,36 @@
using Marco.Pms.Model.Dtos.Collection;
using Marco.Pms.Model.Dtos.PurchaseInvoice;
using Marco.Pms.Model.Employees;
using Marco.Pms.Model.PurchaseInvoice;
using Marco.Pms.Model.Utilities;
using Marco.Pms.Model.ViewModels.PurchaseInvoice;
namespace Marco.Pms.Services.Service.ServiceInterfaces
{
public interface IPurchaseInvoiceService
{
#region =================================================================== Purchase Invoice Functions ===================================================================
Task<ApiResponse<object>> GetPurchaseInvoiceListAsync(string? searchString, string? filter, bool isActive, int pageSize, int pageNumber,
Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
Task<ApiResponse<PurchaseInvoiceDetailsVM>> GetPurchaseInvoiceDetailsAsync(Guid id, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
Task<ApiResponse<PurchaseInvoiceListVM>> CreatePurchaseInvoiceAsync(PurchaseInvoiceDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
Task<ApiResponse<object>> UpdatePurchaseInvoiceAsync(Guid id, PurchaseInvoiceDetails purchaseInvoice, PurchaseInvoiceDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
Task<ApiResponse<object>> DeletePurchaseInvoiceAsync(Guid id, bool isActive, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
#endregion
#region =================================================================== Delivery Challan Functions ===================================================================
Task<ApiResponse<List<DeliveryChallanVM>>> GetDeliveryChallansAsync(Guid purchaseInvoiceId, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
Task<ApiResponse<DeliveryChallanVM>> AddDeliveryChallanAsync(DeliveryChallanDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
#endregion
#region =================================================================== Purchase Invoice History Functions ===================================================================
Task<ApiResponse<object>> GetPurchaseInvoiceHistoryListAsync(Guid purchaseInvoiceId, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
Task<ApiResponse<object>> AddPurchaseInvoicePaymentAsync(ReceivedInvoicePaymentDto model, Employee loggedInEmployee, Guid tenantId, CancellationToken ct);
#endregion
#region =================================================================== Helper Functions ===================================================================
Task<PurchaseInvoiceDetails?> GetPurchaseInvoiceByIdAsync(Guid id, Guid tenantId, CancellationToken ct);
#endregion
}
}

View File

@ -42,7 +42,8 @@
"MongoDB": { "MongoDB": {
"SerilogDatabaseUrl": "mongodb://devuser:DevPass123@147.93.98.152:27017/DotNetLogsLocalDev?authSource=admin&eplicaSet=rs01&directConnection=true", "SerilogDatabaseUrl": "mongodb://devuser:DevPass123@147.93.98.152:27017/DotNetLogsLocalDev?authSource=admin&eplicaSet=rs01&directConnection=true",
"ConnectionString": "mongodb://devuser:DevPass123@147.93.98.152:27017/MarcoBMSLocalCache?authSource=admin&eplicaSet=rs01&directConnection=true&socketTimeoutMS=500&serverSelectionTimeoutMS=500&connectTimeoutMS=500", "ConnectionString": "mongodb://devuser:DevPass123@147.93.98.152:27017/MarcoBMSLocalCache?authSource=admin&eplicaSet=rs01&directConnection=true&socketTimeoutMS=500&serverSelectionTimeoutMS=500&connectTimeoutMS=500",
"ModificationConnectionString": "mongodb://devuser:DevPass123@147.93.98.152:27017/MarcoBMSLocalDev?authSource=admin&eplicaSet=rs01&directConnection=true" "ModificationConnectionString": "mongodb://devuser:DevPass123@147.93.98.152:27017/MarcoBMSLocalDev?authSource=admin&eplicaSet=rs01&directConnection=true",
"MailConnectionString": "mongodb://devuser:DevPass123@147.93.98.152:27017/MailLogsLocalDev?authSource=admin&eplicaSet=rs01&directConnection=true"
}, },
"Razorpay": { "Razorpay": {
"Key": "rzp_test_RXCzgEcXucbuAi", "Key": "rzp_test_RXCzgEcXucbuAi",