Skip to content

Commit 0212c83

Browse files
authored
Merge pull request #506 from aurelianware/claude/add-operating-mode-settings-RCnZt
2 parents bba19c6 + 1631316 commit 0212c83

File tree

4 files changed

+233
-1
lines changed

4 files changed

+233
-1
lines changed

src/portal/CloudHealthOffice.Portal/Pages/Settings.razor

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
@inject HttpClient Http
33
@inject IConfiguration Configuration
44
@inject ISnackbar Snackbar
5+
@inject ITenantContextService TenantContextService
6+
@inject IOperatingModeService OperatingModeService
57

68
<PageTitle>Settings - Cloud Health Office</PageTitle>
79

@@ -153,16 +155,95 @@
153155
</MudAlert>
154156
</MudPaper>
155157
</MudTabPanel>
158+
159+
<!-- Operating Mode Tab -->
160+
<MudTabPanel Text="Operating Mode" Icon="@Icons.Material.Filled.Tune">
161+
<MudPaper Class="pa-4 mt-4">
162+
<MudText Typo="Typo.h5" GutterBottom="true">Operating Mode Configuration</MudText>
163+
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="mb-4">
164+
Controls how Cloud Health Office engines interact with your existing core admin system
165+
</MudText>
166+
167+
@if (isLoadingOperatingMode)
168+
{
169+
<MudProgressCircular Indeterminate="true" />
170+
}
171+
else if (operatingModeConfig != null)
172+
{
173+
@if (isUsingDefaults)
174+
{
175+
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Class="mb-4" Icon="@Icons.Material.Filled.Info">
176+
Operating mode configuration loaded from defaults. Contact support or your account team to customize engine modes for your tenant.
177+
</MudAlert>
178+
}
179+
180+
<MudSimpleTable Hover="true" Striped="true" Dense="false" Class="mb-4" Elevation="0" Style="border: 1px solid var(--mud-palette-table-lines);">
181+
<thead>
182+
<tr>
183+
<th>Engine</th>
184+
<th>Current Mode</th>
185+
<th>Description</th>
186+
</tr>
187+
</thead>
188+
<tbody>
189+
@foreach (var engine in operatingModeConfig.Engines)
190+
{
191+
<tr>
192+
<td><strong>@FormatEngineName(engine.Key)</strong></td>
193+
<td>
194+
@if (engine.Value.Equals("augment", StringComparison.OrdinalIgnoreCase))
195+
{
196+
<MudChip Color="Color.Warning" Size="Size.Small">Augment</MudChip>
197+
}
198+
else
199+
{
200+
<MudChip Color="Color.Success" Size="Size.Small">Replace</MudChip>
201+
}
202+
</td>
203+
<td><MudText Typo="Typo.body2">@GetEngineDescription(engine.Key, engine.Value)</MudText></td>
204+
</tr>
205+
}
206+
</tbody>
207+
</MudSimpleTable>
208+
209+
<MudText Typo="Typo.subtitle2" Class="mt-4 mb-2">Mode Definitions</MudText>
210+
211+
<MudGrid>
212+
<MudItem xs="12" md="6">
213+
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.CompareArrows" NoIcon="false">
214+
<strong>Augment</strong> — Cloud Health Office runs alongside your existing system. Both results are computed and compared, but your legacy system remains authoritative.
215+
</MudAlert>
216+
</MudItem>
217+
<MudItem xs="12" md="6">
218+
<MudAlert Severity="Severity.Success" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.CheckCircle" NoIcon="false">
219+
<strong>Replace</strong> — Cloud Health Office is the authoritative system for this function. Your legacy system is no longer used for this engine.
220+
</MudAlert>
221+
</MudItem>
222+
</MudGrid>
223+
224+
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="mt-4">
225+
@if (operatingModeConfig.UpdatedAt.HasValue)
226+
{
227+
<span>Last updated: @operatingModeConfig.UpdatedAt.Value.ToString("MMM dd, yyyy 'at' h:mm tt") UTC. </span>
228+
}
229+
Operating mode changes are managed by your account team.
230+
</MudText>
231+
}
232+
</MudPaper>
233+
</MudTabPanel>
156234
</MudTabs>
157235
</MudContainer>
158236

