Skip to content

Commit

Permalink
Merge pull request #229 from pticostaricags/development
Browse files Browse the repository at this point in the history
Adding functionality to import contacts from custom excel file
  • Loading branch information
efonsecab authored Nov 18, 2024
2 parents d16bd33 + decd3b9 commit ac2e6da
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
@implements IAsyncDisposable

@using FairPlayCombined.Common
@using FairPlayCombined.Interfaces.Common
@attribute [Route(Constants.Routes.FairPlayCrmRoutes.UserRoutes.ExcelContactsImport)]
@attribute [Authorize]

@inject IContactService contactService
@inject IToastService toastService

<h3>Contacts Import</h3>


<LoadingIndicator ShowSpinners="@this.IsBusy"></LoadingIndicator>

<p>
Select file to import
</p>
<InputFile OnChange="OnFileSelectedAsync"></InputFile>

@code {
private bool IsBusy { get; set; }
private readonly CancellationTokenSource cancellationTokenSource = new();

private async Task OnFileSelectedAsync(InputFileChangeEventArgs inputFileChangeEventArgs)
{
try
{
this.IsBusy = true;
StateHasChanged();
if (inputFileChangeEventArgs.FileCount == 1)
{
var stream = inputFileChangeEventArgs.File.OpenReadStream();
MemoryStream streamCopy = new();
await stream.CopyToAsync(streamCopy);
await this.contactService
.ImportFromExcelFileAsync(streamCopy, this.cancellationTokenSource.Token);
this.toastService.ShowSuccess("Contacts have been imported");
}
}
catch (Exception ex)
{
this.toastService.ShowError(ex.Message);
}
finally
{
this.IsBusy = false;
StateHasChanged();
}
}

public ValueTask DisposeAsync()
{
this.cancellationTokenSource.Dispose();
return ValueTask.CompletedTask;
}
}
30 changes: 18 additions & 12 deletions src/FairPlayCombinedSln/FairPlayCRM/Components/Layout/NavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,48 @@
<AuthorizeView>
<Authorized>
<FluentNavGroup Title="@localizer[ContactsTextKey]" Expanded="ShouldExpandContactsGroup"
Icon="@(new Icons.Regular.Size20.ContactCardGroup())">
Icon="@(new Icons.Regular.Size20.ContactCardGroup())">
<FluentNavLink Href="@Constants.Routes.FairPlayCrmRoutes.UserRoutes.ListContacts"
Icon="@(new Icons.Regular.Size20.BookContacts())">
Icon="@(new Icons.Regular.Size20.BookContacts())">
@localizer[ListContactsTextKey]
</FluentNavLink>
<FluentNavLink Href="@Constants.Routes.FairPlayCrmRoutes.UserRoutes.CreateContact"
Icon="@(new Icons.Regular.Size20.Add())">
Icon="@(new Icons.Regular.Size20.Add())">
@localizer[CreateContactTextKey]
</FluentNavLink>
</FluentNavGroup>
<FluentNavGroup Title="@localizer[CompaniesTextKey]" Expanded="ShouldExpandCompaniesGroup"
Icon="@(new Icons.Regular.Size20.ContactCardGroup())">
Icon="@(new Icons.Regular.Size20.ContactCardGroup())">
<FluentNavLink Href="@Constants.Routes.FairPlayCrmRoutes.UserRoutes.ListCompanies"
Icon="@(new Icons.Regular.Size20.BookContacts())">
Icon="@(new Icons.Regular.Size20.BookContacts())">
@localizer[ListCompaniesTextKey]
</FluentNavLink>
<FluentNavLink Href="@Constants.Routes.FairPlayCrmRoutes.UserRoutes.CreateCompany"
Icon="@(new Icons.Regular.Size20.Add())">
Icon="@(new Icons.Regular.Size20.Add())">
@localizer[CreateCompanyTextKey]
</FluentNavLink>
<FluentNavLink Href="@Constants.Routes.FairPlayCrmRoutes.UserRoutes.LinkedInConnectionsImport"
Icon="@(new Icons.Regular.Size20.CallConnecting())">
Icon="@(new Icons.Regular.Size20.CallConnecting())">
@localizer[ImportLinkedInConnectionsTextKey]
</FluentNavLink>
<FluentNavLink Href="@Constants.Routes.FairPlayCrmRoutes.UserRoutes.LinkedInConnectionList"
Icon="@(new Icons.Regular.Size20.List())">
Icon="@(new Icons.Regular.Size20.List())">
@localizer[LinkedInConnectionListTextKey]
</FluentNavLink>
<FluentNavLink Href="@Constants.Routes.FairPlayCrmRoutes.UserRoutes.ExcelContactsImport"
Icon="@(new Icons.Regular.Size20.CallConnecting())">
@localizer[ImportExcelContactsTextKey]
</FluentNavLink>
</FluentNavGroup>
</Authorized>
</AuthorizeView>

