Added Bestellzustände

This commit is contained in:
2025-11-30 13:19:16 +01:00
parent de5e6be734
commit 03c5d69c98
11 changed files with 593 additions and 34 deletions

View File

@@ -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<IActionResult> AddItem(int orderId, int menuItemId, int quantity, string participantName, string? participantEmail)
public async Task<IActionResult> 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<IActionResult> 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();

View File

@@ -10,13 +10,50 @@ namespace OneForMe.Migrations
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "AdditionalInfo",
table: "Orders",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ImagePath",
table: "Orders",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "IsCompleted",
table: "Orders",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<DateTime>(
name: "CompletedAt",
table: "Orders",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
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");
}
}
}

View File

@@ -0,0 +1,414 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<string>("Id")
.HasColumnType("TEXT");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("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<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b =>
{
b.Property<string>("Id")
.HasColumnType("TEXT");
b.Property<int>("AccessFailedCount")
.HasColumnType("INTEGER");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("TEXT");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<bool>("EmailConfirmed")
.HasColumnType("INTEGER");
b.Property<bool>("LockoutEnabled")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("TEXT");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("PasswordHash")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("INTEGER");
b.Property<string>("SecurityStamp")
.HasColumnType("TEXT");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("INTEGER");
b.Property<string>("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<string>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AspNetUserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("ProviderKey")
.HasColumnType("TEXT");
b.Property<string>("ProviderDisplayName")
.HasColumnType("TEXT");
b.Property<string>("UserId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("AspNetUserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("RoleId")
.HasColumnType("TEXT");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("AspNetUserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
{
b.Property<string>("UserId")
.HasColumnType("TEXT");
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("AspNetUserTokens", (string)null);
});
modelBuilder.Entity("OneForMe.Models.MenuItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("OrderId")
.HasColumnType("INTEGER");
b.Property<decimal>("Price")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("OrderId");
b.ToTable("MenuItems");
});
modelBuilder.Entity("OneForMe.Models.Order", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AdditionalInfo")
.HasColumnType("TEXT");
b.Property<DateTime?>("ClosedAt")
.HasColumnType("TEXT");
b.Property<DateTime?>("CompletedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("CreatorName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ImagePath")
.HasColumnType("TEXT");
b.Property<bool>("IsClosed")
.HasColumnType("INTEGER");
b.Property<bool>("IsCompleted")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("OrderCode")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("OrderCode")
.IsUnique();
b.ToTable("Orders");
});
modelBuilder.Entity("OneForMe.Models.OrderItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("MenuItemId")
.HasColumnType("INTEGER");
b.Property<int>("OrderId")
.HasColumnType("INTEGER");
b.Property<DateTime>("OrderedAt")
.HasColumnType("TEXT");
b.Property<string>("ParticipantEmail")
.HasColumnType("TEXT");
b.Property<string>("ParticipantName")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Quantity")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("MenuItemId");
b.HasIndex("OrderId");
b.ToTable("OrderItems");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
{
b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", 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<string>", 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
}
}
}

View File

@@ -0,0 +1,40 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace OneForMe.Migrations
{
/// <inheritdoc />
public partial class AddCompletedStateToOrder : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsCompleted",
table: "Orders",
type: "INTEGER",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<DateTime>(
name: "CompletedAt",
table: "Orders",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsCompleted",
table: "Orders");
migrationBuilder.DropColumn(
name: "CompletedAt",
table: "Orders");
}
}
}

View File

