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
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,16 @@
</app-page-section>

<app-page-section *ngIf="site?.remoteUsers?.length">
<app-page-subheader>
<app-page-subheader class="remote-users-header">
<ng-container appPageSubheaderTitle>
<mat-icon class="mr-2">account_circle</mat-icon>
Remote Practitioners
</ng-container>
<button mat-icon-button
matTooltip="Remote User Export (csv file)"
(click)="onExportRemoteUsers('csv')">
<mat-icon>description</mat-icon>
</button>
<button mat-icon-button
matTooltip="View Remote Practitioners"
(click)="onRoute([organization.id, AdjudicationRoutes.SITE_REGISTRATION, site.id, AdjudicationRoutes.SITE_REMOTE_USERS])">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@
import { ConfigService } from '@config/config.service';
import { VendorConfig } from '@config/config.model';
import { LinkSiteComponent } from '@shared/components/dialogs/content/link-site/link-site.component';
import { UtilsService } from '@core/services/utils.service';

@Component({
selector: 'app-site-overview',
templateUrl: './site-overview.component.html',
styleUrls: ['./site-overview.component.scss'],
standalone: false
selector: 'app-site-overview',
templateUrl: './site-overview.component.html',
styleUrls: ['./site-overview.component.scss'],
standalone: false
})
export class SiteOverviewComponent implements OnInit {
public busy: Subscription;
Expand Down Expand Up @@ -61,6 +62,7 @@
private organizationResource: OrganizationResource,
private fb: UntypedFormBuilder,
private configService: ConfigService,
private utilsService: UtilsService,

Check warning on line 65 in prime-angular-frontend/src/app/modules/adjudication/pages/site-overview/site-overview.component.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'utilsService: UtilsService' is never reassigned; mark it as `readonly`.

See more on https://sonarcloud.io/project/issues?id=bcgov_moh-prime&issues=AZ500w-_pj_seCmcP3Ua&open=AZ500w-_pj_seCmcP3Ua&pullRequest=2858
) {
this.hasActions = true;
this.refresh = new BehaviorSubject<boolean>(null);
Expand Down Expand Up @@ -179,6 +181,15 @@
});
}

public onExportRemoteUsers(type: string): void {
this.busy = this.adjudicationResource.getRemoteUserExport(this.site.id, type)
.subscribe((file: string) => {
const blob = this.utilsService.base64ToBlob(file, 'text/csv');
//const fileBlob = new Blob([file.fileContents], { type: file.contentType });

Check warning on line 188 in prime-angular-frontend/src/app/modules/adjudication/pages/site-overview/site-overview.component.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this commented out code.

See more on https://sonarcloud.io/project/issues?id=bcgov_moh-prime&issues=AZ500w-_pj_seCmcP3Ub&open=AZ500w-_pj_seCmcP3Ub&pullRequest=2858
this.utilsService.downloadDocument(blob, "remoteUserExport.csv");
});
}

public ngOnInit(): void {
this.createFormInstance();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,22 @@
profileSnapshot.selfDeclarations = orderedSelfDeclarations;
}

public getRemoteUserExport(siteId: number, type: string): Observable<string> {
const params = this.apiResourceUtilsService.makeHttpParams({ format: type });
//const options = { responseType: 'blob' as 'blob' };

Check warning on line 873 in prime-angular-frontend/src/app/modules/adjudication/shared/services/adjudication-resource.service.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this commented out code.

See more on https://sonarcloud.io/project/issues?id=bcgov_moh-prime&issues=AZ500w8Npj_seCmcP3UY&open=AZ500w8Npj_seCmcP3UY&pullRequest=2858
return this.apiResource.get<string>(`sites/${siteId}/remote-users/export`, params)
.pipe(
map((response: ApiHttpResponse<string>) => response.result),
tap((file: string) => this.logger.info('REMOTE_USER_EXPORT', file)),
catchError((error: any) => {
this.toastService.openErrorToast('Remote user export could not be retrieved');
this.logger.error('[Adjudication] AdjudicationResource::getRemoteUserExport error has occurred: ', error);
throw error;
})
);
}


/******************************
* Organization Page resource
******************************/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
.action-icon {
margin-top: -.4rem;
cursor: pointer;
width: 150px;
text-align: right;
}

&.divider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import { CareSettingEnum } from '@shared/enums/care-setting.enum';
import { ValidationErrors } from '@angular/forms';

