diff --git a/eeplus/.gitignore b/eeplus/.gitignore new file mode 100644 index 00000000..2cb90536 --- /dev/null +++ b/eeplus/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +*.sln.iml +.idea/**/* +**/.idea/* \ No newline at end of file diff --git a/eeplus/Apps/EEPlusDemo.cs b/eeplus/Apps/EEPlusDemo.cs new file mode 100644 index 00000000..80623f0d --- /dev/null +++ b/eeplus/Apps/EEPlusDemo.cs @@ -0,0 +1,158 @@ +using Ivy.Client; +using Ivy.Core.Hooks; +using Ivy.Views.Forms; + +namespace EEPlusSoftware.Apps; + +[App(icon: Icons.Box, title: "EEPlus Software Demo")] +public class EEPlusDemo : ViewBase +{ + private static string GetExcelFilePath() => + System.IO.Path.Combine(System.IO.Path.GetTempPath(), "books.xlsx"); + + private static void EnsureExcelFileExists(string filePath) + { + if (!File.Exists(filePath)) + { + using var package = new ExcelPackage(); + package.Workbook.Worksheets.Add("Books"); + package.SaveAs(new FileInfo(filePath)); + } + } + public override object? Build() + { + var filePath = GetExcelFilePath(); + var client = UseService(); + + // Ensure file exists + EnsureExcelFileExists(filePath); + + var booksState = UseState>(() => ExcelManipulation.ReadExcel()); + + + var downloadUrl = this.UseDownload( + async () => + { + // Read the file into memory + byte[] fileBytes = await File.ReadAllBytesAsync(filePath); + + // Return as bytes + return fileBytes; + }, + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + $"books-{DateTime.UtcNow:yyyy-MM-dd}.xlsx" + ); + + var downloadBtn = new Button("Download Excel") + .Primary().Icon(Icons.Download) + .Url(downloadUrl.Value); + + var book = UseState(() => new Book()); + var formBuilder = book.ToForm().Remove(x => x.ID) + .Label(m => m.Title, "Title") + .Builder(m => m.Title, s => s.ToTextInput().Placeholder("Insert title...")) + .Builder(m => m.Author, s => s.ToTextInput().Placeholder("Insert author...")) + .Builder(m => m.Year, s => s.ToNumberInput().Placeholder("Insert year...").Min(0)) + .Required(m => m.Title, m => m.Author, m => m.Year); + + var (onSubmit, formView, validationView, loading) = formBuilder.UseForm(this.Context); + + return Layout.Vertical() + | Text.H2("EEPlus Software Demo") | + new Button("Generate Excel File").HandleClick(_ => ExcelManipulation.WriteExcel(booksState)).Loading(loading).Disabled(loading) + | booksState.Value.ToTable() + .Width(Size.Full()) + .Builder(p => p.Title, f => f.Text()) + .Builder(p => p.Author, f => f.Text()) + .Builder(p => p.Year, f => f.Default()) + | downloadBtn | new Button("Delete All Records").HandleClick(_ => HandleDeleteAsync(booksState, filePath, client)) + .Loading(loading).Disabled(loading) + | formView | new Button("Save Book").HandleClick(async _ => await HandleSubmitAsync(booksState, client, book, onSubmit)) + .Loading(loading).Disabled(loading) + | validationView; + + + } + + #region Handle + async ValueTask HandleSubmitAsync(IState> booksState, IClientProvider client, IState book, Func> onSubmit) + { + try + { + if (await onSubmit()) + { + var filePath = GetExcelFilePath(); + EnsureExcelFileExists(filePath); + + using (var package = new ExcelPackage(new FileInfo(filePath))) + { + var ws = package.Workbook.Worksheets[0]; + int nextRow = ws.Dimension == null ? 2 : ws.Dimension.End.Row + 1; + var bookRecord = new Book(book.Value.Title, book.Value.Author, book.Value.Year); + + ws.Cells[nextRow, 1].Value = (nextRow - 1).ToString(); + ws.Cells[nextRow, 2].Value = bookRecord.Title; + ws.Cells[nextRow, 3].Value = bookRecord.Author; + ws.Cells[nextRow, 4].Value = bookRecord.Year; + ws.Cells[1, 1, nextRow, 4].AutoFitColumns(); + + package.Save(); + } + + booksState.Value = ExcelManipulation.ReadExcel(); + book.Value = new Book(); + client.Toast("Book added!"); + } + } + catch (IOException ex) + { + client.Toast($"File access error. Please try again. Technical error: {ex.Message}"); + // Log ex.Message or ex.ToString() as needed + } + catch (Exception ex) + { + client.Toast($"An unexpected error occurred. Technical error: {ex.Message}"); + // Log ex.Message or ex.ToString() as needed + } + } + + void HandleDeleteAsync(IState> booksState, string filePath, IClientProvider client) + { + try + { + EnsureExcelFileExists(filePath); + + using var package = new ExcelPackage(new FileInfo(filePath)); + var ws = package.Workbook.Worksheets.FirstOrDefault(); + if (ws == null) + { + client.Toast("Worksheet not found."); + return; + } + + var lastRow = ws.Dimension?.End.Row ?? 0; + if (lastRow <= 1) + { + client.Toast("No records to clear."); + return; + } + + ws.DeleteRow(2, lastRow - 1); + ws.Cells[1, 1, 1, 4].AutoFitColumns(); + package.Save(); + + client.Toast("All records cleared."); + booksState.Value = ExcelManipulation.ReadExcel(); + } + catch (IOException ex) + { + client.Toast($"File access error. Please try again. Technical error: {ex.Message}"); + } + catch (Exception ex) + { + client.Toast($"An unexpected error occurred. Technical error: {ex.Message}"); + } + } + #endregion Handle + +} \ No newline at end of file diff --git a/eeplus/EEPlusSoftware.csproj b/eeplus/EEPlusSoftware.csproj new file mode 100644 index 00000000..bd5dee55 --- /dev/null +++ b/eeplus/EEPlusSoftware.csproj @@ -0,0 +1,25 @@ + + + + Exe + net9.0 + enable + enable + CS8618;CS8603;CS8602;CS8604;CS9113 + EEPlusSoftware + + + + + + + + + + + + + + + + diff --git a/eeplus/Entities/Book.cs b/eeplus/Entities/Book.cs new file mode 100644 index 00000000..453d868e --- /dev/null +++ b/eeplus/Entities/Book.cs @@ -0,0 +1,40 @@ +namespace EEPlusSoftware.Entities; + +[AttributeUsage(AttributeTargets.All)] +public class Column : System.Attribute +{ + public int ColumnIndex { get; set; } + + public Column(int column) + { + ColumnIndex = column; + } +} +public class Book +{ + [@Column(1)] + [Required] + public string ID { get; set; } + + [@Column(2)] + [Required] + public string Title { get; set; } + [@Column(3)] + [Required] + public string Author { get; set; } + [@Column(4)] + [Required] + public int Year { get; set; } + + public Book(string title, string author, int year) + { + Title = title; + Author = author; + Year = year; + } + + public Book() + { + + } +}; diff --git a/eeplus/GlobalUsings.cs b/eeplus/GlobalUsings.cs new file mode 100644 index 00000000..359c2883 --- /dev/null +++ b/eeplus/GlobalUsings.cs @@ -0,0 +1,18 @@ +global using EEPlusSoftware.Entities; +global using EEPlusSoftware.Helpers; +global using Ivy; +global using Ivy.Apps; +global using Ivy.Chrome; +global using Ivy.Core; +global using Ivy.Hooks; +global using Ivy.Shared; +global using Ivy.Views; +global using Ivy.Views.Builders; +global using Ivy.Views.Tables; +global using OfficeOpenXml; +global using OfficeOpenXml.Style; +global using System.ComponentModel.DataAnnotations; +global using System.Globalization; +global using System.Reactive.Linq; +global using System.Reflection; + diff --git a/eeplus/Helpers/EPPLusExtensions.cs b/eeplus/Helpers/EPPLusExtensions.cs new file mode 100644 index 00000000..31996847 --- /dev/null +++ b/eeplus/Helpers/EPPLusExtensions.cs @@ -0,0 +1,68 @@ +namespace EEPlusSoftware.Helpers; + +public static class EPPLusExtensions +{ + public static IEnumerable ConvertSheetToObjects(this ExcelWorksheet worksheet) where T : new() + { + + Func columnOnly = y => y.AttributeType == typeof(Column); + + var columns = typeof(T) + .GetProperties() + .Where(x => x.CustomAttributes.Any(columnOnly)) + .Select(p => new + { + Property = p, + Column = p.GetCustomAttributes().First().ColumnIndex //safe because of the where clause above + }).ToList(); + + + var rows = worksheet.Cells + .Select(cell => cell.Start.Row) + .Distinct() + .OrderBy(x => x); + + + //Create the collection container + var collection = rows.Skip(1) + .Select(row => + { + var tnew = new T(); + columns.ForEach(col => + { + //This is the real wrinkle to using reflection - Excel stores all numbers as double including int + var val = worksheet.Cells[row, col.Column]; + //If it is numeric it is a double since that is how excel stores all numbers + if (val.Value == null) + { + col.Property.SetValue(tnew, null); + return; + } + if (col.Property.PropertyType == typeof(Int32)) + { + col.Property.SetValue(tnew, val.GetValue()); + return; + } + if (col.Property.PropertyType == typeof(double)) + { + col.Property.SetValue(tnew, val.GetValue()); + return; + } + if (col.Property.PropertyType == typeof(DateTime)) + { + col.Property.SetValue(tnew, val.GetValue()); + return; + } + //Its a string + col.Property.SetValue(tnew, val.GetValue()); + }); + + return tnew; + }); + + + //Send it back + return collection; + } +} + diff --git a/eeplus/Helpers/ExcelManipulation.cs b/eeplus/Helpers/ExcelManipulation.cs new file mode 100644 index 00000000..d9e5f8a7 --- /dev/null +++ b/eeplus/Helpers/ExcelManipulation.cs @@ -0,0 +1,98 @@ +using Ivy.Core.Hooks; + +namespace EEPlusSoftware.Helpers; + +public static class ExcelManipulation +{ + //using the EPPlus library to generate an Excel file + public static void WriteExcel(IState> booksState) + { + Book[] books = booksState.Value.ToArray(); + if (books is null || books.Count() == 0) + { + books = new[] + { + new Book("The Great Gatsby", "F. Scott Fitzgerald", 1925), + new Book("To Kill a Mockingbird", "Harper Lee", 1960), + new Book("1984", "George Orwell", 1949), + new Book("Pride and Prejudice", "Jane Austen", 1813), + new Book("The Catcher in the Rye", "J.D. Salinger", 1951) + }; + } + //Creating an instance of ExcelPackage + ExcelPackage excel = new ExcelPackage(); + + //name of the sheet + var worksheet = excel.Workbook.Worksheets.Add("Books"); + + //setting the properties of the sheet + worksheet.TabColor = System.Drawing.Color.Black; + worksheet.DefaultRowHeight = 12; + + // Setting the properties of the first row + worksheet.Row(1).Height = 20; + worksheet.Row(1).Style.HorizontalAlignment = ExcelHorizontalAlignment.Center; + worksheet.Row(1).Style.Font.Bold = true; + + // Header of the Excel sheet + worksheet.Cells[1, 1].Value = "ID"; + worksheet.Cells[1, 2].Value = "Title"; + worksheet.Cells[1, 3].Value = "Author"; + worksheet.Cells[1, 4].Value = "Year"; + + // Inserting the article data into excel sheet by using the for each loop + // As we have values to the first row we will start with second row + int recordIndex = 2; + + foreach (var article in books) + { + worksheet.Cells[recordIndex, 1].Value = (recordIndex - 1).ToString(); + worksheet.Cells[recordIndex, 2].Value = article.Title; + worksheet.Cells[recordIndex, 3].Value = article.Author; + worksheet.Cells[recordIndex, 4].Value = article.Year; + recordIndex++; + } + + // By default, the column width is not set to auto fit for the content of the range, so we are using AutoFit() method here. + worksheet.Column(1).AutoFit(); + worksheet.Column(2).AutoFit(); + worksheet.Column(3).AutoFit(); + worksheet.Column(4).AutoFit(); + + // file name with .xlsx extension + string p_strPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "books.xlsx"); + + if (File.Exists(p_strPath)) + File.Delete(p_strPath); + + // Create excel file on physical disk + FileStream objFileStrm = File.Create(p_strPath); + objFileStrm.Close(); + + // Write content to excel file + File.WriteAllBytes(p_strPath, excel.GetAsByteArray()); + //Close Excel package + excel.Dispose(); + // refresh reactive books list + booksState.Value = ExcelManipulation.ReadExcel(); + } + + //using the EEPlus library to read an Excel file + public static List ReadExcel() + { + string _fileName = "books.xlsx"; + string filePath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), _fileName); + var RESOURCES_WORKSHEET = 0; + //Creating an instance of ExcelPackage + using (FileStream fileStream = new FileStream(filePath, FileMode.Open)) + { + ExcelPackage excel = new ExcelPackage(fileStream); + var workSheet = excel.Workbook.Worksheets[RESOURCES_WORKSHEET]; + + IEnumerable newcollection = workSheet.ConvertSheetToObjects(); + return newcollection.ToList(); + } + } + + +} diff --git a/eeplus/Program.cs b/eeplus/Program.cs new file mode 100644 index 00000000..68c37ce0 --- /dev/null +++ b/eeplus/Program.cs @@ -0,0 +1,15 @@ + +CultureInfo.DefaultThreadCurrentCulture = CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo("en-US"); +var server = new Server(); +#if DEBUG +server.UseHotReload(); +#endif +server.AddAppsFromAssembly(); +server.AddConnectionsFromAssembly(); +var chromeSettings = new ChromeSettings() + + .UseTabs(preventDuplicates: true); +server.UseChrome(chromeSettings); + +ExcelPackage.License.SetNonCommercialOrganization("Ivy"); +await server.RunAsync(); diff --git a/eeplus/README.md b/eeplus/README.md new file mode 100644 index 00000000..e401172e --- /dev/null +++ b/eeplus/README.md @@ -0,0 +1,17 @@ +# EEPlusSoftware + +Web application created using [Ivy](https://github.com/Ivy-Interactive/Ivy). + +Ivy is a web framework for building interactive web applications using C# and .NET. + +## Run + +``` +dotnet watch +``` + +## Deploy + +``` +ivy deploy +``` \ No newline at end of file