diff --git a/Controllers/OrderController.cs b/Controllers/OrderController.cs index 412bcf3..316f89c 100644 --- a/Controllers/OrderController.cs +++ b/Controllers/OrderController.cs @@ -94,15 +94,15 @@ public class OrderController : Controller if (order == null) return NotFound("Order not found"); - if (order.IsClosed) - return BadRequest("This order is closed"); + if (order.IsClosed || order.IsCompleted) + return BadRequest("This order cannot be joined"); return View(order); } // POST: Order/AddItem [HttpPost] - public async Task AddItem(int orderId, int menuItemId, int quantity, string participantName, string? participantEmail) + public async Task AddItem(int orderId, int menuItemId, int quantity) { var order = await _context.Orders.FindAsync(orderId); if (order == null || order.IsClosed) @@ -117,8 +117,8 @@ public class OrderController : Controller OrderId = orderId, MenuItemId = menuItemId, Quantity = quantity, - ParticipantName = participantName, - ParticipantEmail = participantEmail + ParticipantName = User.Identity?.Name ?? "", + ParticipantEmail = User.Identity?.Name }; _context.OrderItems.Add(orderItem); @@ -152,6 +152,9 @@ public class OrderController : Controller if (order.IsClosed) return BadRequest("Order is already closed"); + if (order.CreatorName != User.Identity?.Name) + return Forbid("Only the order creator can close this order"); + order.IsClosed = true; order.ClosedAt = DateTime.UtcNow; @@ -161,6 +164,33 @@ public class OrderController : Controller return RedirectToAction("Details", new { code = order.OrderCode }); } + // GET: Order/Complete/{code} + public async Task Complete(string code) + { + var order = await _context.Orders.FirstOrDefaultAsync(o => o.OrderCode == code); + + if (order == null) + return NotFound("Order not found"); + + // Only the creator can complete an order + if (order.CreatorName != User.Identity?.Name) + return Forbid("Only the order creator can mark this as completed"); + + if (!order.IsClosed) + return BadRequest("Order must be closed before marking as completed"); + + if (order.IsCompleted) + return BadRequest("Order is already marked as completed"); + + order.IsCompleted = true; + order.CompletedAt = DateTime.UtcNow; + + _context.Orders.Update(order); + await _context.SaveChangesAsync(); + + return RedirectToAction("Details", new { code = order.OrderCode }); + } + private string GenerateOrderCode() { return Guid.NewGuid().ToString().Substring(0, 8).ToUpper(); diff --git a/Migrations/20251130093404_AddAdditionalInfoAndImageToOrder.cs b/Migrations/20251130093404_AddAdditionalInfoAndImageToOrder.cs index 8d43238..e44f02a 100644 --- a/Migrations/20251130093404_AddAdditionalInfoAndImageToOrder.cs +++ b/Migrations/20251130093404_AddAdditionalInfoAndImageToOrder.cs @@ -10,13 +10,50 @@ namespace OneForMe.Migrations /// protected override void Up(MigrationBuilder migrationBuilder) { + migrationBuilder.AddColumn( + name: "AdditionalInfo", + table: "Orders", + type: "TEXT", + nullable: true); + migrationBuilder.AddColumn( + name: "ImagePath", + table: "Orders", + type: "TEXT", + nullable: true); + + migrationBuilder.AddColumn( + name: "IsCompleted", + table: "Orders", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "CompletedAt", + table: "Orders", + type: "TEXT", + nullable: true); } /// protected override void Down(MigrationBuilder migrationBuilder) { + migrationBuilder.DropColumn( + name: "AdditionalInfo", + table: "Orders"); + migrationBuilder.DropColumn( + name: "ImagePath", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "IsCompleted", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "CompletedAt", + table: "Orders"); } } } diff --git a/Migrations/20251130110018_AddCompletedStateToOrder.Designer.cs b/Migrations/20251130110018_AddCompletedStateToOrder.Designer.cs new file mode 100644 index 0000000..584e472 --- /dev/null +++ b/Migrations/20251130110018_AddCompletedStateToOrder.Designer.cs @@ -0,0 +1,414 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using OneForMe.Data; + +#nullable disable + +namespace OneForMe.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20251130110018_AddCompletedStateToOrder")] + partial class AddCompletedStateToOrder + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.0-rc.2.25502.107"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("OneForMe.Models.MenuItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrderId") + .HasColumnType("INTEGER"); + + b.Property("Price") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrderId"); + + b.ToTable("MenuItems"); + }); + + modelBuilder.Entity("OneForMe.Models.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalInfo") + .HasColumnType("TEXT"); + + b.Property("ClosedAt") + .HasColumnType("TEXT"); + + b.Property("CompletedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ImagePath") + .HasColumnType("TEXT"); + + b.Property("IsClosed") + .HasColumnType("INTEGER"); + + b.Property("IsCompleted") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrderCode") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrderCode") + .IsUnique(); + + b.ToTable("Orders"); + }); + + modelBuilder.Entity("OneForMe.Models.OrderItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("MenuItemId") + .HasColumnType("INTEGER"); + + b.Property("OrderId") + .HasColumnType("INTEGER"); + + b.Property("OrderedAt") + .HasColumnType("TEXT"); + + b.Property("ParticipantEmail") + .HasColumnType("TEXT"); + + b.Property("ParticipantName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("MenuItemId"); + + b.HasIndex("OrderId"); + + b.ToTable("OrderItems"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("OneForMe.Models.MenuItem", b => + { + b.HasOne("OneForMe.Models.Order", "Order") + .WithMany("MenuItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("OneForMe.Models.OrderItem", b => + { + b.HasOne("OneForMe.Models.MenuItem", "MenuItem") + .WithMany("OrderItems") + .HasForeignKey("MenuItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("OneForMe.Models.Order", "Order") + .WithMany("OrderItems") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MenuItem"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("OneForMe.Models.MenuItem", b => + { + b.Navigation("OrderItems"); + }); + + modelBuilder.Entity("OneForMe.Models.Order", b => + { + b.Navigation("MenuItems"); + + b.Navigation("OrderItems"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Migrations/20251130120000_AddCompletedStateToOrder.cs b/Migrations/20251130120000_AddCompletedStateToOrder.cs new file mode 100644 index 0000000..8602d05 --- /dev/null +++ b/Migrations/20251130120000_AddCompletedStateToOrder.cs @@ -0,0 +1,40 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace OneForMe.Migrations +{ + /// + public partial class AddCompletedStateToOrder : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsCompleted", + table: "Orders", + type: "INTEGER", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "CompletedAt", + table: "Orders", + type: "TEXT", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "IsCompleted", + table: "Orders"); + + migrationBuilder.DropColumn( + name: "CompletedAt", + table: "Orders"); + } + } +} diff --git a/Migrations/ApplicationDbContextModelSnapshot.cs b/Migrations/ApplicationDbContextModelSnapshot.cs index a02e928..8ddfd39 100644 --- a/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/Migrations/ApplicationDbContextModelSnapshot.cs @@ -244,6 +244,9 @@ namespace OneForMe.Migrations b.Property("ClosedAt") .HasColumnType("TEXT"); + b.Property("CompletedAt") + .HasColumnType("TEXT"); + b.Property("CreatedAt") .HasColumnType("TEXT"); @@ -257,6 +260,9 @@ namespace OneForMe.Migrations b.Property("IsClosed") .HasColumnType("INTEGER"); + b.Property("IsCompleted") + .HasColumnType("INTEGER"); + b.Property("Name") .IsRequired() .HasColumnType("TEXT"); diff --git a/Models/Order.cs b/Models/Order.cs index 67fd1b4..abb3f3e 100644 --- a/Models/Order.cs +++ b/Models/Order.cs @@ -11,6 +11,8 @@ public class Order public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime? ClosedAt { get; set; } public bool IsClosed { get; set; } = false; + public DateTime? CompletedAt { get; set; } + public bool IsCompleted { get; set; } = false; public ICollection MenuItems { get; set; } = new List(); public ICollection OrderItems { get; set; } = new List(); diff --git a/Resources/SharedResources.de.resx b/Resources/SharedResources.de.resx index fd217a3..f9e7ed0 100644 --- a/Resources/SharedResources.de.resx +++ b/Resources/SharedResources.de.resx @@ -99,6 +99,9 @@ Gesamt + + Erfasster Betrag + Offener Betrag @@ -294,4 +297,13 @@ Erstellt am + + Geliefert + + + Als geliefert markieren + + + Erfüllt + \ No newline at end of file diff --git a/Resources/SharedResources.en.resx b/Resources/SharedResources.en.resx index a3c2e07..913906c 100644 --- a/Resources/SharedResources.en.resx +++ b/Resources/SharedResources.en.resx @@ -99,6 +99,9 @@ Total + + Recorded Amount + Outstanding amount @@ -291,4 +294,13 @@ Created on + + Completed + + + Mark as Completed + + + Fulfilled + \ No newline at end of file diff --git a/Views/Home/Dashboard.cshtml b/Views/Home/Dashboard.cshtml index c3d40a7..97be75e 100644 --- a/Views/Home/Dashboard.cshtml +++ b/Views/Home/Dashboard.cshtml @@ -55,7 +55,7 @@
-
@order.Name
+
@order.Name @Localizer.Get(order.IsClosed ? (order.IsCompleted ? "Completed" : "Closed") : "Open")

