Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions client/src/shared/approvals/approvalDisplay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,6 @@ export function formatCompactHours(hours: number) {

export function getLeaveTone(leaveType: LeaveCategory): Tone {
switch (leaveType) {
case 'Compensatory Time':
return {
background: 'bg-slate-100',
border: 'border-slate-500',
legend: 'border-slate-500 bg-slate-100',
text: 'text-slate-700',
};
case 'Sick Leave':
return {
background: 'bg-emerald-100',
Expand Down
1 change: 0 additions & 1 deletion client/src/shared/approvals/approvalTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export type CalendarLeave = {
};

export type LeaveCategory =
| 'Compensatory Time'
| 'FMLA'
| 'Professional Development'
| 'Sabbatical'
Expand Down
94 changes: 57 additions & 37 deletions server.core/Data/DbInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,13 @@ public class DbInitializer : IDbInitializer
new(DevelopmentSeedData.TestClusterName, DevelopmentSeedData.LocalCaoIamId, "2026-08-21", null, "adminherd", "2026-08-21T08:50:00", null, null),
];

private static readonly LeaveTypeSeed[] DevLeaveTypes =
private static readonly LeaveTypeSeed[] RequiredLeaveTypes =
[
new("Vacation", 10, "Vacation", true, true),
new("Sick", 20, "Sick Leave", true, true),
new("ProfessionalDevelopment", null, "Professional Development", false, true),
new("FamilyCare", 30, "FMLA", false, true),
new("Sabbatical", 40, "Sabbatical", false, true),
new("CompTime", 50, "Compensatory Time", true, true),
];

private static readonly EmployeeAccrualBalanceSeed[] DevEmployeeAccrualBalances =
Expand Down Expand Up @@ -177,6 +176,8 @@ public async Task InitializeAsync(bool includeDevSeed = false, CancellationToken
_logger.LogInformation("Database ensured.");
}

await EnsureRequiredLeaveTypesAsync(cancellationToken);

if (includeDevSeed)
{
await SeedDevelopmentAsync(cancellationToken);
Expand All @@ -201,7 +202,6 @@ private async Task SeedDevelopmentAsync(CancellationToken ct)
await SeedDepartmentChairAssignmentsAsync(usersByIamId, nowUtc, ct);
await SeedClusterCaoAssignmentsAsync(usersByIamId, clustersByName, nowUtc, ct);
await SeedDepartmentEmailRoutingsAsync(usersByIamId, nowUtc, ct);
await SeedLeaveTypesAsync(ct);
await SeedEmployeeAccrualBalancesAsync(ct);

var leaveTypesByKey = await LoadLeaveTypesByKeyAsync(ct);
Expand Down Expand Up @@ -637,7 +637,13 @@ private async Task SeedDepartmentEmailRoutingsAsync(
_logger.LogInformation("Seeded {Count} development DepartmentEmailRouting rows.", missingRoutings.Length);
}

private async Task SeedLeaveTypesAsync(CancellationToken ct)
private Task EnsureRequiredLeaveTypesAsync(CancellationToken ct) =>
EnsureLeaveTypesAsync(RequiredLeaveTypes, "required", ct);

private async Task EnsureLeaveTypesAsync(
IReadOnlyCollection<LeaveTypeSeed> leaveTypeSeeds,
string seedName,
CancellationToken ct)
{
var existingLeaveTypes = await _db.LeaveTypes
.ToListAsync(ct);
Expand All @@ -646,47 +652,22 @@ private async Task SeedLeaveTypesAsync(CancellationToken ct)
leaveType => leaveType.LeaveTypeKey,
StringComparer.OrdinalIgnoreCase);

var updated = false;
var updatedCount = 0;

foreach (var seed in DevLeaveTypes)
foreach (var seed in leaveTypeSeeds)
{
if (!existingByKey.TryGetValue(seed.LeaveTypeKey, out var existingLeaveType))
{
continue;
}

if (existingLeaveType.SourceLeaveTypeNumber != seed.SourceLeaveTypeNumber)
{
existingLeaveType.SourceLeaveTypeNumber = seed.SourceLeaveTypeNumber;
updated = true;
}

if (!string.Equals(existingLeaveType.DisplayName, seed.DisplayName, StringComparison.Ordinal))
{
existingLeaveType.DisplayName = seed.DisplayName;
updated = true;
}

if (existingLeaveType.HasAccrualBalance != seed.HasAccrualBalance)
if (ApplyLeaveTypeSeed(existingLeaveType, seed))
{
existingLeaveType.HasAccrualBalance = seed.HasAccrualBalance;
updated = true;
}

if (existingLeaveType.IsActive != seed.IsActive)
{
existingLeaveType.IsActive = seed.IsActive;
updated = true;
updatedCount++;
}
}

if (updated)
{
await _db.SaveChangesAsync(ct);
_logger.LogInformation("Updated existing development LeaveType rows.");
}

var missingLeaveTypes = DevLeaveTypes
var missingLeaveTypes = leaveTypeSeeds
.Where(leaveType => !existingByKey.ContainsKey(leaveType.LeaveTypeKey))
.Select(leaveType => new LeaveType
{
Expand All @@ -698,14 +679,22 @@ private async Task SeedLeaveTypesAsync(CancellationToken ct)
})
.ToArray();

if (missingLeaveTypes.Length == 0)
if (missingLeaveTypes.Length == 0 && updatedCount == 0)
{
return;
}

await _db.LeaveTypes.AddRangeAsync(missingLeaveTypes, ct);
if (missingLeaveTypes.Length > 0)
{
await _db.LeaveTypes.AddRangeAsync(missingLeaveTypes, ct);
}

await _db.SaveChangesAsync(ct);
_logger.LogInformation("Seeded {Count} development LeaveType rows.", missingLeaveTypes.Length);
_logger.LogInformation(
"Ensured {SeedName} LeaveType rows: added {AddedCount}, updated {UpdatedCount}.",
seedName,
missingLeaveTypes.Length,
updatedCount);
}

private async Task SeedEmployeeAccrualBalancesAsync(CancellationToken ct)
Expand Down Expand Up @@ -981,6 +970,37 @@ private async Task<Dictionary<string, LeaveRequest>> LoadLeaveRequestsByKeyAsync

private static string NormalizeKey(string value) => value.Trim();

private static bool ApplyLeaveTypeSeed(LeaveType leaveType, LeaveTypeSeed seed)
{
var updated = false;

if (leaveType.SourceLeaveTypeNumber != seed.SourceLeaveTypeNumber)
{
leaveType.SourceLeaveTypeNumber = seed.SourceLeaveTypeNumber;
updated = true;
}

if (!string.Equals(leaveType.DisplayName, seed.DisplayName, StringComparison.Ordinal))
{
leaveType.DisplayName = seed.DisplayName;
updated = true;
}

if (leaveType.HasAccrualBalance != seed.HasAccrualBalance)
{
leaveType.HasAccrualBalance = seed.HasAccrualBalance;
updated = true;
}

if (leaveType.IsActive != seed.IsActive)
{
leaveType.IsActive = seed.IsActive;
updated = true;
}

return updated;
}

private static string CreateDepartmentRoutingKey(string departmentCode, string toEmail) => $"{departmentCode}|{toEmail}";

private static string CreateLeaveRequestKey(string iamId, DateOnly startDate, DateOnly endDate)
Expand Down
58 changes: 57 additions & 1 deletion tests/server.tests/Data/DbInitializerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,71 @@ namespace Server.Tests.Data;
public class DbInitializerTests
{
[Fact]
public async Task InitializeAsyncWithoutDevelopmentSeedCreatesDatabaseWithoutSeedRows()
public async Task InitializeAsyncWithoutDevelopmentSeedCreatesTheRequiredLeaveTypeCatalog()
{
await using var db = CreateUninitializedInMemoryContext();
var initializer = new DbInitializer(db, NullLogger<DbInitializer>.Instance);

await initializer.InitializeAsync(includeDevSeed: false);
await initializer.InitializeAsync(includeDevSeed: false);

(await db.Database.EnsureCreatedAsync()).Should().BeFalse();
(await db.AppUsers.CountAsync()).Should().Be(0);
(await db.People.CountAsync()).Should().Be(0);
(await db.LeaveTypes
.OrderBy(leaveType => leaveType.LeaveTypeKey)
.Select(leaveType => new
{
leaveType.LeaveTypeKey,
leaveType.SourceLeaveTypeNumber,
leaveType.DisplayName,
leaveType.HasAccrualBalance,
leaveType.IsActive,
})
.ToListAsync())
.Should()
.BeEquivalentTo([
new
{
LeaveTypeKey = "FamilyCare",
SourceLeaveTypeNumber = (int?)30,
DisplayName = "FMLA",
HasAccrualBalance = false,
IsActive = true,
},
new
{
LeaveTypeKey = "ProfessionalDevelopment",
SourceLeaveTypeNumber = (int?)null,
DisplayName = "Professional Development",
HasAccrualBalance = false,
IsActive = true,
},
new
{
LeaveTypeKey = "Sabbatical",
SourceLeaveTypeNumber = (int?)40,
DisplayName = "Sabbatical",
HasAccrualBalance = false,
IsActive = true,
},
new
{
LeaveTypeKey = "Sick",
SourceLeaveTypeNumber = (int?)20,
DisplayName = "Sick Leave",
HasAccrualBalance = true,
IsActive = true,
},
new
{
LeaveTypeKey = "Vacation",
SourceLeaveTypeNumber = (int?)10,
DisplayName = "Vacation",
HasAccrualBalance = true,
IsActive = true,
},
]);
}

[Fact]
Expand All @@ -31,6 +86,7 @@ public async Task InitializeAsyncWithDevelopmentSeedCreatesDatabaseAndSeedsRows(
(await db.Database.EnsureCreatedAsync()).Should().BeFalse();
(await db.AppUsers.CountAsync()).Should().BeGreaterThan(0);
(await db.People.CountAsync()).Should().BeGreaterThan(0);
(await db.LeaveTypes.CountAsync()).Should().Be(5);
}

private static AppDbContext CreateUninitializedInMemoryContext()
Expand Down
2 changes: 1 addition & 1 deletion tests/server.tests/Data/EmployeeAccrualBalanceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ private static EmployeeAccrualBalance CreateEmployeeAccrualBalance(EmployeeAccru
new("66510837", "lwilson@fake.ucdavis.edu", "Lena Wilson", "2026-06-30", "40002345", 10, "Vacation", 160.00m, 0.00m, 8.00m, 0.00m, 168.00m, 240.00m, "001700", "Professor", "001700", "Professor", "030045", "ANIMAL SCIENCE"),
new("66510837", "lwilson@fake.ucdavis.edu", "Lena Wilson", "2026-06-30", "40002345", 20, "Sick Leave", 272.00m, 0.00m, 8.00m, 0.00m, 280.00m, 0.00m, "001700", "Professor", "001700", "Professor", "030045", "ANIMAL SCIENCE"),
new("36190428", "apatel@fake.ucdavis.edu", "Asha Patel", "2026-06-30", "40003456", 10, "Vacation", 210.00m, 8.00m, 10.00m, 0.00m, 212.00m, 240.00m, "000245", "Department Chair", "000245", "Department Chair", "030045", "ANIMAL SCIENCE"),
new("17628405", "sbaker@fake.ucdavis.edu", "Sofia Baker", "2026-07-12", "40004567", 50, "Compensatory Time", 18.00m, 0.00m, 2.00m, 0.00m, 20.00m, 80.00m, "006257", "Agricultural Technician", "006257", "Agricultural Technician", "030000", "AGR & ENV SCI DEANS OFFICE"),
new("17628405", "sbaker@fake.ucdavis.edu", "Sofia Baker", "2026-07-12", "40004567", 10, "Vacation", 18.00m, 0.00m, 2.00m, 0.00m, 20.00m, 80.00m, "006257", "Agricultural Technician", "006257", "Agricultural Technician", "030000", "AGR & ENV SCI DEANS OFFICE"),
];

private sealed record EmployeeAccrualBalanceSeed(
Expand Down
Loading