[CLAUDE] Domain+App+Infra+Api+FE-Admin+FE-User: S36 Plan G-O2 Phòng họp Mig 36 + BE CRUD + FE 2 app
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m55s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m55s
Phase 10.2 G-O2 Phòng họp BookingCalendar — Mig 36 schema + BE CQRS + FE 2 app mirror cookie-cutter G-H2 HrmConfig pattern. Standalone không depend workflow. BE schema (Mig 36 — em main solo Step 4): - 4 Domain new: MeetingRoom catalog + MeetingBooking header + MeetingBookingAttendee join table N-to-N (NOT JSON per Investigator verdict) + Enums (MeetingBookingStatus 3-state: Confirmed/Cancelled/Completed) - 3 EF Config: UNIQUE Code + composite index (RoomId, StartAt) range query + UNIQUE composite (BookingId, UserId) join - FK strategy: Room→Restrict (preserve history) + Booking→Cascade attendees + User→Restrict (denorm FullName+Email tránh cascade wipe) - Mig 36 3-file rule + ApplicationDbContextModelSnapshot updated + apply Dev+Design DB BE CQRS (~584 LOC — Implementer Case 2): - MeetingFeatures.cs 479 LOC 9 handler: 4 Room CRUD + 5 Booking (List + GetById + Create + Update + Cancel) - SERIALIZABLE transaction overlap check via EXISTS query — throw 409 Conflict "Phòng đã được đặt trong khoảng thời gian này" - MeetingRoomsController 49 LOC + MeetingBookingsController 56 LOC — class-level [Authorize] + Roles="Admin" for write - Application.csproj +Microsoft.EntityFrameworkCore.Relational package (em main fix IsolationLevel overload — Implementer gotcha #53 4th truncation diagnose mid-task) - MenuKeys.cs +4 const (Off_PhongHop sub-group + View/Manage/Book leaf) - DbInitializer +SeedMeetingRoomsAsync 4 sample (PH-A Phòng họp lớn cap=20 + PH-B cap=8 + PHG-501 Giám đốc cap=6 + ONL-1 Online Zoom cap=50) — NOT gated DemoSeed per gotcha #51 INFRASTRUCTURE seed FE 2 app (~1770 LOC × 2 — Implementer Case 2): - types/meeting.ts × 2 SHA256 IDENTICAL (ce0ad9c6d017cde2) — DTO interface mirror - MeetingCalendarPage.tsx × 2 SHA256 IDENTICAL (d6d160ae1e4f2285) ~530 LOC — custom HTML 7-day grid 8h-20h slot, NO FullCalendar dep (~80 KB bundle saved per Investigator verdict alternative) - MeetingRoomsPage.tsx × 2 SHA256 IDENTICAL (ba35a7ef379a5e9c) ~270 LOC — admin catalog CRUD table + Dialog - 4-place mirror Pattern 16-bis 7× cumulative: types + page + App.tsx route + menuKeys + Layout staticMap 3 entry (gotcha #50 silent sidebar drop prevention) Verify: - dotnet build SolutionErp.slnx PASS 0 error 2 pre-existing DocxRenderer warning - dotnet test 130/130 PASS baseline preserve (58 Domain + 72 Infra) - npm build × 2 app PASS 0 TS error (fe-admin 16.91s bundle 1490 KB / fe-user 8.56s bundle 1404 KB, +23 KB gzip both) Pattern reinforced cumulative S36: - Pattern 12-bis cross-module mirror 10× (PE → Contract V2 → Hrm → Office) - Pattern 16-bis 4-place mirror cross-app 7× - Smart Friend Implementer truncation gotcha #53 4th — mitigation tight brief WORK (FE 2 app no truncation, BE truncate diagnose mid only) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
5170
src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260528074125_AddMeetingRooms.Designer.cs
generated
Normal file
5170
src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/20260528074125_AddMeetingRooms.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddMeetingRooms : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MeetingRooms",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Code = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
Capacity = table.Column<int>(type: "int", nullable: false),
|
||||
Location = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||
Equipment = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false),
|
||||
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),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
DeletedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MeetingRooms", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MeetingBookings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
RoomId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
BookedByUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
BookedByFullName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
StartAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
EndAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
|
||||
Status = table.Column<int>(type: "int", nullable: false),
|
||||
Note = table.Column<string>(type: "nvarchar(500)", maxLength: 500, 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),
|
||||
IsDeleted = table.Column<bool>(type: "bit", nullable: false),
|
||||
DeletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
DeletedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_MeetingBookings", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_MeetingBookings_MeetingRooms_RoomId",
|
||||
column: x => x.RoomId,
|
||||
principalTable: "MeetingRooms",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MeetingBookingAttendees",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
BookingId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
FullName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||
Email = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||
Notes = table.Column<string>(type: "nvarchar(200)", maxLength: 200, 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_MeetingBookingAttendees", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_MeetingBookingAttendees_MeetingBookings_BookingId",
|
||||
column: x => x.BookingId,
|
||||
principalTable: "MeetingBookings",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MeetingBookingAttendees_BookingId_UserId",
|
||||
table: "MeetingBookingAttendees",
|
||||
columns: new[] { "BookingId", "UserId" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MeetingBookingAttendees_UserId",
|
||||
table: "MeetingBookingAttendees",
|
||||
column: "UserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MeetingBookings_BookedByUserId",
|
||||
table: "MeetingBookings",
|
||||
column: "BookedByUserId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MeetingBookings_RoomId_StartAt",
|
||||
table: "MeetingBookings",
|
||||
columns: new[] { "RoomId", "StartAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MeetingRooms_Code",
|
||||
table: "MeetingRooms",
|
||||
column: "Code",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "MeetingBookingAttendees");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MeetingBookings");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MeetingRooms");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3448,6 +3448,181 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("Notifications", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Office.MeetingBooking", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("BookedByFullName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<Guid>("BookedByUserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("CreatedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("DeletedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("nvarchar(1000)");
|
||||
|
||||
b.Property<DateTime>("EndAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Note")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<Guid>("RoomId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("StartAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BookedByUserId");
|
||||
|
||||
b.HasIndex("RoomId", "StartAt");
|
||||
|
||||
b.ToTable("MeetingBookings", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Office.MeetingBookingAttendee", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("BookingId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("CreatedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("FullName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.HasIndex("BookingId", "UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MeetingBookingAttendees", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Office.MeetingRoom", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<int>("Capacity")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("CreatedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("DeletedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<string>("Equipment")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsDeleted")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid?>("UpdatedBy")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("MeetingRooms", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@ -4690,6 +4865,28 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Office.MeetingBooking", b =>
|
||||
{
|
||||
b.HasOne("SolutionErp.Domain.Office.MeetingRoom", "Room")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoomId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Room");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Office.MeetingBookingAttendee", b =>
|
||||
{
|
||||
b.HasOne("SolutionErp.Domain.Office.MeetingBooking", "Booking")
|
||||
.WithMany("Attendees")
|
||||
.HasForeignKey("BookingId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Booking");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", b =>
|
||||
{
|
||||
b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", null)
|
||||
@ -4924,6 +5121,11 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("Permissions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.Office.MeetingBooking", b =>
|
||||
{
|
||||
b.Navigation("Attendees");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("SolutionErp.Domain.PurchaseEvaluations.PurchaseEvaluation", b =>
|
||||
{
|
||||
b.Navigation("Approvals");
|
||||
|
||||
Reference in New Issue
Block a user