<AuthorizeView>
<Authorized>
<FluentNavGroup Title="@localizer[AccountsTextKey]" Expanded="@ShouldExpandAccountsGroup"
Icon="@(new Icons.Regular.Size20.PeopleCommunity())">
Icon="@(new Icons.Regular.Size20.PeopleCommunity())">
<FluentNavLink Href="/Account/Manage"
Icon="@(new Icons.Regular.Size20.Edit())">@localizer[ManageAccountTextKey]</FluentNavLink>
Icon="@(new Icons.Regular.Size20.Edit())">@localizer[ManageAccountTextKey]</FluentNavLink>
</FluentNavGroup>
<FluentNavLink Icon="@(new Icons.Regular.Size20.ArrowExit())">
<form action="Account/Logout" method="post">
Expand All @@ -59,9 +63,9 @@
</Authorized>
<NotAuthorized>
<FluentNavLink Href="Account/Register"
Icon="@(new Icons.Regular.Size20.PersonAdd())">@localizer[RegisterTextKey]</FluentNavLink>
Icon="@(new Icons.Regular.Size20.PersonAdd())">@localizer[RegisterTextKey]</FluentNavLink>
<FluentNavLink Href="Account/Login"
Icon="@(new Icons.Regular.Size20.PersonArrowRight())">@localizer[LoginTextKey]</FluentNavLink>
Icon="@(new Icons.Regular.Size20.PersonArrowRight())">@localizer[LoginTextKey]</FluentNavLink>
</NotAuthorized>
</AuthorizeView>

Expand Down Expand Up @@ -120,5 +124,7 @@
public const string ImportLinkedInConnectionsTextKey = "ImportLinkedInConnectionsText";
[ResourceKey(defaultValue: "LinkedIn Connection List")]
public const string LinkedInConnectionListTextKey = "LinkedInConnectionListText";
[ResourceKey(defaultValue: "Import Excel Contacts")]
public const string ImportExcelContactsTextKey = "ImportExcelContactsText";
#endregion Resource Keys
}
2 changes: 2 additions & 0 deletions src/FairPlayCombinedSln/FairPlayCombined.Common/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ public static class UserRoutes

public const string LinkedInConnectionsImport = $"/User/LinkedInConnectionsImport";
public const string LinkedInConnectionList = $"/User/LinkedInConnectionList";

public const string ExcelContactsImport = $"/User/ExcelContactsImport";
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)