@Component({
selector: 'app-overview-container',
templateUrl: './overview-container.component.html',
styleUrls: ['./overview-container.component.scss'],
standalone: false
selector: 'app-overview-container',
templateUrl: './overview-container.component.html',
styleUrls: ['./overview-container.component.scss'],
standalone: false
})
export class OverviewContainerComponent implements OnInit {
@Input() public showEditRedirect: boolean;
Expand Down Expand Up @@ -45,8 +45,7 @@ export class OverviewContainerComponent implements OnInit {
}

public get showRemnoteUsers(): boolean {
return this.site.careSettingCode === CareSettingEnum.HEALTH_AUTHORITY ||
this.site.careSettingCode === CareSettingEnum.PRIVATE_COMMUNITY_HEALTH_PRACTICE;
return this.site.careSettingCode === CareSettingEnum.HEALTH_AUTHORITY;
}

public get showDeviceProviderId(): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
(route)="onRoute($event)">

<ng-container *ngIf="remoteUsers?.length; else noRemoteUsers">
<ng-container *ngFor="let remoteUser of remoteUsers">
<ng-container *ngFor="let remoteUser of remoteUsers; first as first">
<hr *ngIf="!first" />
<div class="mb-3">
<app-enrollee-property title="Name">
<app-enrollee-property title="Name"
makeBold="true">
{{ remoteUser | fullname | default }}
</app-enrollee-property>
</div>
<div class="mb-3">
<app-enrollee-property title="Email">
<app-enrollee-property title="Email"
makeBold="true">
{{ remoteUser.email }}
</app-enrollee-property>
</div>
Expand Down Expand Up @@ -41,7 +44,8 @@
</div>

<div class="mb-3">
<app-enrollee-property title="Created Date">
<app-enrollee-property title="Created Date"
makeBold="true">
{{ remoteUser.createdDate | formatDate }}
</app-enrollee-property>
</div>
Expand Down
48 changes: 48 additions & 0 deletions prime-dotnet-webapi/Controllers/SitesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class SitesController : PrimeControllerBase
private readonly IAdminService _adminService;
private readonly IBusinessEventService _businessEventService;
private readonly ICommunitySiteService _communitySiteService;
private readonly IExportService _exportService;
private readonly IHealthAuthoritySiteService _healthAuthoritySiteService;
private readonly IDocumentService _documentService;
private readonly IEmailService _emailService;
Expand All @@ -42,6 +43,7 @@ public SitesController(
IHealthAuthoritySiteService healthAuthoritySiteService,
IDocumentService documentService,
IEmailService emailService,
IExportService exportService,
IMapper mapper,
IOrganizationService organizationService,
ISiteService siteService,
Expand All @@ -53,6 +55,7 @@ public SitesController(
_communitySiteService = communitySiteService;
_documentService = documentService;
_emailService = emailService;
_exportService = exportService;
_mapper = mapper;
_organizationService = organizationService;
_siteService = siteService;
Expand Down Expand Up @@ -883,6 +886,51 @@ public async Task<ActionResult> SendRemoteUsersEmailAdmin(int siteId)
return NoContent();
}

// GET: api/Sites/5/remote-users/export
/// <summary>
/// Export remote users of a site to CSV or Excel format
/// </summary>
/// <param name="siteId"></param>
/// <param name="format">Export format: 'csv' or 'excel'</param>
[Authorize(Roles = Roles.PrimeSuperAdmin)]
[HttpGet("{siteId}/remote-users/export", Name = nameof(ExportRemoteUsers))]
[ProducesResponseType(typeof(ApiMessageResponse), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(typeof(ApiMessageResponse), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ApiResultResponse<string>), StatusCodes.Status200OK)]
public async Task<ActionResult> ExportRemoteUsers(int siteId, [FromQuery] string format = "csv")
{
var record = await _communitySiteService.GetPermissionsRecordAsync(siteId);
if (record == null)
{
return NotFound($"Site not found with id {siteId}");
}
if (!record.AccessableBy(User))
{
return Forbid();
}

if (format != "csv" && format != "excel")
{
return BadRequest("Format must be 'csv' or 'excel'");
}

var site = await _communitySiteService.GetSiteAsync(siteId);
if (site?.RemoteUsers == null || site.RemoteUsers.Count == 0)
{
return NotFound("No remote users found for this site");
}

byte[] fileContent;

fileContent = await (string.Equals(format, "excel", StringComparison.OrdinalIgnoreCase)
? _exportService.ExportRemoteUsersToExcelAsync(siteId)
: _exportService.ExportRemoteUsersToCSVAsync(siteId));

return Ok(Convert.ToBase64String(fileContent));
}

// POST: api/Sites/5/site-reviewed-email
/// <summary>
/// Send site reviewed notification email to provider enrolment team
Expand Down
123 changes: 123 additions & 0 deletions prime-dotnet-webapi/Services/ExportService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CsvHelper;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using OfficeOpenXml;
using OfficeOpenXml.Style;

namespace Prime.Services
{
public class ExportService : BaseService, IExportService
{
public ExportService(
ApiDbContext context,
ILogger<ExportService> logger)
: base(context, logger)
{
// Set EPPlus license context
ExcelPackage.LicenseContext = LicenseContext.NonCommercial;
}

/// <summary>
/// Export remote users to CSV format
/// </summary>
public async Task<byte[]> ExportRemoteUsersToCSVAsync(int siteId)
{
using (var memoryStream = new MemoryStream())
using (var writer = new StreamWriter(memoryStream, Encoding.UTF8))
using (var csvWriter = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
// Write headers
csvWriter.WriteHeader<RemoteUserExportDto>();
await csvWriter.NextRecordAsync();

// Write data
var exportDtos = await GetRemoteUsers(siteId);
await csvWriter.WriteRecordsAsync(exportDtos);
await writer.FlushAsync();

return memoryStream.ToArray();
}
}

/// <summary>
/// Export remote users to Excel format
/// </summary>
public async Task<byte[]> ExportRemoteUsersToExcelAsync(int siteId)
{
using (var package = new ExcelPackage())
{
var worksheet = package.Workbook.Worksheets.Add("Remote Users");

// Set headers
var headers = new[] { "First Name", "Last Name", "Email", "College", "License Class", "License Number", "Practitioner Id" };
for (int i = 0; i < headers.Length; i++)
{
worksheet.Cells[1, i + 1].Value = headers[i];
}

// Format header row
var headerRow = worksheet.Cells[1, 1, 1, headers.Length];
headerRow.Style.Font.Bold = true;
headerRow.Style.Fill.PatternType = ExcelFillStyle.Solid;
headerRow.Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.LightGray);

// Write data
var exportDtos = await GetRemoteUsers(siteId);
for (int i = 0; i < exportDtos.Count(); i++)
{
var dto = exportDtos.ElementAt(i);
worksheet.Cells[i + 2, 1].Value = dto.FirstName;
worksheet.Cells[i + 2, 2].Value = dto.LastName;
worksheet.Cells[i + 2, 3].Value = dto.Email;
worksheet.Cells[i + 2, 4].Value = dto.College;
worksheet.Cells[i + 2, 5].Value = dto.LicenseClass;
worksheet.Cells[i + 2, 6].Value = dto.LicenseNumber;
worksheet.Cells[i + 2, 7].Value = dto.PractitionerId;
}

// Auto-fit columns
worksheet.Cells.AutoFitColumns();

return await Task.FromResult(package.GetAsByteArray());

Check warning on line 88 in prime-dotnet-webapi/Services/ExportService.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Await GetAsByteArrayAsync instead.

See more on https://sonarcloud.io/project/issues?id=bcgov_moh-prime&issues=AZ500w_opj_seCmcP3Uc&open=AZ500w_opj_seCmcP3Uc&pullRequest=2858
}
}

private async Task<IEnumerable<RemoteUserExportDto>> GetRemoteUsers(int siteId)
{
return await _context.RemoteUsers
.Where(ru => ru.SiteId == siteId)
.Select(ru => new RemoteUserExportDto
{
FirstName = ru.FirstName,
LastName = ru.LastName,
Email = ru.Email,
College = ru.RemoteUserCertification.College.Name,
LicenseClass = ru.RemoteUserCertification.License.Name,
LicenseNumber = ru.RemoteUserCertification.LicenseNumber,
PractitionerId = ru.RemoteUserCertification.PractitionerId
})
.ToListAsync();
}

/// <summary>
/// DTO for exporting remote user data
/// </summary>
public class RemoteUserExportDto
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string College { get; set; }
public string LicenseClass { get; set; }
public string LicenseNumber { get; set; }
public string PractitionerId { get; set; }
}
}
}
19 changes: 19 additions & 0 deletions prime-dotnet-webapi/Services/interfaces/IExportService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Prime.ViewModels.Sites;

