diff --git a/CodeReviews.Console.ExcelReader.sln b/CodeReviews.Console.ExcelReader.sln new file mode 100644 index 0000000..2ded134 --- /dev/null +++ b/CodeReviews.Console.ExcelReader.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocumentProcessor.yemiodetola", "DocumentProcessor.yemiodetola\DocumentProcessor.yemiodetola.csproj", "{C9D0FBE3-174F-6FCE-D2C4-7B102ACA0948}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C9D0FBE3-174F-6FCE-D2C4-7B102ACA0948}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9D0FBE3-174F-6FCE-D2C4-7B102ACA0948}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9D0FBE3-174F-6FCE-D2C4-7B102ACA0948}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9D0FBE3-174F-6FCE-D2C4-7B102ACA0948}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7166A67F-77F9-4000-A6B5-FB0681427764} + EndGlobalSection +EndGlobal diff --git a/DocumentProcessor.yemiodetola/Data/AppContext.cs b/DocumentProcessor.yemiodetola/Data/AppContext.cs new file mode 100644 index 0000000..bdd9d79 --- /dev/null +++ b/DocumentProcessor.yemiodetola/Data/AppContext.cs @@ -0,0 +1,12 @@ +using Microsoft.EntityFrameworkCore; +using DocumentProcessor.yemiodetola.Models; +public class DocumentProcessorDbContext : DbContext +{ + public DbSet Contacts => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder options) + { + var dbName = System.AppContext.GetData("SqliteDbName") as string ?? "excel_data.db"; + options.UseSqlite($"Data Source={dbName}"); + } +} \ No newline at end of file diff --git a/DocumentProcessor.yemiodetola/DocumentProcessor.yemiodetola.csproj b/DocumentProcessor.yemiodetola/DocumentProcessor.yemiodetola.csproj new file mode 100644 index 0000000..01c08f5 --- /dev/null +++ b/DocumentProcessor.yemiodetola/DocumentProcessor.yemiodetola.csproj @@ -0,0 +1,26 @@ + + + Exe + net9.0 + enable + enable + + + + ./output.pdf + excel_data.db + ./sample.xlsx + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + \ No newline at end of file diff --git a/DocumentProcessor.yemiodetola/Migrations/20251208000159_pendingMigrations.Designer.cs b/DocumentProcessor.yemiodetola/Migrations/20251208000159_pendingMigrations.Designer.cs new file mode 100644 index 0000000..0d310c1 --- /dev/null +++ b/DocumentProcessor.yemiodetola/Migrations/20251208000159_pendingMigrations.Designer.cs @@ -0,0 +1,46 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DocumentProcessor.yemiodetola.Migrations +{ + [DbContext(typeof(DocumentProcessorDbContext))] + [Migration("20251208000159_pendingMigrations")] + partial class pendingMigrations + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("DocumentProcessor.yemiodetola.Models.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Contacts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DocumentProcessor.yemiodetola/Migrations/20251208000159_pendingMigrations.cs b/DocumentProcessor.yemiodetola/Migrations/20251208000159_pendingMigrations.cs new file mode 100644 index 0000000..8f9009a --- /dev/null +++ b/DocumentProcessor.yemiodetola/Migrations/20251208000159_pendingMigrations.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DocumentProcessor.yemiodetola.Migrations +{ + /// + public partial class pendingMigrations : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Contacts", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", nullable: false), + Email = table.Column(type: "TEXT", nullable: false), + PhoneNumber = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Contacts", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Contacts"); + } + } +} diff --git a/DocumentProcessor.yemiodetola/Migrations/20251208214440_contextupdate.Designer.cs b/DocumentProcessor.yemiodetola/Migrations/20251208214440_contextupdate.Designer.cs new file mode 100644 index 0000000..ba01e46 --- /dev/null +++ b/DocumentProcessor.yemiodetola/Migrations/20251208214440_contextupdate.Designer.cs @@ -0,0 +1,46 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DocumentProcessor.yemiodetola.Migrations +{ + [DbContext(typeof(DocumentProcessorDbContext))] + [Migration("20251208214440_contextupdate")] + partial class contextupdate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("DocumentProcessor.yemiodetola.Models.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Contacts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DocumentProcessor.yemiodetola/Migrations/20251208214440_contextupdate.cs b/DocumentProcessor.yemiodetola/Migrations/20251208214440_contextupdate.cs new file mode 100644 index 0000000..c2220a8 --- /dev/null +++ b/DocumentProcessor.yemiodetola/Migrations/20251208214440_contextupdate.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DocumentProcessor.yemiodetola.Migrations +{ + /// + public partial class contextupdate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/DocumentProcessor.yemiodetola/Migrations/AppDbContextModelSnapshot.cs b/DocumentProcessor.yemiodetola/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 0000000..91a7e85 --- /dev/null +++ b/DocumentProcessor.yemiodetola/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,43 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DocumentProcessor.yemiodetola.Migrations +{ + [DbContext(typeof(DocumentProcessorDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("DocumentProcessor.yemiodetola.Models.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Contacts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/DocumentProcessor.yemiodetola/Models/Contact.cs b/DocumentProcessor.yemiodetola/Models/Contact.cs new file mode 100644 index 0000000..983564b --- /dev/null +++ b/DocumentProcessor.yemiodetola/Models/Contact.cs @@ -0,0 +1,31 @@ +namespace DocumentProcessor.yemiodetola.Models; + +public class Contact +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string PhoneNumber { get; set; } = string.Empty; + + public bool ValidateEmailAddress(string email) + { + if (String.IsNullOrEmpty(email)) + { + return false; + } + int atIndex = email.IndexOf('@'); + int dotIndex = email.LastIndexOf('.'); + + return atIndex > 0 + && dotIndex > atIndex + 1 + && dotIndex < email.Length - 1; + } + + public bool ValidatePhoneNumber(string phoneNumber) + { + return !String.IsNullOrEmpty(phoneNumber) + && phoneNumber.Length == 11 + && phoneNumber.StartsWith("0") + && phoneNumber.All(char.IsDigit); + } +} \ No newline at end of file diff --git a/DocumentProcessor.yemiodetola/Program.cs b/DocumentProcessor.yemiodetola/Program.cs new file mode 100644 index 0000000..085d5bd --- /dev/null +++ b/DocumentProcessor.yemiodetola/Program.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.DependencyInjection; +using System.Threading.Tasks; +using QuestPDF.Infrastructure; +using DocumentProcessor.yemiodetola.Services; + +class Program +{ + static async Task Main(string[] args) + { + QuestPDF.Settings.License = QuestPDF.Infrastructure.LicenseType.Community; + + var services = new ServiceCollection(); + + services.AddSingleton(); + services.AddSingleton(); + var provider = services.BuildServiceProvider(); + var excelService = provider.GetRequiredService(); + var pdfService = provider.GetRequiredService(); + + var excelPath = AppContext.GetData("ExcelFilePath") as string ?? "./sample.xlsx"; + var rows = await excelService.ReadExcelAsync(excelPath); + + if (rows == null || rows.Count == 0) + { + Console.WriteLine("Excel file is empty or unreadable."); + return; + } + + Console.WriteLine("\nExcel File Contents:\n"); + Console.WriteLine($"{"Name",-25} {"Email",-35} {"Phone Number",-15}"); + Console.WriteLine(new string('-', 80)); + foreach (var contact in rows) + { + Console.WriteLine($"{contact.Name,-25} {contact.Email,-35} {contact.PhoneNumber,-15}"); + } + Console.WriteLine(new string('-', 80)); + + var outputPdf = AppContext.GetData("PdfOutputPath") as string ?? "./output.pdf"; + + await pdfService.GeneratePdfAsync(outputPdf, rows); + + Console.WriteLine($"PDF generated successfully at: {outputPdf}"); + } +} diff --git a/DocumentProcessor.yemiodetola/README.md b/DocumentProcessor.yemiodetola/README.md new file mode 100644 index 0000000..3aa0710 --- /dev/null +++ b/DocumentProcessor.yemiodetola/README.md @@ -0,0 +1,19 @@ +# DocumentProcessor Console App + +## Setup Instructions + +1. **Clone the repository** + ```sh + cd DocumentProcessor.yemiodetola + ``` + +2. **Add your Excel file** + - Place your Excel file (e.g., `sample.xlsx`) in the project directory. A sample file (`sample.xlsx`) is already provided for testing. + - Ensure the worksheet is named `Contacts` and has columns: Name, Email, Phone Number. + +3. **Run the program** + ```sh + dotnet run + ``` + +Enjoy! diff --git a/DocumentProcessor.yemiodetola/Services/ContactsDocument.cs b/DocumentProcessor.yemiodetola/Services/ContactsDocument.cs new file mode 100644 index 0000000..79dcc0d --- /dev/null +++ b/DocumentProcessor.yemiodetola/Services/ContactsDocument.cs @@ -0,0 +1,53 @@ +using QuestPDF.Fluent; +using QuestPDF.Infrastructure; +using DocumentProcessor.yemiodetola.Models; + +namespace DocumentProcessor.yemiodetola.Services; + +public class ContactsDocument : IDocument +{ + private readonly List _contacts; + public ContactsDocument(List contacts) + { + _contacts = contacts; + } + + public DocumentMetadata GetMetadata() => new DocumentMetadata(); + public DocumentSettings GetSettings() => new DocumentSettings(); + + public void Compose(IDocumentContainer container) + { + container.Page(page => + { + page.Margin(30); + + page.Header().Text("Contact List") + .FontSize(20) + .SemiBold() + .AlignCenter(); + + page.Content().Table(table => + { + table.ColumnsDefinition(columns => + { + columns.ConstantColumn(150); + columns.RelativeColumn(); + columns.ConstantColumn(120); + }); + table.Header(header => + { + header.Cell().Text("Name").SemiBold(); + header.Cell().Text("Email").SemiBold(); + header.Cell().Text("Phone Number").SemiBold(); + }); + foreach (var c in _contacts) + { + table.Cell().Text(c.Name); + table.Cell().Text(c.Email); + table.Cell().Text(c.PhoneNumber); + } + }); + }); + } +} + diff --git a/DocumentProcessor.yemiodetola/Services/ExcelReader.cs b/DocumentProcessor.yemiodetola/Services/ExcelReader.cs new file mode 100644 index 0000000..dc3fa6f --- /dev/null +++ b/DocumentProcessor.yemiodetola/Services/ExcelReader.cs @@ -0,0 +1,33 @@ +using ClosedXML.Excel; +using DocumentProcessor.yemiodetola.Models; + +namespace DocumentProcessor.yemiodetola.Services; +public static class ExcelReader +{ + public static List ListContacts(string path, string sheet) + { + var results = new List(); + using var workbook = new XLWorkbook(path); + var ws = workbook.Worksheet(sheet); + + foreach (var row in ws.RowsUsed().Skip(1)) + { + var name = row.Cell(1).GetString().Trim(); + var email = row.Cell(2).GetString().Trim(); + var phone = row.Cell(3).GetString().Trim(); + + var model = new Contact + { + Name = name, + Email = email, + PhoneNumber = phone + }; + + if (!string.IsNullOrWhiteSpace(model.Name)) + { + results.Add(model); + } + } + return results; + } +} \ No newline at end of file diff --git a/DocumentProcessor.yemiodetola/Services/ExcelService.cs b/DocumentProcessor.yemiodetola/Services/ExcelService.cs new file mode 100644 index 0000000..b9ef55e --- /dev/null +++ b/DocumentProcessor.yemiodetola/Services/ExcelService.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using DocumentProcessor.yemiodetola.Models; +namespace DocumentProcessor.yemiodetola.Services; +public class ExcelService : IExcelService +{ + public async Task> ReadExcelAsync(string path) + { + return await Task.Run(() => ExcelReader.ListContacts(path, "Contacts")); + } +} diff --git a/DocumentProcessor.yemiodetola/Services/IExcelService.cs b/DocumentProcessor.yemiodetola/Services/IExcelService.cs new file mode 100644 index 0000000..27b5ea8 --- /dev/null +++ b/DocumentProcessor.yemiodetola/Services/IExcelService.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using DocumentProcessor.yemiodetola.Models; +namespace DocumentProcessor.yemiodetola.Services; +public interface IExcelService +{ + Task> ReadExcelAsync(string path); +} diff --git a/DocumentProcessor.yemiodetola/Services/IPdfService.cs b/DocumentProcessor.yemiodetola/Services/IPdfService.cs new file mode 100644 index 0000000..ff9eb4d --- /dev/null +++ b/DocumentProcessor.yemiodetola/Services/IPdfService.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using DocumentProcessor.yemiodetola.Models; +namespace DocumentProcessor.yemiodetola.Services; +public interface IPdfService +{ + Task GeneratePdfAsync(string outputPath, List contacts); +} diff --git a/DocumentProcessor.yemiodetola/Services/PdfService.cs b/DocumentProcessor.yemiodetola/Services/PdfService.cs new file mode 100644 index 0000000..573fe7a --- /dev/null +++ b/DocumentProcessor.yemiodetola/Services/PdfService.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using QuestPDF.Fluent; +using DocumentProcessor.yemiodetola.Models; +namespace DocumentProcessor.yemiodetola.Services; +public class PdfService : IPdfService +{ + public async Task GeneratePdfAsync(string outputPath, List contacts) + { + await Task.Run(() => + { + var document = new ContactsDocument(contacts); + document.GeneratePdf(outputPath); + }); + } +} diff --git a/DocumentProcessor.yemiodetola/excel_data.db b/DocumentProcessor.yemiodetola/excel_data.db new file mode 100644 index 0000000..d9f7767 Binary files /dev/null and b/DocumentProcessor.yemiodetola/excel_data.db differ diff --git a/DocumentProcessor.yemiodetola/sample.xlsx b/DocumentProcessor.yemiodetola/sample.xlsx new file mode 100644 index 0000000..b60e29c Binary files /dev/null and b/DocumentProcessor.yemiodetola/sample.xlsx differ