159237
@code {
160238
private SubscriptionInfo? subscription;
161239
private bool isLoadingSubscription = true;
240+
private OperatingModeConfiguration? operatingModeConfig;
241+
private bool isLoadingOperatingMode = true;
242+
private bool isUsingDefaults;
162243

163244
protected override async Task OnInitializedAsync()
164245
{
165-
await LoadSubscriptionInfo();
246+
await Task.WhenAll(LoadSubscriptionInfo(), LoadOperatingModeAsync());
166247
}
167248

168249
private async Task LoadSubscriptionInfo()
@@ -243,6 +324,77 @@
243324
return Task.CompletedTask;
244325
}
245326

327+
private async Task LoadOperatingModeAsync()
328+
{
329+
isLoadingOperatingMode = true;
330+
try
331+
{
332+
var tenantId = await TenantContextService.GetTenantIdAsync();
333+
if (string.IsNullOrEmpty(tenantId))
334+
{
335+
isUsingDefaults = true;
336+
operatingModeConfig = new OperatingModeConfiguration
337+
{
338+
TenantId = "unknown",
339+
Engines = new Dictionary<string, string>(OperatingModeConfiguration.DefaultEngines, StringComparer.OrdinalIgnoreCase),
340+
UpdatedAt = null
341+
};
342+
return;
343+
}
344+
345+
operatingModeConfig = await OperatingModeService.GetOperatingModeAsync(tenantId);
346+
347+
// The service sets UpdatedAt = null when returning defaults (service unreachable).
348+
// A real API response will always have a non-null UpdatedAt.
349+
isUsingDefaults = !operatingModeConfig.UpdatedAt.HasValue;
350+
}
351+
catch (Exception ex)
352+
{
353+
Console.WriteLine($"Failed to load operating mode: {ex.Message}");
354+
isUsingDefaults = true;
355+
}
356+
finally
357+
{
358+
isLoadingOperatingMode = false;
359+
}
360+
}
361+
362+
private static string FormatEngineName(string key) => key switch
363+
{
364+
"benefitCalculation" => "Benefit Calculation",
365+
"rateResolution" => "Rate Resolution",
366+
"ncciEdits" => "NCCI Edits",
367+
"eligibilityVerification" => "Eligibility Verification",
368+
"claimsAdjudication" => "Claims Adjudication",
369+
_ => System.Text.RegularExpressions.Regex.Replace(key, "([a-z])([A-Z])", "$1 $2")
370+
};
371+
372+
private static string GetEngineDescription(string key, string mode)
373+
{
374+
var isAugment = mode.Equals("augment", StringComparison.OrdinalIgnoreCase);
375+
return key switch
376+
{
377+
"benefitCalculation" => isAugment
378+
? "Benefit calculations run in parallel with your legacy system for comparison."
379+
: "Cloud Health Office is the sole engine computing member benefit amounts.",
380+
"rateResolution" => isAugment
381+
? "Rate lookups are performed by both systems; legacy rates are authoritative."
382+
: "Cloud Health Office resolves all provider and procedure rates directly.",
383+
"ncciEdits" => isAugment
384+
? "NCCI compliance edits are applied by both systems; legacy edits take precedence."
385+
: "Cloud Health Office applies all NCCI bundling and compliance edits.",
386+
"eligibilityVerification" => isAugment
387+
? "Eligibility checks run through both systems; your existing system is the source of truth."
388+
: "Cloud Health Office handles all member eligibility verification.",
389+
"claimsAdjudication" => isAugment
390+
? "Claims are adjudicated by both engines; your legacy system's decisions are authoritative."
391+
: "Cloud Health Office is the authoritative claims adjudication engine.",
392+
_ => isAugment
393+
? "Running alongside your existing system in comparison mode."
394+
: "Cloud Health Office is the authoritative system for this function."
395+
};
396+
}
397+
246398
private class SubscriptionInfo
247399
{
248400
public string Tier { get; set; } = "";

src/portal/CloudHealthOffice.Portal/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@
205205
builder.Services.AddScoped<ISponsorService, SponsorService>();
206206
builder.Services.AddScoped<IReferenceDataService, ReferenceDataService>();
207207
builder.Services.AddScoped<ITenantService, TenantService>();
208+
builder.Services.AddScoped<IOperatingModeService, OperatingModeService>();
208209
builder.Services.AddSingleton<IEmailNotificationService, SmtpEmailNotificationService>();
209210
builder.Services.AddScoped<ISalesInquiryService, SalesInquiryService>();
210211

src/portal/CloudHealthOffice.Portal/Services/IServices.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,3 +678,25 @@ public class CreateSalesInquiryRequest
678678
public string Message { get; set; } = string.Empty;
679679
public string Source { get; set; } = "Contact Sales Page";
680680
}
681+
682+
// Operating Mode
683+
public interface IOperatingModeService
684+
{
685+
Task<OperatingModeConfiguration> GetOperatingModeAsync(string tenantId);
686+
}
687+
688+
public class OperatingModeConfiguration
689+
{
690+
public static readonly Dictionary<string, string> DefaultEngines = new(StringComparer.OrdinalIgnoreCase)
691+
{
692+
{ "benefitCalculation", "replace" },
693+
{ "rateResolution", "replace" },
694+
{ "ncciEdits", "replace" },
695+
{ "eligibilityVerification", "replace" },
696+
{ "claimsAdjudication", "replace" }
697+
};
698+
699+
public string TenantId { get; set; } = string.Empty;
700+
public Dictionary<string, string> Engines { get; set; } = new(StringComparer.OrdinalIgnoreCase);
701+
public DateTime? UpdatedAt { get; set; }
702+
}