namespace Prime.Services
{
public interface IExportService
{
/// <summary>
/// Export remote users to CSV format
/// </summary>
Task<byte[]> ExportRemoteUsersToCSVAsync(int siteId);

/// <summary>
/// Export remote users to Excel format
/// </summary>
Task<byte[]> ExportRemoteUsersToExcelAsync(int siteId);
}
}
1 change: 1 addition & 0 deletions prime-dotnet-webapi/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public void ConfigureServices(IServiceCollection services)
services.AddScoped<IEnrolleeService, EnrolleeService>();
services.AddScoped<IEnrolleeSubmissionService, EnrolleeSubmissionService>();
services.AddScoped<IEnrolmentCertificateService, EnrolmentCertificateService>();
services.AddScoped<IExportService, ExportService>();
services.AddScoped<IGisService, GisService>();
services.AddScoped<IHealthAuthorityService, HealthAuthorityService>();
services.AddScoped<IHealthAuthoritySiteService, HealthAuthoritySiteService>();
Expand Down
2 changes: 2 additions & 0 deletions prime-dotnet-webapi/prime.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.7" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="EPPlus" Version="7.0.0" />
<PackageReference Include="CsvHelper" Version="30.0.0" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Configuration\Database\Resources\com-pharm-terms-v1.html" />
Expand Down
Loading