diff --git a/Components/Layout/NavMenu.razor b/Components/Layout/NavMenu.razor index 4cefe6d..43782ea 100644 --- a/Components/Layout/NavMenu.razor +++ b/Components/Layout/NavMenu.razor @@ -38,6 +38,14 @@ protected override void OnInitialized() Add + @if (nm.Uri.Contains("admin=")) + { + + } @@ -53,4 +61,9 @@ protected override void OnInitialized() { return _prefix + "/add" + _suffix; } + + private string FinanceLink() + { + return _prefix + "/finance" + _suffix; + } } diff --git a/Components/Layout/NavMenu.razor.css b/Components/Layout/NavMenu.razor.css index 4e15395..b8d8234 100644 --- a/Components/Layout/NavMenu.razor.css +++ b/Components/Layout/NavMenu.razor.css @@ -42,8 +42,8 @@ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); } -.bi-list-nested-nav-menu { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); +.bi-bank-nav-menu { + background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2216%22%20height%3D%2216%22%20fill%3D%22white%22%20class%3D%22bi%20bi-bank%22%20viewBox%3D%220%200%2016%2016%22%3E%0A%20%20%3Cpath%20d%3D%22m8%200%206.61%203h.89a.5.5%200%200%201%20.5.5v2a.5.5%200%200%201-.5.5H15v7a.5.5%200%200%201%20.485.38l.5%202a.498.498%200%200%201-.485.62H.5a.498.498%200%200%201-.485-.62l.5-2A.5.5%200%200%201%201%2013V6H.5a.5.5%200%200%201-.5-.5v-2A.5.5%200%200%201%20.5%203h.89zM3.777%203h8.447L8%201zM2%206v7h1V6zm2%200v7h2.5V6zm3.5%200v7h1V6zm2%200v7H12V6zM13%206v7h1V6zm2-1V4H1v1zm-.39%209H1.39l-.25%201h13.72z%22%2F%3E%0A%3C%2Fsvg%3E"); } .nav-item { diff --git a/Components/Pages/GroupAdd.razor b/Components/Pages/GroupAdd.razor index 1d6ca0e..e12866a 100644 --- a/Components/Pages/GroupAdd.razor +++ b/Components/Pages/GroupAdd.razor @@ -80,6 +80,7 @@ return; } + Order!.PaymentStatus = PaymentStatus.Unpaid; Order!.Group = group; context.Add(Order!); context.SaveChanges(); diff --git a/Components/Pages/GroupFinanze.razor b/Components/Pages/GroupFinanze.razor new file mode 100644 index 0000000..a47b71f --- /dev/null +++ b/Components/Pages/GroupFinanze.razor @@ -0,0 +1,127 @@ +@page "/group/{GroupSlug}/finance" + +@using GroupOrder.Data +@using Microsoft.AspNetCore.WebUtilities +@using Microsoft.EntityFrameworkCore +@rendermode InteractiveServer + +@inject IDbContextFactory DbFactory +@inject NavigationManager nm + +Mampf.Link @Group?.GroupName Finances + +@if (Group != null) +{ +

Finance Overview: @Group?.GroupName

+ + + + + + + + + + + + @foreach (Order order in Group!.Orders) + { + + + + + + } + +
+ Name + + Price + + Payment Status +
+ @order.Name + + @order.getPrice()€ + + + + + + +
+ + +} + +@code { + + private Group? Group { get; set; } + + private bool Loading { get; set; } = false; + private bool NotFound { get; set; } = false; + + [Parameter] + public string? GroupSlug { get; set; } + + private string? AdminCode { get; set; } + + protected override Task OnInitializedAsync() + { + _context = DbFactory.CreateDbContext(); + return base.OnInitializedAsync(); + } + + protected override async Task OnParametersSetAsync() + { + await LoadGroupAsync(); + await base.OnParametersSetAsync(); + } + + GroupContext? _context; + + // Loads the contact. + private async Task LoadGroupAsync() + { + if (nm.Uri.Contains("?") && QueryHelpers.ParseQuery(nm.Uri.Split("?")[1]).TryGetValue("admin", out var code)) + { + AdminCode = code; + } + else + { + NotFound = true; + return; + } + + if (Loading) + { + return; //avoid concurrent requests. + } + + Group = null; + Loading = true; + + if (_context!.Groups is not null) + { + Group = await _context!.Groups + .Include(group => group.Orders) + .SingleOrDefaultAsync( + c => c.GroupSlug == GroupSlug && c.AdminCode == AdminCode); + + if (Group is null) + { + NotFound = true; + } + } + + Loading = false; + } + + private async void Save() + { + if (Group == null) return; + if (Group.AdminCode != AdminCode) return; // this theoretically should not happen + await _context!.SaveChangesAsync(); + } + +} diff --git a/Components/Pages/GroupOverview.razor b/Components/Pages/GroupOverview.razor index f176a49..f27c368 100644 --- a/Components/Pages/GroupOverview.razor +++ b/Components/Pages/GroupOverview.razor @@ -42,12 +42,12 @@ @order.Food - @getPrice(order)€ + @order.getPrice()€ - @if (Group.PaypalUsername != null) + @if (!string.IsNullOrEmpty(Group.PaypalUsername)) { - 💳 + 💳 } @@ -101,10 +101,5 @@ Loading = false; } - - private String getPrice(Order order) - { - return (order.Price / 100) + "." + System.String.Format("{0:D2}", order.Price % 100); - } } diff --git a/Data/Order.cs b/Data/Order.cs index 435715b..e51b689 100644 --- a/Data/Order.cs +++ b/Data/Order.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using System.ComponentModel.DataAnnotations; using Microsoft.EntityFrameworkCore; @@ -20,4 +21,20 @@ public class Order [Required] public int? Price { get; set; } // in cent + + [Required] + [DefaultValue(PaymentStatus.Unpaid)] + public PaymentStatus PaymentStatus { get; set; } + + public String getPrice() + { + return (this.Price / 100) + "." + System.String.Format("{0:D2}", this.Price % 100); + } +} + +public enum PaymentStatus +{ + Unpaid, + PaymentPending, + Paid } \ No newline at end of file diff --git a/Migrations/20240629204340_Finance.Designer.cs b/Migrations/20240629204340_Finance.Designer.cs new file mode 100644 index 0000000..17d27f5 --- /dev/null +++ b/Migrations/20240629204340_Finance.Designer.cs @@ -0,0 +1,111 @@ +// +using System; +using GroupOrder.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GroupOrder.Migrations +{ + [DbContext(typeof(GroupContext))] + [Migration("20240629204340_Finance")] + partial class Finance + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.6"); + + modelBuilder.Entity("GroupOrder.Data.Group", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdminCode") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("GroupSlug") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaypalUsername") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GroupSlug") + .IsUnique(); + + b.ToTable("Groups"); + }); + + modelBuilder.Entity("GroupOrder.Data.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Food") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PaymentStatus") + .HasColumnType("INTEGER"); + + b.Property("Price") + .IsRequired() + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GroupId"); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("GroupOrder.Data.Order", b => + { + b.HasOne("GroupOrder.Data.Group", "Group") + .WithMany("Orders") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("GroupOrder.Data.Group", b => + { + b.Navigation("Orders"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20240629204340_Finance.cs b/Migrations/20240629204340_Finance.cs new file mode 100644 index 0000000..d052c62 --- /dev/null +++ b/Migrations/20240629204340_Finance.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GroupOrder.Migrations +{ + /// + public partial class Finance : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PaymentStatus", + table: "Orders", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PaymentStatus", + table: "Orders"); + } + } +} diff --git a/Migrations/GroupContextModelSnapshot.cs b/Migrations/GroupContextModelSnapshot.cs index 92cada9..db1a2e2 100644 --- a/Migrations/GroupContextModelSnapshot.cs +++ b/Migrations/GroupContextModelSnapshot.cs @@ -73,6 +73,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(100) .HasColumnType("TEXT"); + b.Property("PaymentStatus") + .HasColumnType("INTEGER"); + b.Property("Price") .IsRequired() .HasColumnType("INTEGER"); diff --git a/Program.cs b/Program.cs index ac0d82c..a6f5801 100644 --- a/Program.cs +++ b/Program.cs @@ -11,13 +11,13 @@ var app = builder.Build(); -app.UseExceptionHandler("/Error", createScopeForErrors: true); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); + app.UseExceptionHandler("/Error", createScopeForErrors: true); } app.UseHttpsRedirection();