[CLAUDE] App+Domain+Infra+Api+FE: Notifications module end-to-end
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m43s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m43s
Domain: - Notification entity + NotificationType enum (stable ints) - Nullable RefId cho correlation (contract, user, ...) Infrastructure: - NotificationConfiguration: bảng Notifications, index theo (UserId, ReadAt) - NotificationService: ghi vào DbContext, không SaveChanges (để caller quyết định unit-of-work — đảm bảo atomic với domain mutation) - EF migration AddNotifications Application: - INotificationService (Notify + NotifyMany) - CQRS: ListMyNotifications / GetMyUnreadCount / MarkRead / MarkAllRead Api: - NotificationsController: GET /api/notifications + unread-count + mark-read Integration: - ContractWorkflowService emit notification tới Drafter khi HĐ chuyển phase (skip nếu actor chính là Drafter). Title + type theo phase đích: DaPhatHanh → ContractPublished, TuChoi → ContractRejected, khác → ContractPhaseTransition. FE: - Both NotificationBell (admin + user) dùng /api/notifications thật (thay cho derived-from-inbox MVP trước đó). 30s refetch, click mark-read, 'Đọc hết' bulk action. Foundation sẵn cho SignalR push + email outbox sau này — chỉ cần mở rộng NotificationService mà không đổi caller. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -5,6 +5,7 @@ using SolutionErp.Domain.Contracts;
|
||||
using SolutionErp.Domain.Forms;
|
||||
using SolutionErp.Domain.Identity;
|
||||
using SolutionErp.Domain.Master;
|
||||
using SolutionErp.Domain.Notifications;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence;
|
||||
|
||||
@ -25,6 +26,7 @@ public class ApplicationDbContext
|
||||
public DbSet<ContractComment> ContractComments => Set<ContractComment>();
|
||||
public DbSet<ContractAttachment> ContractAttachments => Set<ContractAttachment>();
|
||||
public DbSet<ContractCodeSequence> ContractCodeSequences => Set<ContractCodeSequence>();
|
||||
public DbSet<Notification> Notifications => Set<Notification>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using SolutionErp.Domain.Notifications;
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Configurations;
|
||||
|
||||
public class NotificationConfiguration : IEntityTypeConfiguration<Notification>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Notification> e)
|
||||
{
|
||||
e.ToTable("Notifications");
|
||||
e.HasKey(x => x.Id);
|
||||
e.Property(x => x.Type).HasConversion<int>();
|
||||
e.Property(x => x.Title).HasMaxLength(300).IsRequired();
|
||||
e.Property(x => x.Description).HasMaxLength(1000);
|
||||
e.Property(x => x.Href).HasMaxLength(500);
|
||||
|
||||
e.HasIndex(x => new { x.UserId, x.ReadAt });
|
||||
e.HasIndex(x => x.CreatedAt);
|
||||
}
|
||||
}
|
||||
1068
src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260421082148_AddNotifications.Designer.cs
generated
Normal file
1068
src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260421082148_AddNotifications.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddNotifications : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Notifications",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Type = table.Column<int>(type: "int", nullable: false),
|
||||
Title = table.Column<string>(type: "nvarchar(300)", maxLength: 300, nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
|
||||
Href = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
ReadAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
RefId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Notifications", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Notifications_CreatedAt",
|
||||
table: "Notifications",
|
||||
column: "CreatedAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Notifications_UserId_ReadAt",
|
||||
table: "Notifications",
|
||||
columns: new[] { "UserId", "ReadAt" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Notifications");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -879,6 +879,58 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("Suppliers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Notifications.Notification", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("CreatedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<string>("Href")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<DateTime?>("ReadAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("RefId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("nvarchar(300)");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.HasIndex("UserId", "ReadAt");
|
||||
|
||||
b.ToTable("Notifications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("SolutionErp.Domain.Identity.Role", null)
|
||||
|
||||
Reference in New Issue
Block a user