From b3eb9d297ad2a17e7c98c69b5c6bec33e798b5a8 Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Fri, 29 May 2026 18:08:12 -0700 Subject: [PATCH] feat(expense): add expense category entities + seed (11 groups / 38 subs) Co-Authored-By: Claude Sonnet 4.6 --- API/ROLAC.API/Data/AppDbContext.cs | 22 ++++++++++ API/ROLAC.API/Data/DbSeeder.cs | 41 +++++++++++++++++++ .../Entities/ExpenseCategoryGroup.cs | 13 ++++++ API/ROLAC.API/Entities/ExpenseSubCategory.cs | 14 +++++++ 4 files changed, 90 insertions(+) create mode 100644 API/ROLAC.API/Entities/ExpenseCategoryGroup.cs create mode 100644 API/ROLAC.API/Entities/ExpenseSubCategory.cs diff --git a/API/ROLAC.API/Data/AppDbContext.cs b/API/ROLAC.API/Data/AppDbContext.cs index b070ac0..ea6a837 100644 --- a/API/ROLAC.API/Data/AppDbContext.cs +++ b/API/ROLAC.API/Data/AppDbContext.cs @@ -15,6 +15,8 @@ public class AppDbContext : IdentityDbContext public DbSet OfferingSessions => Set(); public DbSet Givings => Set(); public DbSet Ministries => Set(); + public DbSet ExpenseCategoryGroups => Set(); + public DbSet ExpenseSubCategories => Set(); protected override void OnModelCreating(ModelBuilder builder) { @@ -150,5 +152,25 @@ public class AppDbContext : IdentityDbContext entity.Property(e => e.Name_en).HasMaxLength(200).IsRequired(); entity.Property(e => e.Name_zh).HasMaxLength(200); }); + + // ── ExpenseCategoryGroup ───────────────────────────────────────────── + builder.Entity(entity => + { + entity.Property(e => e.Name_en).HasMaxLength(200).IsRequired(); + entity.Property(e => e.Name_zh).HasMaxLength(200); + entity.Property(e => e.CreatedBy).HasMaxLength(450); + entity.Property(e => e.UpdatedBy).HasMaxLength(450); + }); + + // ── ExpenseSubCategory ─────────────────────────────────────────────── + builder.Entity(entity => + { + entity.Property(e => e.Name_en).HasMaxLength(200).IsRequired(); + entity.Property(e => e.Name_zh).HasMaxLength(200); + entity.Property(e => e.CreatedBy).HasMaxLength(450); + entity.Property(e => e.UpdatedBy).HasMaxLength(450); + entity.HasOne(e => e.Group).WithMany(g => g.SubCategories) + .HasForeignKey(e => e.GroupId).OnDelete(DeleteBehavior.Restrict); + }); } } diff --git a/API/ROLAC.API/Data/DbSeeder.cs b/API/ROLAC.API/Data/DbSeeder.cs index 2924010..c8ecaa1 100644 --- a/API/ROLAC.API/Data/DbSeeder.cs +++ b/API/ROLAC.API/Data/DbSeeder.cs @@ -29,6 +29,22 @@ public static class DbSeeder ("Catering", "餐飲", 10), ]; + // (GroupEn, GroupZh, Sort, SubItems[(SubEn, SubZh)]) + private static readonly (string En, string Zh, int Sort, (string En, string Zh)[] Subs)[] ExpenseCategorySeed = + [ + ("Equipment", "設備", 1, [("Purchase","購置"),("Rental","租借"),("Maintenance & Repair","維修")]), + ("Consumables", "消耗品", 2, [("Batteries","電池"),("Accessories","配件"),("Cleaning Supplies","清潔用品"),("Office Supplies","文具")]), + ("Food & Beverage", "餐飲", 3, [("Catering","出餐費用"),("Food Ingredients","食材採購"),("Utensils","器具"),("Consumables","消耗品")]), + ("Training", "培訓", 4, [("Course Fees","課程費用"),("Books","書籍"),("Conference","研討會"),("Travel","差旅")]), + ("Materials", "教材", 5, [("Printing","印刷費用"),("Craft Supplies","手工材料"),("Copyright & Licensing","版權購買")]), + ("Facility", "場地", 6, [("Rent","場地租金"),("Utilities","水電"),("Property Insurance","財產保險"),("Decoration","裝飾")]), + ("Printing", "印刷", 7, [("Bulletins","週報"),("Order of Service","程序單"),("Posters","海報")]), + ("Missions", "宣教", 8, [("Offering Transfer","奉獻轉帳"),("Missionary Support","宣教士支援"),("Travel","差旅")]), + ("Benevolence", "關懷救助", 9, [("Emergency Aid","急難救助"),("Condolence Gifts","慰問禮品"),("Visit Expenses","探訪費用")]), + ("Other", "其他", 10, [("Miscellaneous","雜支")]), + ("Personnel", "人事", 11, [("Salary & Wages","薪資"),("Payroll Taxes","薪資稅費"),("Employee Benefits","員工福利"),("Workers Compensation","勞工保險"),("Honorarium","酬庸"),("Staff Training","同工進修"),("Contract Labor","外包勞務")]), + ]; + private static readonly (string Name, string Description)[] Roles = [ ("super_admin", "System administrator — full access"), @@ -90,6 +106,30 @@ public static class DbSeeder await db.SaveChangesAsync(); } + public static async Task SeedExpenseCategoriesAsync(AppDbContext db) + { + foreach (var (gEn, gZh, gSort, subs) in ExpenseCategorySeed) + { + var group = await db.ExpenseCategoryGroups.FirstOrDefaultAsync(g => g.Name_en == gEn); + if (group is null) + { + group = new ExpenseCategoryGroup { Name_en = gEn, Name_zh = gZh, SortOrder = gSort, IsActive = true }; + db.ExpenseCategoryGroups.Add(group); + await db.SaveChangesAsync(); // assign group.Id + } + + var sub = 1; + foreach (var (sEn, sZh) in subs) + { + if (!await db.ExpenseSubCategories.AnyAsync(s => s.GroupId == group.Id && s.Name_en == sEn)) + db.ExpenseSubCategories.Add(new ExpenseSubCategory + { GroupId = group.Id, Name_en = sEn, Name_zh = sZh, SortOrder = sub, IsActive = true }); + sub++; + } + } + await db.SaveChangesAsync(); + } + /// /// Seeds roles and (in Development) the default admin account. /// Called once on application startup after migrations have been applied. @@ -105,6 +145,7 @@ public static class DbSeeder var db = services.GetRequiredService(); await SeedGivingCategoriesAsync(db); await SeedMinistriesAsync(db); + await SeedExpenseCategoriesAsync(db); if (env.IsDevelopment()) await SeedAdminUserAsync(userManager); diff --git a/API/ROLAC.API/Entities/ExpenseCategoryGroup.cs b/API/ROLAC.API/Entities/ExpenseCategoryGroup.cs new file mode 100644 index 0000000..125ab1e --- /dev/null +++ b/API/ROLAC.API/Entities/ExpenseCategoryGroup.cs @@ -0,0 +1,13 @@ +using ROLAC.API.Entities.Base; +namespace ROLAC.API.Entities; + +public class ExpenseCategoryGroup : AuditableEntity +{ + public int Id { get; set; } + public string Name_en { get; set; } = null!; + public string? Name_zh { get; set; } + public int SortOrder { get; set; } + public bool IsActive { get; set; } = true; + + public List SubCategories { get; set; } = []; +} diff --git a/API/ROLAC.API/Entities/ExpenseSubCategory.cs b/API/ROLAC.API/Entities/ExpenseSubCategory.cs new file mode 100644 index 0000000..ea289ab --- /dev/null +++ b/API/ROLAC.API/Entities/ExpenseSubCategory.cs @@ -0,0 +1,14 @@ +using ROLAC.API.Entities.Base; +namespace ROLAC.API.Entities; + +public class ExpenseSubCategory : AuditableEntity +{ + public int Id { get; set; } + public int GroupId { get; set; } + public string Name_en { get; set; } = null!; + public string? Name_zh { get; set; } + public int SortOrder { get; set; } + public bool IsActive { get; set; } = true; + + public ExpenseCategoryGroup? Group { get; set; } +}