src/portal/CloudHealthOffice.Portal/Services/ServiceImplementations.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3125,3 +3125,60 @@ private static MailMessage BuildConfirmationEmail(string from, SalesInquiry inqu
31253125
};
31263126
}
31273127
}
3128+
3129+
public class OperatingModeService : IOperatingModeService
3130+
{
3131+
private readonly HttpClient _httpClient;
3132+
private readonly IConfiguration _configuration;
3133+
private readonly ILogger<OperatingModeService> _logger;
3134+
3135+
public OperatingModeService(HttpClient httpClient, IConfiguration configuration, ILogger<OperatingModeService> logger)
3136+
{
3137+
_httpClient = httpClient;
3138+
_configuration = configuration;
3139+
_logger = logger;
3140+
}
3141+
3142+
public async Task<OperatingModeConfiguration> GetOperatingModeAsync(string tenantId)
3143+
{
3144+
var baseUrl = _configuration["Services:TenantService"];
3145+
try
3146+
{
3147+
var result = await _httpClient.GetFromJsonAsync<OperatingModeConfiguration>(
3148+
$"{baseUrl}/v1/tenants/{tenantId}/operating-mode");
3149+
if (result != null)
3150+
return NormalizeConfiguration(result, tenantId);
3151+
3152+
return GetDefaultConfiguration(tenantId);
3153+
}
3154+
catch (Exception ex)
3155+
{
3156+
_logger.LogWarning(ex, "Unable to fetch operating mode for tenant {TenantId}, returning defaults", tenantId);
3157+
return GetDefaultConfiguration(tenantId);
3158+
}
3159+
}
3160+
3161+
private static OperatingModeConfiguration NormalizeConfiguration(OperatingModeConfiguration config, string tenantId)
3162+
{
3163+
// Merge API results onto defaults so missing engines get "replace" mode
3164+
var merged = new Dictionary<string, string>(OperatingModeConfiguration.DefaultEngines, StringComparer.OrdinalIgnoreCase);
3165+
foreach (var kvp in config.Engines)
3166+
{
3167+
merged[kvp.Key] = kvp.Value;
3168+
}
3169+
3170+
config.TenantId = string.IsNullOrEmpty(config.TenantId) ? tenantId : config.TenantId;
3171+
config.Engines = merged;
3172+
return config;
3173+
}
3174+
3175+
private static OperatingModeConfiguration GetDefaultConfiguration(string tenantId)
3176+
{
3177+
return new OperatingModeConfiguration
3178+
{
3179+
TenantId = tenantId,
3180+
Engines = new Dictionary<string, string>(OperatingModeConfiguration.DefaultEngines, StringComparer.OrdinalIgnoreCase),
3181+
UpdatedAt = null
3182+
};
3183+
}
3184+
}

0 commit comments

Comments
 (0)