@Localizer.Get("Code"): @order.OrderCode
@Localizer.Get("Created"): @order.CreatedAt.ToString("MMM dd, yyyy HH:mm")
@@ -64,14 +64,6 @@

@Localizer.Get("Total"): @Localizer["Currency", order.OrderItems.Sum(oi => oi.MenuItem.Price * oi.Quantity).ToString("F2")]

@Localizer.Get("View") - @if (!order.IsClosed) - { - @Localizer.Get("CloseOrder") - } - else - { - @Localizer.Get("Closed") - }
diff --git a/Views/Order/Details.cshtml b/Views/Order/Details.cshtml index d705462..524141b 100644 --- a/Views/Order/Details.cshtml +++ b/Views/Order/Details.cshtml @@ -24,7 +24,40 @@ {

@Model.AdditionalInfo

} -

@Localizer.Get("Status"): @(Model.IsClosed ? Localizer.Get("Closed") : Localizer.Get("Open"))

+

@Localizer.Get("Status"): +

+ +
+
Offen
+
+ +
+ +
+
Abgeschlossen
+
+ +
+ +
+
Geliefert
+
+
+

+
+ @if (!Model.IsClosed && Model.CreatorName == User.Identity?.Name) + { + @Localizer.Get("CloseOrder") + } + @if (Model.IsClosed && !Model.IsCompleted && Model.CreatorName == User.Identity?.Name) + { + @Localizer.Get("MarkCompleted") + } + @if (Model.IsCompleted) + { + + } +
@@ -117,15 +150,6 @@

@Localizer.Get("TotalRevenue"): @Localizer["Currency", Model.OrderItems.Sum(oi => oi.MenuItem.Price * oi.Quantity).ToString("F2")]

- - @if (!Model.IsClosed) - { - @Localizer.Get("CloseOrder") - } - else - { -
@Localizer.Get("Closed")
- } diff --git a/Views/Order/Join.cshtml b/Views/Order/Join.cshtml index c3d8673..4c1e0d6 100644 --- a/Views/Order/Join.cshtml +++ b/Views/Order/Join.cshtml @@ -38,16 +38,6 @@
-
- - -
- -
- - -
-