diff --git a/Backend/ERPCore.Tests/PermissionCoverageTests.cs b/Backend/ERPCore.Tests/PermissionCoverageTests.cs
index 0477b6f..1405fcf 100644
--- a/Backend/ERPCore.Tests/PermissionCoverageTests.cs
+++ b/Backend/ERPCore.Tests/PermissionCoverageTests.cs
@@ -157,6 +157,14 @@ public class PermissionCoverageTests
[InlineData("hrm.payroll")]
[InlineData("sales-returns")]
[InlineData("settings.audit-logs")]
+ [InlineData("dashboard.stock")]
+ [InlineData("dashboard.procurement")]
+ [InlineData("dashboard.approvals")]
+ [InlineData("dashboard.sales")]
+ [InlineData("dashboard.accounts")]
+ [InlineData("dashboard.hrm")]
+ [InlineData("dashboard.production")]
+ [InlineData("dashboard.service")]
public void NewPermissions_AreSeeded(string code)
{
using var db = CreateContext();
diff --git a/Backend/ERPCore/Controllers/DashboardController.cs b/Backend/ERPCore/Controllers/DashboardController.cs
index a979c63..bf4ddca 100644
--- a/Backend/ERPCore/Controllers/DashboardController.cs
+++ b/Backend/ERPCore/Controllers/DashboardController.cs
@@ -12,12 +12,25 @@ namespace ERPCore.Controllers;
public sealed class DashboardController : ApiControllerBase
{
private readonly IDashboardService _dashboard;
+ private readonly IDashboardChartsService _charts;
- public DashboardController(IDashboardService dashboard) => _dashboard = dashboard;
+ public DashboardController(IDashboardService dashboard, IDashboardChartsService charts)
+ {
+ _dashboard = dashboard;
+ _charts = charts;
+ }
- /// Aggregate counts for stock, GRN, and procurement.
+ /// Aggregate counts for stock, GRN, procurement, sales, accounts, service, production and HR.
[HttpGet("stats")]
[ProducesResponseType(typeof(DashboardStatsDto), StatusCodes.Status200OK)]
- public async Task> GetStats([FromQuery] int? branchId, CancellationToken ct)
- => Ok(await _dashboard.GetStatsAsync(branchId, ct));
+ public async Task> GetStats(
+ [FromQuery] int? branchId, [FromQuery] DateOnly? from, [FromQuery] DateOnly? to, CancellationToken ct)
+ => Ok(await _dashboard.GetStatsAsync(branchId, from, to, ct));
+
+ /// Datasets for the dashboard graphs; the time-based ones follow the optional from / to period.
+ [HttpGet("charts")]
+ [ProducesResponseType(typeof(DashboardChartsDto), StatusCodes.Status200OK)]
+ public async Task> GetCharts(
+ [FromQuery] int? branchId, [FromQuery] DateOnly? from, [FromQuery] DateOnly? to, CancellationToken ct)
+ => Ok(await _charts.GetChartsAsync(branchId, from, to, ct));
}
diff --git a/Backend/ERPCore/Dtos/Dashboard/DashboardChartsDtos.cs b/Backend/ERPCore/Dtos/Dashboard/DashboardChartsDtos.cs
new file mode 100644
index 0000000..1de8e6f
--- /dev/null
+++ b/Backend/ERPCore/Dtos/Dashboard/DashboardChartsDtos.cs
@@ -0,0 +1,46 @@
+namespace ERPCore.Dtos.Dashboard;
+
+///
+/// Every dataset behind the dashboard graphs, in one computed-on-read object (same idea as
+/// ). Time-based sets follow the From-To period the dashboard filter sends;
+/// "snapshot" sets (ageing, batches expiring, low stock, warranties) describe what is open now.
+/// Purchases-vs-sales by month and stock value by warehouse are already in .
+///
+public sealed record DashboardChartsDto(
+ // Sales
+ IReadOnlyList DailySales,
+ IReadOnlyList PaymentSplit,
+ IReadOnlyList TopItems,
+ IReadOnlyList SalesByCategory,
+ IReadOnlyList SalesByCustomer,
+ IReadOnlyList DocumentStatus,
+ // Money
+ IReadOnlyList CashInHandByDay,
+ IReadOnlyList ReceivablesAgeing,
+ IReadOnlyList PayablesAgeing,
+ IReadOnlyList ExpenseBreakdown,
+ // Purchasing
+ IReadOnlyList TopVendors,
+ // Stock
+ IReadOnlyList StockInVsOut,
+ IReadOnlyList BatchesExpiring,
+ // HR
+ IReadOnlyList AttendanceMix,
+ IReadOnlyList PayrollCostByMonth,
+ IReadOnlyList LeaveStatus,
+ // Service and production
+ IReadOnlyList TicketsOverTime,
+ IReadOnlyList ProductionStatus,
+ IReadOnlyList WarrantyExpiryByMonth);
+
+/// One labelled value: a bar, a donut slice or a point on a line.
+public sealed record ChartPointDto(string Label, decimal Value);
+
+/// Two values under one label (stock in/out by warehouse, tickets opened/resolved by day).
+public sealed record ChartPairDto(string Label, decimal First, decimal Second);
+
+/// One day of the Daily Sales, Gross Profit and Expenses line chart.
+public sealed record DailySalesPointDto(string Label, decimal Sales, decimal GrossProfit, decimal Expenses);
+
+/// Draft / Posted / Cancelled counts for one kind of document.
+public sealed record DocumentStatusDto(string Doc, int Draft, int Posted, int Cancelled);
diff --git a/Backend/ERPCore/Dtos/Dashboard/DashboardDtos.cs b/Backend/ERPCore/Dtos/Dashboard/DashboardDtos.cs
index 0b39522..8ab9414 100644
--- a/Backend/ERPCore/Dtos/Dashboard/DashboardDtos.cs
+++ b/Backend/ERPCore/Dtos/Dashboard/DashboardDtos.cs
@@ -10,6 +10,9 @@ namespace ERPCore.Dtos.Dashboard;
/// Recent movements isn't here — it's just GET /stock/ledger with a small pageSize, no
/// aggregation needed. A single computed-on-read object, not a stored entity — same as
/// reorder alerts (docs/11 §5.7).
+/// Most counts are a snapshot of what is open right now. The few that describe activity
+/// (sales returns, bundle sales, bank deposits, employees present) are counted inside the
+/// optional From-To period the dashboard filter sends (default: today).
///
public sealed record DashboardStatsDto(
int LowStockAlerts,
@@ -24,7 +27,37 @@ public sealed record DashboardStatsDto(
int OpenRfqs,
int PendingLeaveRequests,
int OverdueSalesInvoices,
- IReadOnlyList MonthlyPurchasesVsSales);
+ IReadOnlyList MonthlyPurchasesVsSales,
+ // Stock & warehouse
+ int BatchesExpiringSoon,
+ int TransfersInTransit,
+ int DraftStockAdjustments,
+ int WastagePendingApproval,
+ // Approvals
+ int PendingApprovals,
+ int PriceAdjustmentsPending,
+ // Sales (SalesReturns and BundleSales are counted inside the From-To period)
+ int DraftSalesInvoices,
+ int DraftSalesSlips,
+ int SalesReturns,
+ int BundleSales,
+ // Purchasing
+ int PurchaseReturnsPending,
+ int UnpaidPurchaseInvoices,
+ // Money
+ decimal CashInHand,
+ decimal BankDeposits,
+ // Service & warranty
+ int OpenSupportTickets,
+ int WarrantiesExpiringSoon,
+ int WarrantyClaimsOpen,
+ // Production
+ int ProductionRunsInProgress,
+ // HR
+ int PayrollRunsPending,
+ int AttendanceBatchesPending,
+ int EmployeesPresent,
+ int TotalActiveEmployees);
/// One bar in the Stock Valuation chart — total FIFO layer value for a warehouse.
public sealed record WarehouseValuationDto(int WarehouseId, decimal Total);
diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/PermissionConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/PermissionConfiguration.cs
index 80e9490..d1c205e 100644
--- a/Backend/ERPCore/Infra/Persistence/Configurations/PermissionConfiguration.cs
+++ b/Backend/ERPCore/Infra/Persistence/Configurations/PermissionConfiguration.cs
@@ -103,7 +103,15 @@ public sealed class PermissionConfiguration : IEntityTypeConfiguration
+using System;
+using ERPCore.Infra.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace ERPCore.Migrations
+{
+ [DbContext(typeof(ErpDbContext))]
+ [Migration("20261021080837_AddDashboardPermissions")]
+ partial class AddDashboardPermissions
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .UseCollation("utf8mb4_unicode_ci")
+ .HasAnnotation("ProductVersion", "9.0.20")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.AccessConfig", b =>
+ {
+ b.Property("ConfigId")
+ .HasColumnType("int");
+
+ b.Property("DefaultRoleId")
+ .HasColumnType("int");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("int unsigned");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("int");
+
+ b.HasKey("ConfigId");
+
+ b.HasIndex("DefaultRoleId");
+
+ b.HasIndex("UpdatedBy");
+
+ b.ToTable("access_config", null, t =>
+ {
+ t.HasCheckConstraint("ck_access_config_singleton", "`ConfigId` = 1");
+ });
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.ApprovalConfig", b =>
+ {
+ b.Property("ApprovalConfigId")
+ .HasColumnType("int");
+
+ b.Property("Level2RoleIds")
+ .HasMaxLength(500)
+ .HasColumnType("varchar(500)");
+
+ b.Property("PriceAdjustmentLevel2ThresholdPercent")
+ .HasPrecision(9, 4)
+ .HasColumnType("decimal(9,4)");
+
+ b.Property("PriceAdjustmentLevels")
+ .HasColumnType("int");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("int unsigned");
+
+ b.Property("StockAdjustmentLevel2ThresholdValue")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("StockAdjustmentLevels")
+ .HasColumnType("int");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("int");
+
+ b.Property("WastageLevels")
+ .HasColumnType("int");
+
+ b.HasKey("ApprovalConfigId");
+
+ b.ToTable("approval_config", null, t =>
+ {
+ t.HasCheckConstraint("ck_approval_config_singleton", "`ApprovalConfigId` = 1");
+ });
+
+ b.HasData(
+ new
+ {
+ ApprovalConfigId = 1,
+ PriceAdjustmentLevel2ThresholdPercent = 0m,
+ PriceAdjustmentLevels = 2,
+ RowVersion = 1u,
+ StockAdjustmentLevel2ThresholdValue = 0m,
+ StockAdjustmentLevels = 2,
+ WastageLevels = 0
+ });
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.AttendanceRecord", b =>
+ {
+ b.Property("AttendanceRecordId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AttendanceRecordId"));
+
+ b.Property("AttendanceDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("AttendanceStatus")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)");
+
+ b.Property("AttendanceUploadBatchId")
+ .HasColumnType("int");
+
+ b.Property("CheckIn")
+ .HasColumnType("time(6)");
+
+ b.Property("CheckOut")
+ .HasColumnType("time(6)");
+
+ b.Property("DuplicateOfAttendanceRecordId")
+ .HasColumnType("int");
+
+ b.Property("EarlyLeaveMinutes")
+ .HasColumnType("int");
+
+ b.Property("EditedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("EditedBy")
+ .HasColumnType("int");
+
+ b.Property("EmployeeId")
+ .HasColumnType("int");
+
+ b.Property("IsManualOverride")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("LateMinutes")
+ .HasColumnType("int");
+
+ b.Property("Notes")
+ .HasMaxLength(1000)
+ .HasColumnType("varchar(1000)");
+
+ b.Property("OvertimeMinutes")
+ .HasColumnType("int");
+
+ b.Property("RowValidationStatus")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("int unsigned");
+
+ b.Property("WorkShiftId")
+ .HasColumnType("int");
+
+ b.Property("WorkingMinutes")
+ .HasColumnType("int");
+
+ b.HasKey("AttendanceRecordId");
+
+ b.HasIndex("AttendanceUploadBatchId");
+
+ b.HasIndex("WorkShiftId");
+
+ b.HasIndex("EmployeeId", "AttendanceDate");
+
+ b.ToTable("hr_attendance_records", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.AttendanceUploadBatch", b =>
+ {
+ b.Property("AttendanceUploadBatchId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AttendanceUploadBatchId"));
+
+ b.Property("ConfirmedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("ConfirmedBy")
+ .HasColumnType("int");
+
+ b.Property("DocNo")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("varchar(30)");
+
+ b.Property("OriginalFileName")
+ .HasMaxLength(260)
+ .HasColumnType("varchar(260)");
+
+ b.Property("PeriodEnd")
+ .HasColumnType("datetime(6)");
+
+ b.Property("PeriodStart")
+ .HasColumnType("datetime(6)");
+
+ b.Property("RowCountDuplicate")
+ .HasColumnType("int");
+
+ b.Property("RowCountError")
+ .HasColumnType("int");
+
+ b.Property("RowCountTotal")
+ .HasColumnType("int");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("int unsigned");
+
+ b.Property("SourceType")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)");
+
+ b.Property("UploadedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("UploadedBy")
+ .HasColumnType("int");
+
+ b.HasKey("AttendanceUploadBatchId");
+
+ b.HasIndex("DocNo")
+ .IsUnique();
+
+ b.HasIndex("Status");
+
+ b.HasIndex("PeriodStart", "PeriodEnd");
+
+ b.ToTable("hr_attendance_upload_batches", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.AuditLog", b =>
+ {
+ b.Property("AuditId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AuditId"));
+
+ b.Property("Action")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("varchar(10)");
+
+ b.Property("ChangeSet")
+ .IsRequired()
+ .HasColumnType("longtext");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("EntityId")
+ .HasColumnType("int");
+
+ b.Property("EntityType")
+ .IsRequired()
+ .HasMaxLength(80)
+ .HasColumnType("varchar(80)");
+
+ b.Property("UserId")
+ .HasColumnType("int");
+
+ b.HasKey("AuditId");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("EntityType", "EntityId");
+
+ b.ToTable("audit_logs", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.BankDeposit", b =>
+ {
+ b.Property("BankDepositId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BankDepositId"));
+
+ b.Property("Amount")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("BranchId")
+ .HasColumnType("int");
+
+ b.Property("BusinessDate")
+ .HasColumnType("date");
+
+ b.Property("CancelReason")
+ .HasMaxLength(250)
+ .HasColumnType("varchar(250)");
+
+ b.Property("CancelledAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("CancelledBy")
+ .HasColumnType("int");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("CreatedBy")
+ .HasColumnType("int");
+
+ b.Property("DepositDate")
+ .HasColumnType("date");
+
+ b.Property("DepositSlipNo")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("DepositedBy")
+ .HasMaxLength(150)
+ .HasColumnType("varchar(150)");
+
+ b.Property("DocNo")
+ .IsRequired()
+ .HasMaxLength(30)
+ .HasColumnType("varchar(30)");
+
+ b.Property("FromCashAccountCode")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)");
+
+ b.Property("FromCashAccountName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("FromGlCashAccountId")
+ .HasColumnType("bigint");
+
+ b.Property("GlJournalNo")
+ .HasMaxLength(30)
+ .HasColumnType("varchar(30)");
+
+ b.Property("GlPostedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("GlReversalJournalNo")
+ .HasMaxLength(30)
+ .HasColumnType("varchar(30)");
+
+ b.Property("Remarks")
+ .HasMaxLength(500)
+ .HasColumnType("varchar(500)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("int unsigned");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)")
+ .HasDefaultValue("Posted");
+
+ b.Property("ToAccountNumber")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("ToBankAccountCode")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)");
+
+ b.Property("ToBankAccountName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("ToBankName")
+ .HasMaxLength(150)
+ .HasColumnType("varchar(150)");
+
+ b.Property("ToGlBankAccountId")
+ .HasColumnType("bigint");
+
+ b.HasKey("BankDepositId");
+
+ b.HasIndex("CreatedBy");
+
+ b.HasIndex("DocNo")
+ .IsUnique();
+
+ b.HasIndex("ToGlBankAccountId");
+
+ b.HasIndex("BranchId", "BusinessDate", "Status");
+
+ b.ToTable("bank_deposits", null, t =>
+ {
+ t.HasCheckConstraint("ck_bank_deposits_amount", "`Amount` > 0");
+ });
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Batch", b =>
+ {
+ b.Property("BatchId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BatchId"));
+
+ b.Property("Barcode")
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)");
+
+ b.Property("BatchNo")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("ExpiryDate")
+ .HasColumnType("date");
+
+ b.Property("ItemId")
+ .HasColumnType("int");
+
+ b.HasKey("BatchId");
+
+ b.HasIndex("Barcode")
+ .IsUnique();
+
+ b.HasIndex("ItemId", "BatchNo")
+ .IsUnique();
+
+ b.ToTable("batches", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Bin", b =>
+ {
+ b.Property("BinId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BinId"));
+
+ b.Property("BinType")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("WarehouseId")
+ .HasColumnType("int");
+
+ b.HasKey("BinId");
+
+ b.HasIndex("WarehouseId", "Code")
+ .IsUnique();
+
+ b.ToTable("bins", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Branch", b =>
+ {
+ b.Property("BranchId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BranchId"));
+
+ b.Property("Address")
+ .HasMaxLength(500)
+ .HasColumnType("varchar(500)");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("PhoneNumber")
+ .HasMaxLength(30)
+ .HasColumnType("varchar(30)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("int unsigned");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)")
+ .HasDefaultValue("Active");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.HasKey("BranchId");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.HasIndex("Status");
+
+ b.ToTable("hr_branches", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.BranchConfig", b =>
+ {
+ b.Property("ConfigId")
+ .HasColumnType("int");
+
+ b.Property("BranchingEnabled")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("tinyint(1)")
+ .HasDefaultValue(true);
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("int unsigned");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("UpdatedBy")
+ .HasColumnType("int");
+
+ b.Property("WarehouseBranchingEnabled")
+ .HasColumnType("tinyint(1)");
+
+ b.HasKey("ConfigId");
+
+ b.HasIndex("UpdatedBy");
+
+ b.ToTable("branch_config", null, t =>
+ {
+ t.HasCheckConstraint("ck_branch_config_singleton", "`ConfigId` = 1");
+ });
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Brand", b =>
+ {
+ b.Property("BrandId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BrandId"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("int unsigned");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)")
+ .HasDefaultValue("Active");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.HasKey("BrandId");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.HasIndex("Status");
+
+ b.ToTable("brands", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.BundleSale", b =>
+ {
+ b.Property("BundleSaleId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BundleSaleId"));
+
+ b.Property("BundleCode")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("BundleDate")
+ .HasColumnType("datetime(6)");
+
+ b.Property("BundleName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("BundleNo")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("BundlePrice")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("BundleSaleTemplateId")
+ .HasColumnType("int");
+
+ b.Property("CardAmount")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("CashAmount")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("CashierUserId")
+ .HasColumnType("int");
+
+ b.Property("ComponentSubtotal")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("CustomerId")
+ .HasColumnType("int");
+
+ b.Property("CustomerSnapshotName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("DayEndId")
+ .HasColumnType("int");
+
+ b.Property("DiscountTotal")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("GrandTotal")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("MarginAmount")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("int unsigned");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)")
+ .HasDefaultValue("Draft");
+
+ b.Property("TaxTotal")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("WarehouseId")
+ .HasColumnType("int");
+
+ b.HasKey("BundleSaleId");
+
+ b.HasIndex("BundleNo")
+ .IsUnique();
+
+ b.HasIndex("BundleSaleTemplateId");
+
+ b.HasIndex("CashierUserId");
+
+ b.HasIndex("CustomerId");
+
+ b.HasIndex("DayEndId");
+
+ b.HasIndex("WarehouseId");
+
+ b.ToTable("bundle_sales", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleLine", b =>
+ {
+ b.Property("BundleSaleLineId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BundleSaleLineId"));
+
+ b.Property("BundleSaleId")
+ .HasColumnType("int");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("IncludeInBundle")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("tinyint(1)")
+ .HasDefaultValue(true);
+
+ b.Property("IsComponent")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("tinyint(1)")
+ .HasDefaultValue(true);
+
+ b.Property("ItemId")
+ .HasColumnType("int");
+
+ b.Property("LineTotal")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("ParentLineId")
+ .HasColumnType("int");
+
+ b.Property("Qty")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("UnitPrice")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("WarehouseId")
+ .HasColumnType("int");
+
+ b.HasKey("BundleSaleLineId");
+
+ b.HasIndex("BundleSaleId");
+
+ b.HasIndex("ItemId");
+
+ b.HasIndex("WarehouseId");
+
+ b.ToTable("bundle_sale_lines", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleTemplate", b =>
+ {
+ b.Property("BundleSaleTemplateId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BundleSaleTemplateId"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Description")
+ .HasMaxLength(1000)
+ .HasColumnType("varchar(1000)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("int unsigned");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)")
+ .HasDefaultValue("Active");
+
+ b.Property("TemplateCode")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("TemplateName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.HasKey("BundleSaleTemplateId");
+
+ b.HasIndex("TemplateCode")
+ .IsUnique();
+
+ b.ToTable("bundle_sale_templates", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleTemplateLine", b =>
+ {
+ b.Property("BundleSaleTemplateLineId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BundleSaleTemplateLineId"));
+
+ b.Property("BundleSaleTemplateId")
+ .HasColumnType("int");
+
+ b.Property("IncludeInBundle")
+ .HasColumnType("tinyint(1)");
+
+ b.Property("ItemId")
+ .HasColumnType("int");
+
+ b.Property("Qty")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("SortOrder")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasDefaultValue(0);
+
+ b.Property("UnitPrice")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("WarehouseId")
+ .HasColumnType("int");
+
+ b.HasKey("BundleSaleTemplateLineId");
+
+ b.HasIndex("BundleSaleTemplateId");
+
+ b.HasIndex("ItemId");
+
+ b.HasIndex("WarehouseId");
+
+ b.ToTable("bundle_sale_template_lines", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Category", b =>
+ {
+ b.Property("CategoryId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CategoryId"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("int unsigned");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)")
+ .HasDefaultValue("Active");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.HasKey("CategoryId");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.HasIndex("Status");
+
+ b.ToTable("categories", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Customer", b =>
+ {
+ b.Property("CustomerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CustomerId"));
+
+ b.Property("AddressLine1")
+ .HasMaxLength(250)
+ .HasColumnType("varchar(250)");
+
+ b.Property("AddressLine2")
+ .HasMaxLength(250)
+ .HasColumnType("varchar(250)");
+
+ b.Property("BranchId")
+ .HasColumnType("int");
+
+ b.Property("City")
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)");
+
+ b.Property("Country")
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("CreditDays")
+ .HasColumnType("int");
+
+ b.Property("CreditLimit")
+ .HasPrecision(18, 4)
+ .HasColumnType("decimal(18,4)");
+
+ b.Property("CustomerCode")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("CustomerType")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)")
+ .HasDefaultValue("B2C");
+
+ b.Property("DefaultWarehouseId")
+ .HasColumnType("int");
+
+ b.Property("DisplayName")
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("Email")
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("Phone")
+ .HasMaxLength(30)
+ .HasColumnType("varchar(30)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("int unsigned");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)")
+ .HasDefaultValue("Active");
+
+ b.Property("TaxRegistrationNo")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.HasKey("CustomerId");
+
+ b.HasIndex("BranchId");
+
+ b.HasIndex("CustomerCode")
+ .IsUnique();
+
+ b.HasIndex("CustomerType");
+
+ b.HasIndex("DefaultWarehouseId");
+
+ b.HasIndex("Status");
+
+ b.ToTable("customers", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Department", b =>
+ {
+ b.Property("DepartmentId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("DepartmentId"));
+
+ b.Property("BranchId")
+ .HasColumnType("int");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("HeadEmployeeId")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("ParentDepartmentId")
+ .HasColumnType("int");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("int unsigned");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)")
+ .HasDefaultValue("Active");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.HasKey("DepartmentId");
+
+ b.HasIndex("BranchId");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.HasIndex("HeadEmployeeId");
+
+ b.HasIndex("ParentDepartmentId");
+
+ b.HasIndex("Status");
+
+ b.ToTable("hr_departments", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.Designation", b =>
+ {
+ b.Property("DesignationId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("DesignationId"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)");
+
+ b.Property("RowVersion")
+ .IsConcurrencyToken()
+ .HasColumnType("int unsigned");
+
+ b.Property("Status")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)")
+ .HasDefaultValue("Active");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)");
+
+ b.HasKey("DesignationId");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.HasIndex("Status");
+
+ b.ToTable("hr_designations", (string)null);
+ });
+
+ modelBuilder.Entity("ERPCore.Domain.Entities.DocumentApproval", b =>
+ {
+ b.Property("DocumentApprovalId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("DocumentApprovalId"));
+
+ b.Property("Comment")
+ .HasMaxLength(250)
+ .HasColumnType("varchar(250)");
+
+ b.Property("DecidedAt")
+ .HasColumnType("datetime(6)");
+
+ b.Property("DecidedBy")
+ .HasColumnType("int");
+
+ b.Property("Decision")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)");
+
+ b.Property("DocId")
+ .HasColumnType("int");
+
+ b.Property("DocType")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("varchar(10)");
+
+ b.Property