diff --git a/DocumentProcessor.davetn657/Data/Migrations/20260325194708_InitialCreate.Designer.cs b/DocumentProcessor.davetn657/Data/Migrations/20260325194708_InitialCreate.Designer.cs new file mode 100644 index 0000000..dfb6b25 --- /dev/null +++ b/DocumentProcessor.davetn657/Data/Migrations/20260325194708_InitialCreate.Designer.cs @@ -0,0 +1,51 @@ +// +using DocumentProcessor.davetn657.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DocumentProcessor.davetn657.Data.Migrations +{ + [DbContext(typeof(PhonebookContext))] + [Migration("20260325194708_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); + + modelBuilder.Entity("DocumentProcessor.davetn657.Data.Models.PhonebookProperties", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Category") + .IsRequired() + .HasColumnType("TEXT"); + + 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.davetn657/Data/Migrations/20260325194708_InitialCreate.cs b/DocumentProcessor.davetn657/Data/Migrations/20260325194708_InitialCreate.cs new file mode 100644 index 0000000..1c1bb5f --- /dev/null +++ b/DocumentProcessor.davetn657/Data/Migrations/20260325194708_InitialCreate.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DocumentProcessor.davetn657.Data.Migrations +{ + /// + public partial class InitialCreate : 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), + PhoneNumber = table.Column(type: "TEXT", nullable: false), + Email = table.Column(type: "TEXT", nullable: false), + Category = 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.davetn657/Data/Migrations/PhonebookContextModelSnapshot.cs b/DocumentProcessor.davetn657/Data/Migrations/PhonebookContextModelSnapshot.cs new file mode 100644 index 0000000..1ebae2b --- /dev/null +++ b/DocumentProcessor.davetn657/Data/Migrations/PhonebookContextModelSnapshot.cs @@ -0,0 +1,48 @@ +// +using DocumentProcessor.davetn657.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DocumentProcessor.davetn657.Data.Migrations +{ + [DbContext(typeof(PhonebookContext))] + partial class PhonebookContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.5"); + + modelBuilder.Entity("DocumentProcessor.davetn657.Data.Models.PhonebookProperties", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Category") + .IsRequired() + .HasColumnType("TEXT"); + + 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.davetn657/Data/Models/PhonebookProperties.cs b/DocumentProcessor.davetn657/Data/Models/PhonebookProperties.cs new file mode 100644 index 0000000..c61f8ce --- /dev/null +++ b/DocumentProcessor.davetn657/Data/Models/PhonebookProperties.cs @@ -0,0 +1,10 @@ +namespace DocumentProcessor.davetn657.Data.Models; + +public class PhonebookProperties +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string PhoneNumber { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string Category { get; set; } = string.Empty; +} diff --git a/DocumentProcessor.davetn657/Data/PhonebookContext.cs b/DocumentProcessor.davetn657/Data/PhonebookContext.cs new file mode 100644 index 0000000..373dda7 --- /dev/null +++ b/DocumentProcessor.davetn657/Data/PhonebookContext.cs @@ -0,0 +1,24 @@ +using DocumentProcessor.davetn657.Data.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; + +namespace DocumentProcessor.davetn657.Data; + +public class PhonebookContext : DbContext +{ + public DbSet Contacts { get; set; } + private string DbPath { get; set; } + + public PhonebookContext() + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .Build(); + + DbPath = builder.GetConnectionString("DefaultConnection"); + } + + protected override void OnConfiguring(DbContextOptionsBuilder options) + => options.UseSqlite(DbPath); +} \ No newline at end of file diff --git a/DocumentProcessor.davetn657/DocFiles/SeedData.xlsx b/DocumentProcessor.davetn657/DocFiles/SeedData.xlsx new file mode 100644 index 0000000..9a73705 Binary files /dev/null and b/DocumentProcessor.davetn657/DocFiles/SeedData.xlsx differ diff --git a/DocumentProcessor.davetn657/DocumentProcessor.davetn657.csproj b/DocumentProcessor.davetn657/DocumentProcessor.davetn657.csproj new file mode 100644 index 0000000..cb41a78 --- /dev/null +++ b/DocumentProcessor.davetn657/DocumentProcessor.davetn657.csproj @@ -0,0 +1,30 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/DocumentProcessor.davetn657/DocumentProcessor.davetn657.slnx b/DocumentProcessor.davetn657/DocumentProcessor.davetn657.slnx new file mode 100644 index 0000000..f54a560 --- /dev/null +++ b/DocumentProcessor.davetn657/DocumentProcessor.davetn657.slnx @@ -0,0 +1,3 @@ + + + diff --git a/DocumentProcessor.davetn657/Phonebook.db b/DocumentProcessor.davetn657/Phonebook.db new file mode 100644 index 0000000..17d62dc Binary files /dev/null and b/DocumentProcessor.davetn657/Phonebook.db differ diff --git a/DocumentProcessor.davetn657/Program.cs b/DocumentProcessor.davetn657/Program.cs new file mode 100644 index 0000000..5fd4ef9 --- /dev/null +++ b/DocumentProcessor.davetn657/Program.cs @@ -0,0 +1,30 @@ +using DocumentProcessor.davetn657.Data; +using DocumentProcessor.davetn657.Services; +using DocumentProcessor.davetn657.Views; +using IronSoftware.Abstractions.Pdf; +using Microsoft.Extensions.DependencyInjection; + +namespace DocumentProcessor.davetn657; + +internal class Program +{ + static void Main(string[] args) + { + var services = new ServiceCollection() + .AddDbContext() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .BuildServiceProvider(); + + using var scope = services.CreateScope(); + + var dataSeeder = scope.ServiceProvider.GetRequiredService(); + dataSeeder.SeedIfEmpty(); + + var ui = scope.ServiceProvider.GetRequiredService(); + ui.Start(); + } +} \ No newline at end of file diff --git a/DocumentProcessor.davetn657/Properties/launchSettings.json b/DocumentProcessor.davetn657/Properties/launchSettings.json new file mode 100644 index 0000000..a93ff4e --- /dev/null +++ b/DocumentProcessor.davetn657/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "DocumentProcessor.davetn657": { + "commandName": "Project", + "workingDirectory": "C:\\Users\\davei\\Documents\\.Coding\\.C#Academy\\.Backup\\DocumentProcessor.davetn657\\DocumentProcessor.davetn657" + } + } +} \ No newline at end of file diff --git a/DocumentProcessor.davetn657/README.md b/DocumentProcessor.davetn657/README.md new file mode 100644 index 0000000..4dd0346 --- /dev/null +++ b/DocumentProcessor.davetn657/README.md @@ -0,0 +1,21 @@ +# Document Processor Application + +## Overview + +Console based application that processes data from xlsx or csv file to import into a database or export database data to xlsx, csv, or pdf form. + +## Requirements + +- Needs to be able to seed data from xlsx or csv files into the database +- If no data is in database needs to auto seed data +- Export data to a pdf +- Handles errors +- Option to import/export data + + +## Technologies + +- C# +- Sqlite +- IronXL +- IronPdf diff --git a/DocumentProcessor.davetn657/Services/DataSeederService.cs b/DocumentProcessor.davetn657/Services/DataSeederService.cs new file mode 100644 index 0000000..1d8f74c --- /dev/null +++ b/DocumentProcessor.davetn657/Services/DataSeederService.cs @@ -0,0 +1,31 @@ +using DocumentProcessor.davetn657.Data; + +namespace DocumentProcessor.davetn657.Services; + +public interface IDataSeederService +{ + public void SeedIfEmpty(); +} + +public class DataSeederService : IDataSeederService +{ + private readonly PhonebookContext _dbContext; + private readonly IFileReaderService _fileReader; + public DataSeederService(PhonebookContext dbContext, IFileReaderService fileReader) + { + _dbContext = dbContext; + _fileReader = fileReader; + } + + public void SeedIfEmpty() + { + + if (!_dbContext.Contacts.Any()) + { + var path = Path.Combine(Directory.GetCurrentDirectory(), "DocFiles"); + var contacts = _fileReader.FormatFile(path, "SeedData.xlsx"); + _dbContext.Contacts.AddRange(contacts); + _dbContext.SaveChanges(); + } + } +} \ No newline at end of file diff --git a/DocumentProcessor.davetn657/Services/ExportDataService.cs b/DocumentProcessor.davetn657/Services/ExportDataService.cs new file mode 100644 index 0000000..7d97605 --- /dev/null +++ b/DocumentProcessor.davetn657/Services/ExportDataService.cs @@ -0,0 +1,129 @@ +using DocumentProcessor.davetn657.Data; +using IronXL; +using IronPdf; +using Spectre.Console; +using System.Text; +using IronSoftware.Abstractions.Pdf; + +namespace DocumentProcessor.davetn657.Services; + +public interface IExportDataService +{ + void ExportToPdf(); + void ExportToXlsx(); + void ExportToCsv(); +} + +public class ExportDataService : IExportDataService +{ + private readonly PhonebookContext _dbContext; + private readonly IExtensibleRenderer _renderer; + public ExportDataService(PhonebookContext dbContext, IExtensibleRenderer renderer) + { + _dbContext = dbContext; + _renderer = renderer; + } + + public void ExportToPdf() + { + try + { + var _renderer = new ChromePdfRenderer(); + + var htmlContent = @$" + + + + + + + + + + + + + + {HtmlTables()} +
NamePhone NumberEmailCategory
+ + "; + + var pdf = _renderer.RenderHtmlAsPdf(htmlContent); + + pdf.SaveAs("DocFiles\\Contacts.pdf"); + AnsiConsole.WriteLine("Successfully exported to pdf"); + } + catch + { + AnsiConsole.WriteLine("Failed to fully export to pdf - data may be missing or incomplete!"); + } + } + + public void ExportToXlsx() + { + ExportWorkBook(wb => wb.SaveAs("DocFiles\\Contacts.xlsx")); + } + + public void ExportToCsv() + { + ExportWorkBook(wb => wb.SaveAsCsv("DocFiles\\Contacts.csv")); + } + + private string HtmlTables() + { + var contacts = _dbContext.Contacts; + var htmlTableContent = new StringBuilder(); + + foreach(var contact in contacts) + { + htmlTableContent.Append(@$" + + {contact.Name} + {contact.PhoneNumber} + {contact.Email} + {contact.Category} + + "); + } + + return htmlTableContent.ToString(); + } + + private void ExportWorkBook(Action save) + { + try + { + var contacts = _dbContext.Contacts.ToList(); + + var workbook = WorkBook.Create(ExcelFileFormat.XLSX); + var worksheet = workbook.CreateWorkSheet("Contacts"); + + worksheet["A1"].Value = "Names"; + worksheet["B1"].Value = "Phone Numbers"; + worksheet["C1"].Value = "Emails"; + worksheet["D1"].Value = "Categories"; + + for (int i = 0; i < contacts.Count; i++) + { + var row = i + 2; + worksheet["A" + row].Value = contacts[i].Name; + worksheet["B" + row].Value = contacts[i].PhoneNumber; + worksheet["C" + row].Value = contacts[i].Email; + worksheet["D" + row].Value = contacts[i].Category; + } + + save(workbook); + AnsiConsole.WriteLine("Successfully exported workbook"); + } + catch + { + AnsiConsole.WriteLine("Failed to fully export workbook - some data may be missing or incomplete!"); + } + } +} \ No newline at end of file diff --git a/DocumentProcessor.davetn657/Services/FileReaderService.cs b/DocumentProcessor.davetn657/Services/FileReaderService.cs new file mode 100644 index 0000000..6b840b6 --- /dev/null +++ b/DocumentProcessor.davetn657/Services/FileReaderService.cs @@ -0,0 +1,110 @@ +using DocumentProcessor.davetn657.Data.Models; +using IronXL; +using Spectre.Console; + +namespace DocumentProcessor.davetn657.Services; + +public interface IFileReaderService +{ + public List FormatFile(string filePath, string fileName); +} + +public class FileReaderService : IFileReaderService +{ + public FileReaderService() + { + + } + + public List FormatFile(string filePath, string fileName) + { + var fullPath = Path.Combine(filePath, fileName); + var file = fileName.Split('.'); + var fileType = file[1]; + + var properties = new List(); + WorkSheet? workSheet; + + switch (fileType) + { + case "xlsx": + workSheet = ReadXlsxFile(fullPath); + break; + case "csv": + workSheet = ReadCsvFile(fullPath); + break; + default: + workSheet = null; + break; + } + + if (workSheet == null) + { + AnsiConsole.WriteLine("File is empty or could not open!"); + AnsiConsole.Prompt(new TextPrompt("Return?").AllowEmpty()); + return []; + } + properties = FormatWorkSheetData(workSheet); + + return properties; + } + + private WorkSheet? ReadXlsxFile(string filePath) + { + if (!File.Exists(filePath)) + { + Console.WriteLine("File doesn't exist"); + return null; + } + + var workBook = WorkBook.Load(filePath); + var workSheet = workBook.WorkSheets.First(); + + return workSheet; + } + + private WorkSheet? ReadCsvFile(string filePath) + { + if (!File.Exists(filePath)) + { + Console.WriteLine("File doesn't exist"); + return null; + } + + var workBook = WorkBook.LoadCSV(filePath, ExcelFileFormat.XLSX, listDelimiter: ","); + var workSheet = workBook.DefaultWorkSheet; + + return workSheet; + } + + private List FormatWorkSheetData(WorkSheet sheetData) + { + var properties = new List(); + var rowCount = sheetData.RowCount; + + for (var i = 2; i < rowCount; i++) + { + try + { + var data = new PhonebookProperties + { + Name = sheetData["A" + i].TryGetValue(out var name) ? name : string.Empty, + PhoneNumber = sheetData["B" + i].TryGetValue(out var phone) ? phone : string.Empty, + Email = sheetData["C" + i].TryGetValue(out var email) ? email : string.Empty, + Category = sheetData["D" + i].TryGetValue(out var category) ? category : string.Empty, + }; + + properties.Add(data); + } + catch (Exception ex) + { + AnsiConsole.WriteLine("Could not create property! spreadsheet format is not correct!"); + AnsiConsole.WriteLine(ex.Message); + AnsiConsole.Prompt(new TextPrompt("Return").AllowEmpty()); + break; + } + } + + return properties; + } +} \ No newline at end of file diff --git a/DocumentProcessor.davetn657/Views/UserInterface.cs b/DocumentProcessor.davetn657/Views/UserInterface.cs new file mode 100644 index 0000000..21ce01c --- /dev/null +++ b/DocumentProcessor.davetn657/Views/UserInterface.cs @@ -0,0 +1,151 @@ +using DocumentProcessor.davetn657.Data; +using DocumentProcessor.davetn657.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Spectre.Console; + +namespace DocumentProcessor.davetn657.Views; + +public class UserInterface +{ + private readonly IFileReaderService _fileReader; + private readonly IExportDataService _exporter; + private readonly PhonebookContext _dbContext; + + public UserInterface(IFileReaderService fileReader, IExportDataService exporter, PhonebookContext dbContext) + { + _fileReader = fileReader; + _exporter = exporter; + _dbContext = dbContext; + } + + public void Start() + { + while (true) + { + TitleCard("Main Menu"); + var filePath = Path.Combine(Directory.GetCurrentDirectory(), "DocFiles"); + try + { + var fileNames = Directory.GetFiles(filePath) + .Where(s => s.EndsWith(".xlsx") || s.EndsWith(".csv")); + + var menuOptions = new List() + { + "Exit", + "Export", + "Delete all database data" + }; + + foreach (var file in fileNames) + { + menuOptions.Add(Path.GetFileName(file)); + } + + AnsiConsole.WriteLine("All files in DocFiles directory"); + var selected = AnsiConsole.Prompt(new SelectionPrompt().AddChoices(menuOptions)); + + switch (selected) + { + case "Exit": + return; + case "Export": + ExportData(); + break; + case "Delete all database data": + _dbContext.Contacts.ExecuteDelete(); + _dbContext.SaveChanges(); + break; + default: + FileDetails(selected, filePath); + break; + } + } + catch (DirectoryNotFoundException ex) + { + + AnsiConsole.WriteLine($"Couldn't find file path"); + AnsiConsole.WriteLine("Error: " + ex); + AnsiConsole.WriteLine("Try entering a new path?"); + + var selected = AnsiConsole.Prompt(new SelectionPrompt().AddChoices(new string[] { "Yes", "No" })); + + if (selected.Equals("No")) return; + + filePath = AnsiConsole.Ask("Enter here:"); + } + } + } + + public void FileDetails(string fileName, string filePath) + { + TitleCard(fileName + " Details"); + + var table = new Table(); + var contacts = _fileReader.FormatFile(filePath, fileName); + + table.AddColumns(new string[]{ + "Name", + "Phone Number", + "Email" + }); + + foreach(var contact in contacts.Take(10)) + { + table.AddRow(contact.Name, contact.PhoneNumber, contact.Email); + } + + AnsiConsole.WriteLine("Top Excel Rows:"); + AnsiConsole.Write(table); + + var menuOptions = new List + { + "Import Data", + "Return" + }; + + var selected = AnsiConsole.Prompt(new SelectionPrompt().AddChoices(menuOptions)); + + if (selected.Equals("Return")) return; + + _dbContext.Contacts.AddRange(contacts); + _dbContext.SaveChanges(); + + AnsiConsole.WriteLine("Successfully imported data!"); + AnsiConsole.Prompt(new TextPrompt("Return?")); + } + + public void ExportData() + { + TitleCard("Export Current Data"); + + var menuOptions = new Dictionary() + { + { "Return", null }, + {".pdf", _exporter.ExportToPdf }, + { ".xlsx", _exporter.ExportToXlsx }, + { ".csv", _exporter.ExportToCsv } + }; + + var selected = AnsiConsole.Prompt(new SelectionPrompt().AddChoices(menuOptions.Keys)); + + if (menuOptions.TryGetValue(selected, out var action) && action != null) + { + action(); + } + + AnsiConsole.Prompt(new TextPrompt("Return").AllowEmpty()); + } + + public void TitleCard(string title) + { + var titleCard = new FigletText(title) + { + Justification = Justify.Center, + Color = Color.Blue1 + }; + + AnsiConsole.Clear(); + AnsiConsole.Write(titleCard); + } +} \ No newline at end of file diff --git a/DocumentProcessor.davetn657/appsettings.json b/DocumentProcessor.davetn657/appsettings.json new file mode 100644 index 0000000..1b40b17 --- /dev/null +++ b/DocumentProcessor.davetn657/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source=Phonebook.db" + } +} \ No newline at end of file