@@ -244,6 +244,9 @@ namespace OneForMe.Migrations
b.Property<DateTime?>("ClosedAt")
.HasColumnType("TEXT");
b.Property<DateTime?>("CompletedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
@@ -257,6 +260,9 @@ namespace OneForMe.Migrations
b.Property<bool>("IsClosed")
.HasColumnType("INTEGER");
b.Property<bool>("IsCompleted")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");

View File

@@ -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<MenuItem> MenuItems { get; set; } = new List<MenuItem>();
public ICollection<OrderItem> OrderItems { get; set; } = new List<OrderItem>();

View File

@@ -99,6 +99,9 @@
<data name="Total" xml:space="preserve">
<value>Gesamt</value>
</data>
<data name="RecordedAmount" xml:space="preserve">
<value>Erfasster Betrag</value>
</data>
<data name="IOwe" xml:space="preserve">
<value>Offener Betrag</value>
</data>
@@ -294,4 +297,13 @@
<data name="CreatedOn" xml:space="preserve">
<value>Erstellt am</value>
</data>
<data name="Completed" xml:space="preserve">
<value>Geliefert</value>
</data>
<data name="MarkCompleted" xml:space="preserve">
<value>Als geliefert markieren</value>
</data>
<data name="Fulfilled" xml:space="preserve">
<value>Erfüllt</value>
</data>
</root>

View File

@@ -99,6 +99,9 @@
<data name="Total" xml:space="preserve">
<value>Total</value>
</data>
<data name="RecordedAmount" xml:space="preserve">
<value>Recorded Amount</value>
</data>
<data name="IOwe" xml:space="preserve">
<value>Outstanding amount</value>
</data>
@@ -291,4 +294,13 @@
<data name="CreatedOn" xml:space="preserve">
<value>Created on</value>
</data>
<data name="Completed" xml:space="preserve">
<value>Completed</value>
</data>
<data name="MarkCompleted" xml:space="preserve">
<value>Mark as Completed</value>
</data>
<data name="Fulfilled" xml:space="preserve">
<value>Fulfilled</value>
</data>
</root>

View File

@@ -55,7 +55,7 @@
<div class="col-md-6 mb-3">
<div class="card shadow">
<div class="card-body">
<h5 class="card-title">@order.Name</h5>
<h5 class="card-title">@order.Name <span class="badge @(order.IsCompleted ? "bg-success" : "bg-warning text-dark")" style="float: right">@Localizer.Get(order.IsClosed ? (order.IsCompleted ? "Completed" : "Closed") : "Open")</span></h5>
<p class="card-text">
<small class="text-muted">@Localizer.Get("Code"): <strong>@order.OrderCode</strong></small><br>
<small class="text-muted">@Localizer.Get("Created"): @order.CreatedAt.ToString("MMM dd, yyyy HH:mm")</small><br>
@@ -64,14 +64,6 @@
<p class="text-success"><strong>@Localizer.Get("Total"): @Localizer["Currency", order.OrderItems.Sum(oi => oi.MenuItem.Price * oi.Quantity).ToString("F2")]</strong></p>
<div class="d-flex gap-2">
<a href="/order/details?code=@order.OrderCode" class="btn btn-sm btn-primary">@Localizer.Get("View")</a>
@if (!order.IsClosed)
{
<a href="/order/close?code=@order.OrderCode" class="btn btn-sm btn-danger">@Localizer.Get("CloseOrder")</a>
}
else
{
<span class="badge bg-secondary align-content-center">@Localizer.Get("Closed")</span>
}
</div>
</div>
</div>

View File

@@ -24,7 +24,40 @@
{
<p class="border rounded p-3" style="white-space: pre-wrap;">@Model.AdditionalInfo</p>
}
<p>@Localizer.Get("Status"): <span class="badge @(Model.IsClosed ? "bg-danger" : "bg-success")">@(Model.IsClosed ? Localizer.Get("Closed") : Localizer.Get("Open"))</span></p>
<p>@Localizer.Get("Status"):
<div class="d-flex align-items-center gap-3">
<div class="text-center">
<div class="badge bg-success")">Offen</div>
</div>
<div class="flex-grow-1 border-top" style="border-width:3px !important;"></div>
<div class="text-center">
<div class="badge @(Model.IsClosed ? "bg-success" : "bg-danger")">Abgeschlossen</div>
</div>
<div class="flex-grow-1 border-top" style="border-width:3px !important;"></div>
<div class="text-center">
<div class="badge @(Model.IsCompleted ? "bg-success" : "bg-danger")">Geliefert</div>
</div>
</div>
</p>
<div class="d-grid gap-2 mt-4">
@if (!Model.IsClosed && Model.CreatorName == User.Identity?.Name)
{
<a href="/order/close?code=@Model.OrderCode" class="btn btn-warning">@Localizer.Get("CloseOrder")</a>
}
@if (Model.IsClosed && !Model.IsCompleted && Model.CreatorName == User.Identity?.Name)
{
<a href="/order/complete?code=@Model.OrderCode" class="btn btn-success">@Localizer.Get("MarkCompleted")</a>
}
@if (Model.IsCompleted)
{
<button class="btn btn-success" disabled>@Localizer.Get("Completed")</button>
}
</div>
</div>
</div>
@@ -117,15 +150,6 @@
<p>@Localizer.Get("TotalRevenue"): <strong>@Localizer["Currency", Model.OrderItems.Sum(oi => oi.MenuItem.Price * oi.Quantity).ToString("F2")]</strong></p>
</div>
</div>
@if (!Model.IsClosed)
{
<a href="/order/close?code=@Model.OrderCode" class="btn btn-danger w-100">@Localizer.Get("CloseOrder")</a>
}
else
{
<div class="alert alert-danger w-100">@Localizer.Get("Closed")</div>
}
</div>
</div>
</div>

View File

@@ -38,16 +38,6 @@
<form method="post" action="/order/additem">
<input type="hidden" name="orderId" value="@Model.Id">
<div class="mb-3">
<label for="participantName" class="form-label">@Localizer["YourName"]</label>
<input type="text" class="form-control" id="participantName" name="participantName" placeholder="@Localizer["EnterYourName"]" required>
</div>
<div class="mb-3">
<label for="participantEmail" class="form-label">@Localizer["YourEmail"]</label>
<input type="email" class="form-control" id="participantEmail" name="participantEmail" placeholder="@Localizer["EnterYourEmail"]">
</div>
<div class="mb-3">
<label for="menuItemId" class="form-label">@Localizer["SelectItem"]</label>
<select class="form-select" id="menuItemId" name="menuItemId" required onchange="updatePrice()">