feat(expense): add expense category entities + seed (11 groups / 38 subs)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,8 @@ public class AppDbContext : IdentityDbContext<AppUser, AppRole, string>
|
|||||||
public DbSet<OfferingSession> OfferingSessions => Set<OfferingSession>();
|
public DbSet<OfferingSession> OfferingSessions => Set<OfferingSession>();
|
||||||
public DbSet<Giving> Givings => Set<Giving>();
|
public DbSet<Giving> Givings => Set<Giving>();
|
||||||
public DbSet<Ministry> Ministries => Set<Ministry>();
|
public DbSet<Ministry> Ministries => Set<Ministry>();
|
||||||
|
public DbSet<ExpenseCategoryGroup> ExpenseCategoryGroups => Set<ExpenseCategoryGroup>();
|
||||||
|
public DbSet<ExpenseSubCategory> ExpenseSubCategories => Set<ExpenseSubCategory>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder builder)
|
protected override void OnModelCreating(ModelBuilder builder)
|
||||||
{
|
{
|
||||||
@@ -150,5 +152,25 @@ public class AppDbContext : IdentityDbContext<AppUser, AppRole, string>
|
|||||||
entity.Property(e => e.Name_en).HasMaxLength(200).IsRequired();
|
entity.Property(e => e.Name_en).HasMaxLength(200).IsRequired();
|
||||||
entity.Property(e => e.Name_zh).HasMaxLength(200);
|
entity.Property(e => e.Name_zh).HasMaxLength(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── ExpenseCategoryGroup ─────────────────────────────────────────────
|
||||||
|
builder.Entity<ExpenseCategoryGroup>(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<ExpenseSubCategory>(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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,22 @@ public static class DbSeeder
|
|||||||
("Catering", "餐飲", 10),
|
("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 =
|
private static readonly (string Name, string Description)[] Roles =
|
||||||
[
|
[
|
||||||
("super_admin", "System administrator — full access"),
|
("super_admin", "System administrator — full access"),
|
||||||
@@ -90,6 +106,30 @@ public static class DbSeeder
|
|||||||
await db.SaveChangesAsync();
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Seeds roles and (in Development) the default admin account.
|
/// Seeds roles and (in Development) the default admin account.
|
||||||
/// Called once on application startup after migrations have been applied.
|
/// Called once on application startup after migrations have been applied.
|
||||||
@@ -105,6 +145,7 @@ public static class DbSeeder
|
|||||||
var db = services.GetRequiredService<AppDbContext>();
|
var db = services.GetRequiredService<AppDbContext>();
|
||||||
await SeedGivingCategoriesAsync(db);
|
await SeedGivingCategoriesAsync(db);
|
||||||
await SeedMinistriesAsync(db);
|
await SeedMinistriesAsync(db);
|
||||||
|
await SeedExpenseCategoriesAsync(db);
|
||||||
|
|
||||||
if (env.IsDevelopment())
|
if (env.IsDevelopment())
|
||||||
await SeedAdminUserAsync(userManager);
|
await SeedAdminUserAsync(userManager);
|
||||||
|
|||||||
@@ -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<ExpenseSubCategory> SubCategories { get; set; } = [];
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user