diff --git a/src/Backend/DiunaBI.Core/Database/Migrations/20250725133501_DataInbox.LayerId.Designer.cs b/src/Backend/DiunaBI.Core/Database/Migrations/20250725133501_DataInbox.LayerId.Designer.cs
new file mode 100644
index 0000000..8943b9c
--- /dev/null
+++ b/src/Backend/DiunaBI.Core/Database/Migrations/20250725133501_DataInbox.LayerId.Designer.cs
@@ -0,0 +1,418 @@
+//
+using System;
+using DiunaBI.Core.Database.Context;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace DiunaBI.Core.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20250725133501_DataInbox.LayerId")]
+ partial class DataInboxLayerId
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("DiunaBI.Core.Models.DataInbox", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("Data")
+ .IsRequired()
+ .HasMaxLength(2147483647)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("LayerId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("Source")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.HasKey("Id");
+
+ b.ToTable("DataInbox");
+ });
+
+ modelBuilder.Entity("DiunaBI.Core.Models.Layer", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedById")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("IsCancelled")
+ .HasColumnType("bit");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("ModifiedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("ModifiedById")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("Number")
+ .HasColumnType("int");
+
+ b.Property("ParentId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Type")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("ModifiedById");
+
+ b.HasIndex("ParentId");
+
+ b.ToTable("Layers");
+ });
+
+ modelBuilder.Entity("DiunaBI.Core.Models.ProcessSource", b =>
+ {
+ b.Property("LayerId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("SourceId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("LayerId", "SourceId");
+
+ b.HasIndex("SourceId");
+
+ b.ToTable("ProcessSources");
+ });
+
+ modelBuilder.Entity("DiunaBI.Core.Models.QueueJob", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CompletedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedById")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("JobType")
+ .HasColumnType("int");
+
+ b.Property("LastAttemptAt")
+ .HasColumnType("datetime2");
+
+ b.Property("LastError")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("LayerId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("LayerName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("MaxRetries")
+ .HasColumnType("int");
+
+ b.Property("ModifiedAtUtc")
+ .HasColumnType("datetime2");
+
+ b.Property("ModifiedById")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("PluginName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Priority")
+ .HasColumnType("int");
+
+ b.Property("RetryCount")
+ .HasColumnType("int");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.ToTable("QueueJobs");
+ });
+
+ modelBuilder.Entity("DiunaBI.Core.Models.Record", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedById")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Desc1")
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("LayerId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ModifiedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("ModifiedById")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Value1")
+ .HasColumnType("float");
+
+ b.Property("Value10")
+ .HasColumnType("float");
+
+ b.Property("Value11")
+ .HasColumnType("float");
+
+ b.Property("Value12")
+ .HasColumnType("float");
+
+ b.Property("Value13")
+ .HasColumnType("float");
+
+ b.Property("Value14")
+ .HasColumnType("float");
+
+ b.Property("Value15")
+ .HasColumnType("float");
+
+ b.Property("Value16")
+ .HasColumnType("float");
+
+ b.Property("Value17")
+ .HasColumnType("float");
+
+ b.Property("Value18")
+ .HasColumnType("float");
+
+ b.Property("Value19")
+ .HasColumnType("float");
+
+ b.Property("Value2")
+ .HasColumnType("float");
+
+ b.Property("Value20")
+ .HasColumnType("float");
+
+ b.Property("Value21")
+ .HasColumnType("float");
+
+ b.Property("Value22")
+ .HasColumnType("float");
+
+ b.Property("Value23")
+ .HasColumnType("float");
+
+ b.Property("Value24")
+ .HasColumnType("float");
+
+ b.Property("Value25")
+ .HasColumnType("float");
+
+ b.Property("Value26")
+ .HasColumnType("float");
+
+ b.Property("Value27")
+ .HasColumnType("float");
+
+ b.Property("Value28")
+ .HasColumnType("float");
+
+ b.Property("Value29")
+ .HasColumnType("float");
+
+ b.Property("Value3")
+ .HasColumnType("float");
+
+ b.Property("Value30")
+ .HasColumnType("float");
+
+ b.Property("Value31")
+ .HasColumnType("float");
+
+ b.Property("Value32")
+ .HasColumnType("float");
+
+ b.Property("Value4")
+ .HasColumnType("float");
+
+ b.Property("Value5")
+ .HasColumnType("float");
+
+ b.Property("Value6")
+ .HasColumnType("float");
+
+ b.Property("Value7")
+ .HasColumnType("float");
+
+ b.Property("Value8")
+ .HasColumnType("float");
+
+ b.Property("Value9")
+ .HasColumnType("float");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("LayerId");
+
+ b.HasIndex("ModifiedById");
+
+ b.ToTable("Records");
+ });
+
+ modelBuilder.Entity("DiunaBI.Core.Models.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("Email")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("UserName")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("DiunaBI.Core.Models.Layer", b =>
+ {
+ b.HasOne("DiunaBI.Core.Models.User", "CreatedBy")
+ .WithMany()
+ .HasForeignKey("CreatedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("DiunaBI.Core.Models.User", "ModifiedBy")
+ .WithMany()
+ .HasForeignKey("ModifiedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("DiunaBI.Core.Models.Layer", "Parent")
+ .WithMany()
+ .HasForeignKey("ParentId");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("ModifiedBy");
+
+ b.Navigation("Parent");
+ });
+
+ modelBuilder.Entity("DiunaBI.Core.Models.ProcessSource", b =>
+ {
+ b.HasOne("DiunaBI.Core.Models.Layer", "Source")
+ .WithMany()
+ .HasForeignKey("SourceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Source");
+ });
+
+ modelBuilder.Entity("DiunaBI.Core.Models.Record", b =>
+ {
+ b.HasOne("DiunaBI.Core.Models.User", "CreatedBy")
+ .WithMany()
+ .HasForeignKey("CreatedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("DiunaBI.Core.Models.Layer", null)
+ .WithMany("Records")
+ .HasForeignKey("LayerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("DiunaBI.Core.Models.User", "ModifiedBy")
+ .WithMany()
+ .HasForeignKey("ModifiedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("ModifiedBy");
+ });
+
+ modelBuilder.Entity("DiunaBI.Core.Models.Layer", b =>
+ {
+ b.Navigation("Records");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Backend/DiunaBI.Core/Database/Migrations/20250725133501_DataInbox.LayerId.cs b/src/Backend/DiunaBI.Core/Database/Migrations/20250725133501_DataInbox.LayerId.cs
new file mode 100644
index 0000000..c16f817
--- /dev/null
+++ b/src/Backend/DiunaBI.Core/Database/Migrations/20250725133501_DataInbox.LayerId.cs
@@ -0,0 +1,29 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace DiunaBI.Core.Migrations
+{
+ ///
+ public partial class DataInboxLayerId : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "LayerId",
+ table: "DataInbox",
+ type: "uniqueidentifier",
+ nullable: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "LayerId",
+ table: "DataInbox");
+ }
+ }
+}
diff --git a/src/Backend/DiunaBI.Core/Database/Migrations/20250726091001_Remove DataInbox.LayerId.Designer.cs b/src/Backend/DiunaBI.Core/Database/Migrations/20250726091001_Remove DataInbox.LayerId.Designer.cs
new file mode 100644
index 0000000..bc53e1c
--- /dev/null
+++ b/src/Backend/DiunaBI.Core/Database/Migrations/20250726091001_Remove DataInbox.LayerId.Designer.cs
@@ -0,0 +1,407 @@
+//
+using System;
+using DiunaBI.Core.Database.Context;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace DiunaBI.Core.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20250726091001_Remove DataInbox.LayerId")]
+ partial class RemoveDataInboxLayerId
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("DiunaBI.Core.Models.DataInbox", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("Data")
+ .IsRequired()
+ .HasMaxLength(2147483647)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("Source")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.HasKey("Id");
+
+ b.ToTable("DataInbox");
+ });
+
+ modelBuilder.Entity("DiunaBI.Core.Models.Layer", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedById")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("IsCancelled")
+ .HasColumnType("bit");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("ModifiedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("ModifiedById")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("Number")
+ .HasColumnType("int");
+
+ b.Property("ParentId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Type")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("ModifiedById");
+
+ b.ToTable("Layers");
+ });
+
+ modelBuilder.Entity("DiunaBI.Core.Models.ProcessSource", b =>
+ {
+ b.Property("LayerId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("SourceId")
+ .HasColumnType("uniqueidentifier");
+
+ b.HasKey("LayerId", "SourceId");
+
+ b.HasIndex("SourceId");
+
+ b.ToTable("ProcessSources");
+ });
+
+ modelBuilder.Entity("DiunaBI.Core.Models.QueueJob", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CompletedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedAtUtc")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedById")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("JobType")
+ .HasColumnType("int");
+
+ b.Property("LastAttemptAt")
+ .HasColumnType("datetime2");
+
+ b.Property("LastError")
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("LayerId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("LayerName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)");
+
+ b.Property("MaxRetries")
+ .HasColumnType("int");
+
+ b.Property("ModifiedAtUtc")
+ .HasColumnType("datetime2");
+
+ b.Property("ModifiedById")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("PluginName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Priority")
+ .HasColumnType("int");
+
+ b.Property("RetryCount")
+ .HasColumnType("int");
+
+ b.Property("Status")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.ToTable("QueueJobs");
+ });
+
+ modelBuilder.Entity("DiunaBI.Core.Models.Record", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("CreatedById")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Desc1")
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("IsDeleted")
+ .HasColumnType("bit");
+
+ b.Property("LayerId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("ModifiedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("ModifiedById")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Value1")
+ .HasColumnType("float");
+
+ b.Property("Value10")
+ .HasColumnType("float");
+
+ b.Property("Value11")
+ .HasColumnType("float");
+
+ b.Property("Value12")
+ .HasColumnType("float");
+
+ b.Property("Value13")
+ .HasColumnType("float");
+
+ b.Property("Value14")
+ .HasColumnType("float");
+
+ b.Property("Value15")
+ .HasColumnType("float");
+
+ b.Property("Value16")
+ .HasColumnType("float");
+
+ b.Property("Value17")
+ .HasColumnType("float");
+
+ b.Property("Value18")
+ .HasColumnType("float");
+
+ b.Property("Value19")
+ .HasColumnType("float");
+
+ b.Property("Value2")
+ .HasColumnType("float");
+
+ b.Property("Value20")
+ .HasColumnType("float");
+
+ b.Property("Value21")
+ .HasColumnType("float");
+
+ b.Property("Value22")
+ .HasColumnType("float");
+
+ b.Property("Value23")
+ .HasColumnType("float");
+
+ b.Property("Value24")
+ .HasColumnType("float");
+
+ b.Property("Value25")
+ .HasColumnType("float");
+
+ b.Property("Value26")
+ .HasColumnType("float");
+
+ b.Property("Value27")
+ .HasColumnType("float");
+
+ b.Property("Value28")
+ .HasColumnType("float");
+
+ b.Property("Value29")
+ .HasColumnType("float");
+
+ b.Property("Value3")
+ .HasColumnType("float");
+
+ b.Property("Value30")
+ .HasColumnType("float");
+
+ b.Property("Value31")
+ .HasColumnType("float");
+
+ b.Property("Value32")
+ .HasColumnType("float");
+
+ b.Property("Value4")
+ .HasColumnType("float");
+
+ b.Property("Value5")
+ .HasColumnType("float");
+
+ b.Property("Value6")
+ .HasColumnType("float");
+
+ b.Property("Value7")
+ .HasColumnType("float");
+
+ b.Property("Value8")
+ .HasColumnType("float");
+
+ b.Property("Value9")
+ .HasColumnType("float");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("LayerId");
+
+ b.HasIndex("ModifiedById");
+
+ b.ToTable("Records");
+ });
+
+ modelBuilder.Entity("DiunaBI.Core.Models.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2");
+
+ b.Property("Email")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("UserName")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("DiunaBI.Core.Models.Layer", b =>
+ {
+ b.HasOne("DiunaBI.Core.Models.User", "CreatedBy")
+ .WithMany()
+ .HasForeignKey("CreatedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("DiunaBI.Core.Models.User", "ModifiedBy")
+ .WithMany()
+ .HasForeignKey("ModifiedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("ModifiedBy");
+ });
+
+ modelBuilder.Entity("DiunaBI.Core.Models.ProcessSource", b =>
+ {
+ b.HasOne("DiunaBI.Core.Models.Layer", "Source")
+ .WithMany()
+ .HasForeignKey("SourceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Source");
+ });
+
+ modelBuilder.Entity("DiunaBI.Core.Models.Record", b =>
+ {
+ b.HasOne("DiunaBI.Core.Models.User", "CreatedBy")
+ .WithMany()
+ .HasForeignKey("CreatedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("DiunaBI.Core.Models.Layer", null)
+ .WithMany("Records")
+ .HasForeignKey("LayerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("DiunaBI.Core.Models.User", "ModifiedBy")
+ .WithMany()
+ .HasForeignKey("ModifiedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("ModifiedBy");
+ });
+
+ modelBuilder.Entity("DiunaBI.Core.Models.Layer", b =>
+ {
+ b.Navigation("Records");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Backend/DiunaBI.Core/Database/Migrations/20250726091001_Remove DataInbox.LayerId.cs b/src/Backend/DiunaBI.Core/Database/Migrations/20250726091001_Remove DataInbox.LayerId.cs
new file mode 100644
index 0000000..c50a662
--- /dev/null
+++ b/src/Backend/DiunaBI.Core/Database/Migrations/20250726091001_Remove DataInbox.LayerId.cs
@@ -0,0 +1,49 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace DiunaBI.Core.Migrations
+{
+ ///
+ public partial class RemoveDataInboxLayerId : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_Layers_Layers_ParentId",
+ table: "Layers");
+
+ migrationBuilder.DropIndex(
+ name: "IX_Layers_ParentId",
+ table: "Layers");
+
+ migrationBuilder.DropColumn(
+ name: "LayerId",
+ table: "DataInbox");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "LayerId",
+ table: "DataInbox",
+ type: "uniqueidentifier",
+ nullable: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Layers_ParentId",
+ table: "Layers",
+ column: "ParentId");
+
+ migrationBuilder.AddForeignKey(
+ name: "FK_Layers_Layers_ParentId",
+ table: "Layers",
+ column: "ParentId",
+ principalTable: "Layers",
+ principalColumn: "Id");
+ }
+ }
+}
diff --git a/src/Backend/DiunaBI.Core/Database/Migrations/AppDbContextModelSnapshot.cs b/src/Backend/DiunaBI.Core/Database/Migrations/AppDbContextModelSnapshot.cs
index c325531..1a764c7 100644
--- a/src/Backend/DiunaBI.Core/Database/Migrations/AppDbContextModelSnapshot.cs
+++ b/src/Backend/DiunaBI.Core/Database/Migrations/AppDbContextModelSnapshot.cs
@@ -95,8 +95,6 @@ namespace DiunaBI.Core.Migrations
b.HasIndex("ModifiedById");
- b.HasIndex("ParentId");
-
b.ToTable("Layers");
});
@@ -355,15 +353,9 @@ namespace DiunaBI.Core.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
- b.HasOne("DiunaBI.Core.Models.Layer", "Parent")
- .WithMany()
- .HasForeignKey("ParentId");
-
b.Navigation("CreatedBy");
b.Navigation("ModifiedBy");
-
- b.Navigation("Parent");
});
modelBuilder.Entity("DiunaBI.Core.Models.ProcessSource", b =>
diff --git a/src/Backend/DiunaBI.Core/Models/Layer.cs b/src/Backend/DiunaBI.Core/Models/Layer.cs
index ca44018..e8782a3 100644
--- a/src/Backend/DiunaBI.Core/Models/Layer.cs
+++ b/src/Backend/DiunaBI.Core/Models/Layer.cs
@@ -41,6 +41,5 @@ public class Layer
public Guid ModifiedById { get; set; }
public User? ModifiedBy { get; init; }
public Guid? ParentId { get; init; }
- public Layer? Parent { get; init; }
#endregion
}
\ No newline at end of file
diff --git a/src/Backend/DiunaBI.Plugins.Morska/Importers/MorskaD3Importer.cs b/src/Backend/DiunaBI.Plugins.Morska/Importers/MorskaD3Importer.cs
index ffae136..d2942e9 100644
--- a/src/Backend/DiunaBI.Plugins.Morska/Importers/MorskaD3Importer.cs
+++ b/src/Backend/DiunaBI.Plugins.Morska/Importers/MorskaD3Importer.cs
@@ -5,14 +5,18 @@ using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore;
using DiunaBI.Core.Models;
using DiunaBI.Core.Database.Context;
+using DiunaBI.Core.Services;
+using Google.Apis.Sheets.v4;
+using Google.Apis.Sheets.v4.Data;
namespace DiunaBI.Plugins.Morska.Importers;
public class MorskaD3Importer : MorskaBaseImporter
{
public override string ImporterType => "Morska.Import.D3";
-
+
private readonly AppDbContext _db;
+ private readonly SpreadsheetsResource.ValuesResource _googleSheetValues;
private readonly ILogger _logger;
// Configuration properties
@@ -20,15 +24,20 @@ public class MorskaD3Importer : MorskaBaseImporter
private string? ImportMonth { get; set; }
private string? ImportName { get; set; }
private string? ImportType { get; set; }
- private DateTime? StartDate { get; set; }
- private DateTime? EndDate { get; set; }
+ private string? SheetId { get; set; }
private bool IsEnabled { get; set; }
+ // Cached deserialized data
+ private List? _cachedRecords;
+ private string? _cachedDataInboxId;
+
public MorskaD3Importer(
AppDbContext db,
+ SpreadsheetsResource.ValuesResource googleSheetValues,
ILogger logger)
{
_db = db;
+ _googleSheetValues = googleSheetValues;
_logger = logger;
}
@@ -36,14 +45,21 @@ public class MorskaD3Importer : MorskaBaseImporter
{
try
{
- _logger.LogInformation("{ImporterType}: Starting import for {ImportWorkerName} ({ImportWorkerId})",
+ _logger.LogInformation("{ImporterType}: Starting import for {ImportWorkerName} ({ImportWorkerId})",
ImporterType, importWorker.Name, importWorker.Id);
+ // Clear cache at start
+ _cachedRecords = null;
+ _cachedDataInboxId = null;
+
LoadConfiguration(importWorker);
-
+
+ // Deserialize data early - right after LoadConfiguration
+ DeserializeDataInboxData();
+
if (!ShouldPerformImport(importWorker))
{
- _logger.LogInformation("{ImporterType}: Import not needed for {ImportWorkerName}",
+ _logger.LogInformation("{ImporterType}: Import not needed for {ImportWorkerName}",
ImporterType, importWorker.Name);
return;
}
@@ -51,16 +67,25 @@ public class MorskaD3Importer : MorskaBaseImporter
ValidateConfiguration();
PerformImport(importWorker);
-
- _logger.LogInformation("{ImporterType}: Successfully completed import for {ImportWorkerName}",
+
+ // Export to Google Sheets after successful import
+ ExportToGoogleSheets();
+
+ _logger.LogInformation("{ImporterType}: Successfully completed import for {ImportWorkerName}",
ImporterType, importWorker.Name);
}
catch (Exception e)
{
- _logger.LogError(e, "{ImporterType}: Failed to import {ImportWorkerName} ({ImportWorkerId})",
+ _logger.LogError(e, "{ImporterType}: Failed to import {ImportWorkerName} ({ImportWorkerId})",
ImporterType, importWorker.Name, importWorker.Id);
throw;
}
+ finally
+ {
+ // Clear cache after import
+ _cachedRecords = null;
+ _cachedDataInboxId = null;
+ }
}
private void LoadConfiguration(Layer importWorker)
@@ -71,56 +96,107 @@ public class MorskaD3Importer : MorskaBaseImporter
ImportMonth = GetRecordValue(importWorker.Records, "ImportMonth");
ImportName = GetRecordValue(importWorker.Records, "ImportName");
ImportType = GetRecordValue(importWorker.Records, "ImportType");
+ SheetId = GetRecordValue(importWorker.Records, "SheetId");
IsEnabled = GetRecordValue(importWorker.Records, "IsEnabled") == "True";
- var startDateStr = GetRecordValue(importWorker.Records, "StartDate");
- if (startDateStr != null && DateTime.TryParseExact(startDateStr, "yyyy.MM.dd", null, DateTimeStyles.None, out var startDate))
+ _logger.LogDebug(
+ "{ImporterType}: Configuration loaded for {ImportWorkerName} - Type: {ImportType}, SheetId: {SheetId}",
+ ImporterType, importWorker.Name, ImportType, SheetId);
+ }
+
+ private void DeserializeDataInboxData()
+ {
+ var dataInbox = _db.DataInbox.OrderByDescending(x => x.CreatedAt).FirstOrDefault(x => x.Name == ImportType);
+ if (dataInbox == null)
{
- StartDate = startDate;
+ throw new InvalidOperationException($"DataInbox not found for type: {ImportType}");
}
- var endDateStr = GetRecordValue(importWorker.Records, "EndDate");
- if (endDateStr != null && DateTime.TryParseExact(endDateStr, "yyyy.MM.dd", null, DateTimeStyles.None, out var endDate))
+ _logger.LogDebug("{ImporterType}: Found DataInbox {DataInboxId}, created at {CreatedAt}",
+ ImporterType, dataInbox.Id, dataInbox.CreatedAt);
+
+ try
{
- EndDate = endDate;
+ var data = Convert.FromBase64String(dataInbox.Data);
+ var jsonString = Encoding.UTF8.GetString(data);
+
+ _logger.LogDebug("{ImporterType}: Decoded {DataSize} bytes from base64",
+ ImporterType, data.Length);
+
+ var records = JsonSerializer.Deserialize>(jsonString);
+ if (records == null)
+ {
+ throw new InvalidOperationException($"DataInbox.Data is empty for: {dataInbox.Name}");
+ }
+
+ _logger.LogDebug("{ImporterType}: Deserialized {RecordCount} records from JSON",
+ ImporterType, records.Count);
+
+ // Cache the deserialized data
+ _cachedRecords = records;
+ _cachedDataInboxId = dataInbox.Id.ToString();
+ }
+ catch (FormatException e)
+ {
+ _logger.LogError(e, "{ImporterType}: Invalid base64 data in DataInbox {DataInboxName}",
+ ImporterType, ImportType);
+ throw new InvalidOperationException($"Invalid base64 data in DataInbox: {ImportType}", e);
+ }
+ catch (JsonException e)
+ {
+ _logger.LogError(e, "{ImporterType}: Invalid JSON data in DataInbox {DataInboxName}",
+ ImporterType, ImportType);
+ throw new InvalidOperationException($"Invalid JSON data in DataInbox: {ImportType}", e);
+ }
+ }
+
+ private List GetFilteredRecords()
+ {
+ if (_cachedRecords == null)
+ {
+ throw new InvalidOperationException("Data has not been deserialized yet");
}
- _logger.LogDebug("{ImporterType}: Configuration loaded for {ImportWorkerName} - Type: {ImportType}",
- ImporterType, importWorker.Name, ImportType);
+ var filteredRecords = _cachedRecords.Where(x => x.Code!.StartsWith($"{ImportYear}{ImportMonth}")).ToList();
+ if (filteredRecords.Count == 0)
+ {
+ throw new InvalidOperationException($"No records found for period: {ImportYear}{ImportMonth}");
+ }
+
+ _logger.LogDebug("{ImporterType}: Filtered to {FilteredCount} records for period {Year}{Month}",
+ ImporterType, filteredRecords.Count, ImportYear, ImportMonth);
+
+ return filteredRecords;
}
private bool ShouldPerformImport(Layer importWorker)
{
if (!IsEnabled)
{
- _logger.LogDebug("{ImporterType}: Import disabled for {ImportWorkerName}",
+ _logger.LogDebug("{ImporterType}: Import disabled for {ImportWorkerName}",
ImporterType, importWorker.Name);
return false;
}
- if (StartDate.HasValue && EndDate.HasValue)
+ var dataInbox = _db.DataInbox.OrderByDescending(x => x.CreatedAt).FirstOrDefault(x => x.Name == ImportType);
+ if (dataInbox == null)
{
- var now = DateTime.UtcNow.Date;
- if (now >= StartDate.Value.Date && now <= EndDate.Value.Date)
- {
- _logger.LogDebug("{ImporterType}: Within date range, import needed for {ImportWorkerName}",
- ImporterType, importWorker.Name);
- return true;
- }
-
- if (!IsImportedLayerUpToDate(importWorker))
- {
- _logger.LogDebug("{ImporterType}: Outside date range but layer is out of date, import needed for {ImportWorkerName}",
- ImporterType, importWorker.Name);
- return true;
- }
-
- _logger.LogDebug("{ImporterType}: Outside date range and layer is up to date for {ImportWorkerName}",
- ImporterType, importWorker.Name);
+ _logger.LogDebug("{ImporterType}: No DataInbox found for type {ImportType}",
+ ImporterType, ImportType);
return false;
}
+
+ // Check if imported layer is up to date
+ if (!IsImportedLayerUpToDate(importWorker))
+ {
+ _logger.LogDebug("{ImporterType}: Layer is out of date, import needed for {ImportWorkerName}",
+ ImporterType, importWorker.Name);
+ return true;
+ }
- return true;
+ _logger.LogDebug("{ImporterType}: Layer is up to date for {ImportWorkerName}",
+ ImporterType, importWorker.Name);
+ return false;
}
private void ValidateConfiguration()
@@ -131,6 +207,7 @@ public class MorskaD3Importer : MorskaBaseImporter
if (string.IsNullOrEmpty(ImportMonth)) errors.Add("ImportMonth is required");
if (string.IsNullOrEmpty(ImportName)) errors.Add("ImportName is required");
if (string.IsNullOrEmpty(ImportType)) errors.Add("ImportType is required");
+ if (string.IsNullOrEmpty(SheetId)) errors.Add("SheetId is required");
if (errors.Any())
{
@@ -142,116 +219,182 @@ public class MorskaD3Importer : MorskaBaseImporter
{
var newestLayer = _db.Layers
.Include(x => x.Records)
- .Where(x => x.ParentId == importWorker.Id)
+ .Where(x =>
+ x.ParentId == importWorker.Id
+ && x.Name!.Contains($"I-{ImportName}-{ImportYear}/{ImportMonth}"))
.OrderByDescending(x => x.CreatedAt)
.AsNoTracking()
.FirstOrDefault();
if (newestLayer == null)
{
- _logger.LogDebug("{ImporterType}: No child layers found for {ImportWorkerName}, treating as up to date",
+ _logger.LogDebug("{ImporterType}: No child layers found for {ImportWorkerName}, import needed",
ImporterType, importWorker.Name);
- return true;
+ return false;
}
try
{
- var dataInbox = _db.DataInbox.OrderByDescending(x => x.CreatedAt).FirstOrDefault(x => x.Name == ImportType);
- if (dataInbox == null)
+ var currentRecords = GetFilteredRecords();
+
+ // Compare record counts first
+ if (newestLayer.Records?.Count != currentRecords.Count)
{
- _logger.LogWarning("{ImporterType}: No DataInbox found for type {ImportType}",
- ImporterType, ImportType);
- return true; // Assume up to date if no data source
+ _logger.LogDebug("{ImporterType}: Record count mismatch - DB: {DbCount}, DataInbox: {DataCount}",
+ ImporterType, newestLayer.Records?.Count ?? 0, currentRecords.Count);
+ return false;
}
- // Compare timestamps - if DataInbox is newer than our layer, we need to import
- var isUpToDate = newestLayer.CreatedAt >= dataInbox.CreatedAt;
-
- _logger.LogDebug("{ImporterType}: Layer created at {LayerTime}, DataInbox created at {DataTime}, up to date: {IsUpToDate}",
- ImporterType, newestLayer.CreatedAt, dataInbox.CreatedAt, isUpToDate);
+ // Compare individual records
+ foreach (var currentRecord in currentRecords)
+ {
+ var existingRecord = newestLayer.Records?.FirstOrDefault(x => x.Code == currentRecord.Code);
+ if (existingRecord == null)
+ {
+ _logger.LogDebug("{ImporterType}: Record with code {Code} not found in existing layer",
+ ImporterType, currentRecord.Code);
+ return false;
+ }
- return isUpToDate;
+ // Compare all relevant fields
+ if (Math.Abs((existingRecord.Value1 ?? 0) - (currentRecord.Value1 ?? 0)) > 0.001 ||
+ existingRecord.Desc1 != currentRecord.Desc1)
+ {
+ _logger.LogDebug("{ImporterType}: Data difference found for code {Code}",
+ ImporterType, currentRecord.Code);
+ return false;
+ }
+ }
+
+ _logger.LogDebug("{ImporterType}: All records match, layer is up to date for {ImportWorkerName}",
+ ImporterType, importWorker.Name);
+ return true;
}
catch (Exception e)
{
- _logger.LogError(e, "{ImporterType}: Error checking if layer {ImportWorkerName} is up to date",
+ _logger.LogError(e, "{ImporterType}: Error checking if layer {ImportWorkerName} is up to date",
ImporterType, importWorker.Name);
- throw;
+ return false;
}
}
private void PerformImport(Layer importWorker)
{
- _logger.LogDebug("{ImporterType}: Looking for DataInbox with type {ImportType}",
+ _logger.LogDebug("{ImporterType}: Starting import for DataInbox type {ImportType}",
ImporterType, ImportType);
- var dataInbox = _db.DataInbox.OrderByDescending(x => x.CreatedAt).FirstOrDefault(x => x.Name == ImportType);
- if (dataInbox == null)
- {
- throw new InvalidOperationException($"DataInbox not found for type: {ImportType}");
- }
+ var filteredRecords = GetFilteredRecords();
- _logger.LogDebug("{ImporterType}: Found DataInbox {DataInboxId}, created at {CreatedAt}",
- ImporterType, dataInbox.Id, dataInbox.CreatedAt);
+ // Prepare records for saving
+ var recordsToSave = filteredRecords.Select(x =>
+ {
+ x.Id = Guid.NewGuid();
+ x.CreatedAt = DateTime.UtcNow;
+ x.ModifiedAt = DateTime.UtcNow;
+ return x;
+ }).ToList();
+
+ var layer = new Layer
+ {
+ Id = Guid.NewGuid(),
+ Number = _db.Layers.Count() + 1,
+ ParentId = importWorker.Id,
+ Type = LayerType.Import,
+ CreatedById = Guid.Parse("F392209E-123E-4651-A5A4-0B1D6CF9FF9D"),
+ ModifiedById = Guid.Parse("F392209E-123E-4651-A5A4-0B1D6CF9FF9D"),
+ CreatedAt = DateTime.UtcNow,
+ ModifiedAt = DateTime.UtcNow
+ };
+ layer.Name = $"L{layer.Number}-I-{ImportName}-{ImportYear}/{ImportMonth}-{DateTime.Now:yyyyMMddHHmm}";
+
+ _db.Layers.Add(layer);
+ SaveRecords(layer.Id, recordsToSave);
+ _db.SaveChanges();
+
+ _logger.LogInformation(
+ "{ImporterType}: Successfully imported {RecordCount} records for layer {LayerName} ({LayerId})",
+ ImporterType, recordsToSave.Count, layer.Name, layer.Id);
+ }
+
+ private void ExportToGoogleSheets()
+ {
+ if (string.IsNullOrEmpty(SheetId))
+ {
+ _logger.LogWarning("{ImporterType}: SheetId not configured, skipping Google Sheets export",
+ ImporterType);
+ return;
+ }
try
{
- var data = Convert.FromBase64String(dataInbox.Data);
- var jsonString = Encoding.UTF8.GetString(data);
-
- _logger.LogDebug("{ImporterType}: Decoded {DataSize} bytes from base64",
- ImporterType, data.Length);
+ _logger.LogDebug("{ImporterType}: Starting Google Sheets export to {SheetId}",
+ ImporterType, SheetId);
- var records = JsonSerializer.Deserialize>(jsonString);
- if (records == null)
+ var filteredRecords = GetFilteredRecords();
+ var sheetTabName = ProcessHelper.GetSheetName(int.Parse(ImportMonth!), int.Parse(ImportYear!));
+
+ _logger.LogDebug("{ImporterType}: Using sheet tab name: {SheetTabName}",
+ ImporterType, sheetTabName);
+
+ // Get current sheet data
+ var currentSheetData = _googleSheetValues.Get(SheetId!, $"{sheetTabName}!C7:D200").Execute();
+
+ var updateRequests = new List();
+
+ for (var rowIndex = 0; rowIndex < 194; rowIndex++)
{
- throw new InvalidOperationException($"DataInbox.Data is empty for: {dataInbox.Name}");
+ if (currentSheetData.Values == null || rowIndex >= currentSheetData.Values.Count)
+ continue;
+
+ var existingRow = currentSheetData.Values[rowIndex];
+ if (existingRow.Count == 0 || string.IsNullOrEmpty(existingRow[0]?.ToString()))
+ continue;
+
+ var accountCode = existingRow[0].ToString()!;
+ var matchingRecord = filteredRecords.FirstOrDefault(x => x.Desc1 == accountCode);
+ if (matchingRecord == null)
+ continue;
+
+ var newValue = matchingRecord.Value1?.ToString(CultureInfo.GetCultureInfo("pl-PL")) ?? "0";
+ var existingValue = (existingRow.Count > 1 ? existingRow[1] : "")?.ToString();
+
+ if (existingValue != newValue)
+ {
+ var range = $"{sheetTabName}!D{rowIndex + 7}";
+ updateRequests.Add(new ValueRange
+ {
+ Range = range,
+ Values = new List> { new List