modelBuilder.Entity<Contact>(entity =>
{
entity.HasIndex(e => e.EmailAddress, "UI_Contact_EmailAddress")
.IsUnique()
.HasFilter("([EmailAddress] IS NOT NULL)");

entity.HasOne(d => d.OwnerApplicationUser).WithMany(p => p.Contact)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_Contact_AspNetUsers");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

namespace FairPlayCombined.DataAccess.Models.dboSchema;

[Index("EmailAddress", Name = "UI_Contact_EmailAddress", IsUnique = true)]
public partial class Contact
{
[Key]
Expand All @@ -33,7 +32,6 @@ public partial class Contact
[StringLength(50)]
public string Lastname { get; set; }

[Required]
[StringLength(50)]
public string EmailAddress { get; set; }

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using FairPlayCombined.Models.Common.Contact;
using FairPlayCombined.Models.Pagination;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.Extensions.Logging;

namespace FairPlayCombined.Interfaces.Common;
public interface IContactService
Expand All @@ -21,4 +19,5 @@ Task DeleteContactByIdAsync(
Task<PaginationOfT<ContactModel>> GetPaginatedContactAsync(
PaginationRequest paginationRequest,
CancellationToken cancellationToken);
Task ImportFromExcelFileAsync(Stream stream, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ public class CreateContactModel : ICreateModel
[Required]
[StringLength(50)]
public string? Lastname { get; set; }
[Required]
[StringLength(50)]
[EmailAddress]
public string? EmailAddress { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ public class UpdateContactModel : IUpdateModel
[Required]
[StringLength(50)]
public string? Lastname { get; set; }
[Required]
[StringLength(50)]
[EmailAddress]
public string? EmailAddress { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
using FairPlayCombined.Interfaces.Common;
using FairPlayCombined.Models.Common.Contact;
using FairPlayCombined.Models.Pagination;
using System.Diagnostics;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using FairPlayCombined.Interfaces;

namespace FairPlayCombined.Services.Common
{
Expand All @@ -18,5 +24,73 @@ namespace FairPlayCombined.Services.Common
>]
public partial class ContactService : BaseService, IContactService
{
private readonly IUserProviderService? userProviderService;
public ContactService(IDbContextFactory<FairPlayCombinedDbContext> dbContextFactory,
ILogger<ContactService> logger, IUserProviderService userProviderService
) : this(dbContextFactory, logger)
{
this.userProviderService = userProviderService;
}
public async Task ImportFromExcelFileAsync(Stream stream, CancellationToken cancellationToken)
{
using SpreadsheetDocument spreadsheetDocument = SpreadsheetDocument.Open(stream: stream, isEditable: false);
if (spreadsheetDocument != null)
{
IEnumerable<Sheet> sheets = spreadsheetDocument.WorkbookPart!.Workbook.GetFirstChild<Sheets>()!.Elements<Sheet>();
string relationshipId = sheets.First().Id!.Value!;
WorksheetPart worksheetPart = (WorksheetPart)spreadsheetDocument.WorkbookPart.GetPartById(relationshipId);
Worksheet workSheet = worksheetPart.Worksheet;
SheetData sheetData = workSheet!.GetFirstChild<SheetData>()!;
IEnumerable<Row> rows = sheetData.Descendants<Row>();
Dictionary<int, string> columns = new();
int pos = 0;
foreach (var cellInnerText in rows.ElementAt(0).Select(p=>p.InnerText))
{
columns.Add(pos, cellInnerText);
pos++;
}
await InsertExcelContactsAsync(dbContextFactory, rows, columns, cancellationToken);
}
}

private async Task InsertExcelContactsAsync(IDbContextFactory<FairPlayCombinedDbContext> dbContextFactory, IEnumerable<Row> rows, Dictionary<int, string> columns, CancellationToken cancellationToken)
{
int pos = 0;
var userId = userProviderService!.GetCurrentUserId();
var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
foreach (Row row in rows)
{
if (pos == 0)
{
pos++;
continue;
}
Debug.WriteLine(row.InnerText);
Contact contactEntity = new()
{
OwnerApplicationUserId = userId,
};
for (int i = 0; i < row.Descendants<Cell>().Count(); i++)
{
var cell = row.Descendants<Cell>().ElementAt(i);
if (String.IsNullOrWhiteSpace(cell.InnerText))
continue;
Debug.WriteLine(cell.InnerText);
var columnName = columns[i];
switch (columnName)
{
case "First Name": contactEntity.Name = cell.InnerText; break;
case "Last Name": contactEntity.Lastname = cell.InnerText; break;
case "LinkedIn": contactEntity.LinkedInProfileUrl = cell.InnerText; break;
case "YouTube Url": contactEntity.YouTubeChannelUrl = cell.InnerText; break;
case "Email": contactEntity.EmailAddress = cell.InnerText; break;
case "Instagram Url": contactEntity.InstagramUrl = cell.InnerText; break;
case "Description": contactEntity.Notes = cell.InnerText; break;
}
}
await dbContext.Contact.AddAsync(contactEntity, cancellationToken);
}
await dbContext.SaveChangesAsync(cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<PackageReference Include="Azure.AI.ContentSafety" Version="1.0.0" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.1.0-beta.2" />
<PackageReference Include="CsvHelper" Version="33.0.1" />
<PackageReference Include="DocumentFormat.OpenXml" Version="3.1.1" />
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.68.0.3596" />
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0-rc.1.24381.5" />
<PackageReference Include="PayoutsSdk" Version="1.1.1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[OwnerApplicationUserId] NVARCHAR(450) NOT NULL,
[Name] NVARCHAR(50) NOT NULL,
[Lastname] NVARCHAR(50) NOT NULL,
[EmailAddress] NVARCHAR(50) NOT NULL,
[EmailAddress] NVARCHAR(50) NULL,
[LinkedInProfileUrl] NVARCHAR(MAX) NULL,
[YouTubeChannelUrl] NVARCHAR(MAX) NULL,
[InstagramUrl] NVARCHAR(MAX) NULL,
Expand All @@ -18,4 +18,4 @@

GO

CREATE UNIQUE INDEX [UI_Contact_EmailAddress] ON [dbo].[Contact] ([EmailAddress])
CREATE UNIQUE INDEX [UI_Contact_EmailAddress] ON [dbo].[Contact] ([EmailAddress]) WHERE ([EmailAddress] IS NOT NULL)

0 comments on commit ac2e6da

Please sign in to comment.