Skip to content

Commit b8bb463

Browse files
committed
feat(portal): build world-class payer operations UI (PR13–PR17)
Add full-featured screens for all major health plan operational workflows: - Member enrollment ops: PCP view/change, coverage history timeline, 834 transaction viewer, termination dialog with confirmation guard - EDI Operations Center: 834 batch resolution, 277CA/835 downloads, transaction history (paginated), error queue - Payment Runs: live SignalR progress, ERA download, create/detail dialogs - Premium Billing: billing cycles, inline rate editing, collections (overdue color gradient), invoice generation - Claim adjudication transparency: pipeline step timeline + NCCI results, fee schedule + cost-sharing breakdown + accumulator progress bars - Workflows dashboard: Argo run table with step expand, retry, 30s auto-refresh via PeriodicTimer, SignalR live updates - Reports & Analytics: 5-tab dashboard — Claims Summary (chart + CSV export), Payment Summary, Eligibility Stats, Prior Auth, Provider Performance (sortable) - Infrastr- Infrastr- Infrastr- Infrastr- Infrastr- Infrastr- Infrastr- Inservice registrations, nav items, appsettings keys
1 parent 06f57fa commit b8bb463

20 files changed

+6276
-162
lines changed
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
@using CloudHealthOffice.Portal.Services
2+
@inject IProviderService ProviderService
3+
@inject IMemberService MemberService
4+
@inject ISnackbar Snackbar
5+
6+
<MudDialog>
7+
<TitleContent>
8+
<MudText Typo="Typo.h6">
9+
<MudIcon Icon="@Icons.Material.Filled.LocalHospital" Class="mr-2" Style="vertical-align: middle;" />
10+
Assign Primary Care Physician
11+
</MudText>
12+
</TitleContent>
13+
<DialogContent>
14+
@if (_currentPcp != null)
15+
{
16+
<MudAlert Severity="Severity.Info" Class="mb-4" Dense="true">
17+
Current PCP: <strong>@_currentPcp.ProviderName</strong> (@_currentPcp.Specialty) —
18+
<MudChip T="string" Size="Size.Small" Color="@(_currentPcp.NetworkStatus == "In-Network" ? Color.Success : Color.Warning)">
19+
@_currentPcp.NetworkStatus
20+
</MudChip>
21+
</MudAlert>
22+
}
23+
24+
<MudGrid Spacing="3">
25+
<MudItem xs="12">
26+
<MudTextField @bind-Value="_searchTerm"
27+
Label="Search Providers (name, NPI, or specialty)"
28+
Variant="Variant.Outlined"
29+
Adornment="Adornment.End"
30+
AdornmentIcon="@Icons.Material.Filled.Search"
31+
OnKeyDown="HandleSearchKey"
32+
HelperText="Search for a primary care provider to assign" />
33+
</MudItem>
34+
<MudItem xs="12" md="6">
35+
<MudSelect @bind-Value="_specialtyFilter" Label="Filter by Specialty" Variant="Variant.Outlined">
36+
<MudSelectItem Value='@("")'>All Specialties</MudSelectItem>
37+
<MudSelectItem Value="@("Family Medicine")">Family Medicine</MudSelectItem>
38+
<MudSelectItem Value="@("Internal Medicine")">Internal Medicine</MudSelectItem>
39+
<MudSelectItem Value="@("Pediatrics")">Pediatrics</MudSelectItem>
40+
<MudSelectItem Value="@("General Practice")">General Practice</MudSelectItem>
41+
<MudSelectItem Value="@("OB/GYN")">OB/GYN</MudSelectItem>
42+
<MudSelectItem Value="@("Geriatrics")">Geriatrics</MudSelectItem>
43+
</MudSelect>
44+
</MudItem>
45+
<MudItem xs="12" md="6">
46+
<MudButton Variant="Variant.Filled" Color="Color.Primary" FullWidth="true"
47+
Style="height: 56px;"
48+
OnClick="SearchProviders"
49+
Disabled="_searching">
50+
@if (_searching)
51+
{
52+
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
53+
}
54+
Search
55+
</MudButton>
56+
</MudItem>
57+
</MudGrid>
58+
59+
@if (_providers.Any())
60+
{
61+
<MudTable Items="_providers" Dense="true" Hover="true" Class="mt-4"
62+
SelectedItem="_selectedProvider"
63+
SelectedItemChanged="OnProviderSelected"
64+
T="ProviderListItem">
65+
<HeaderContent>
66+
<MudTh>Provider Name</MudTh>
67+
<MudTh>NPI</MudTh>
68+
<MudTh>Specialty</MudTh>
69+
<MudTh>Location</MudTh>
70+
<MudTh>Network</MudTh>
71+
<MudTh>Credentialing</MudTh>
72+
</HeaderContent>
73+
<RowTemplate>
74+
<MudTd DataLabel="Provider Name">
75+
<div class="d-flex align-center gap-2">
76+
@if (_selectedProvider?.ProviderId == context.ProviderId)
77+
{
78+
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" />
79+
}
80+
<MudText Typo="Typo.body2" Style="font-weight: @(_selectedProvider?.ProviderId == context.ProviderId ? "bold" : "normal")">
81+
@context.Name
82+
</MudText>
83+
</div>
84+
</MudTd>
85+
<MudTd DataLabel="NPI">
86+
<MudText Typo="Typo.body2" Style="font-family: monospace; color: rgba(0,255,255,0.8);">@context.NPI</MudText>
87+
</MudTd>
88+
<MudTd DataLabel="Specialty">@context.Specialty</MudTd>
89+
<MudTd DataLabel="Location">@context.City, @context.State</MudTd>
90+
<MudTd DataLabel="Network">
91+
<MudChip T="string" Size="Size.Small"
92+
Color="@(context.NetworkStatus == "In-Network" ? Color.Success : context.NetworkStatus == "Pending" ? Color.Warning : Color.Error)">
93+
@context.NetworkStatus
94+
</MudChip>
95+
</MudTd>
96+
<MudTd DataLabel="Credentialing">
97+
<MudChip T="string" Size="Size.Small"
98+
Color="@(context.CredentialingStatus == "Active" ? Color.Success : context.CredentialingStatus == "Pending" ? Color.Warning : Color.Error)">
99+
@context.CredentialingStatus
100+
</MudChip>
101+
</MudTd>
102+
</RowTemplate>
103+
</MudTable>
104+
}
105+
else if (_hasSearched && !_searching)
106+
{
107+
<MudAlert Severity="Severity.Info" Class="mt-4">No providers found matching your search criteria.</MudAlert>
108+
}
109+
110+
@if (_selectedProvider != null)
111+
{
112+
<MudDivider Class="my-4" />
113+
<MudText Typo="Typo.subtitle1" Class="mb-3">
114+
<MudIcon Icon="@Icons.Material.Filled.Assignment" Class="mr-1" Size="Size.Small" />
115+
Assignment Details
116+
</MudText>
117+
<MudGrid Spacing="3">
118+
<MudItem xs="12" md="6">
119+
<MudDatePicker @bind-Date="_effectiveDate"
120+
Label="Effective Date"
121+
Variant="Variant.Outlined"
122+
MinDate="DateTime.Today"
123+
HelperText="Date when PCP assignment takes effect" />
124+
</MudItem>
125+
<MudItem xs="12" md="6">
126+
<MudTextField @bind-Value="_reason"
127+
Label="Reason for Change (optional)"
128+
Variant="Variant.Outlined"
129+
HelperText="e.g., Member request, provider left network" />
130+
</MudItem>
131+
</MudGrid>
132+
}
133+
</DialogContent>
134+
<DialogActions>
135+
<MudButton OnClick="Cancel" Variant="Variant.Text">Cancel</MudButton>
136+
<MudButton Variant="Variant.Filled"
137+
Color="Color.Primary"
138+
OnClick="AssignPcp"
139+
Disabled="@(_selectedProvider == null || _assigning || !_effectiveDate.HasValue)">
140+
@if (_assigning)
141+
{
142+
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
143+
}
144+
Assign PCP
145+
</MudButton>
146+
</DialogActions>
147+
</MudDialog>
148+
149+
@code {
150+
[CascadingParameter]
151+
MudDialogInstance MudDialog { get; set; } = null!;
152+
153+
[Parameter]
154+
public string MemberId { get; set; } = string.Empty;
155+
156+
[Parameter]
157+
public MemberPcp? CurrentPcp { get; set; }
158+
159+
private MemberPcp? _currentPcp;
160+
private string _searchTerm = string.Empty;
161+
private string _specialtyFilter = string.Empty;
162+
private List<ProviderListItem> _providers = new();
163+
private ProviderListItem? _selectedProvider;
164+
private DateTime? _effectiveDate = DateTime.Today;
165+
private string _reason = string.Empty;
166+
private bool _searching = false;
167+
private bool _assigning = false;
168+
private bool _hasSearched = false;
169+
170+
protected override void OnInitialized()
171+
{
172+
_currentPcp = CurrentPcp;
173+
}
174+
175+
private async Task HandleSearchKey(KeyboardEventArgs args)
176+
{
177+
if (args.Key == "Enter") await SearchProviders();
178+
}
179+
180+
private async Task SearchProviders()
181+
{
182+
_searching = true;
183+
_hasSearched = true;
184+
try
185+
{
186+
_providers = await ProviderService.SearchProvidersAsync(
187+
specialty: string.IsNullOrEmpty(_specialtyFilter) ? null : _specialtyFilter,
188+
networkStatus: "In-Network",
189+
searchTerm: string.IsNullOrEmpty(_searchTerm) ? null : _searchTerm);
190+
// Filter to individual providers (PCPs are individual practitioners)
191+
_providers = _providers.Where(p => p.PracticeType == "Individual" || p.PracticeType == "").ToList();
192+
}
193+
catch (Exception ex)
194+
{
195+
Snackbar.Add($"Error searching providers: {ex.Message}", Severity.Error);
196+
}
197+
finally
198+
{
199+
_searching = false;
200+
StateHasChanged();
201+
}
202+
}
203+
204+
private void OnProviderSelected(ProviderListItem provider)
205+
{
206+
_selectedProvider = _selectedProvider?.ProviderId == provider.ProviderId ? null : provider;
207+
}
208+
209+
private async Task AssignPcp()
210+
{
211+
if (_selectedProvider == null || !_effectiveDate.HasValue) return;
212+
_assigning = true;
213+
try
214+
{
215+
await MemberService.AssignPcpAsync(new AssignPcpRequest
216+
{
217+
MemberId = MemberId,
218+
ProviderId = _selectedProvider.ProviderId,
219+
EffectiveDate = _effectiveDate.Value,
220+
Reason = string.IsNullOrWhiteSpace(_reason) ? null : _reason
221+
});
222+
223+
var newPcp = new MemberPcp
224+
{
225+
ProviderId = _selectedProvider.ProviderId,
226+
ProviderName = _selectedProvider.Name,
227+
NPI = _selectedProvider.NPI,
228+
Specialty = _selectedProvider.Specialty,
229+
NetworkStatus = _selectedProvider.NetworkStatus,
230+
AssignedDate = _effectiveDate.Value,
231+
PracticeName = _selectedProvider.PracticeName
232+
};
233+
234+
Snackbar.Add($"PCP assigned: {_selectedProvider.Name}", Severity.Success);
235+
MudDialog.Close(DialogResult.Ok(newPcp));
236+
}
237+
catch (Exception ex)
238+
{
239+
Snackbar.Add($"Error assigning PCP: {ex.Message}", Severity.Error);
240+
}
241+
finally
242+
{
243+
_assigning = false;
244+
}
245+
}
246+
247+
private void Cancel() => MudDialog.Cancel();
248+
}

0 commit comments

Comments
 (0)