diff --git a/.gitignore b/.gitignore index 9480a89..9074a90 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ bin obj -*.db \ No newline at end of file +*.db* diff --git a/Controllers/AuthController.cs b/Controllers/AuthController.cs new file mode 100644 index 0000000..f4ce3d6 --- /dev/null +++ b/Controllers/AuthController.cs @@ -0,0 +1,69 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace OneForMe.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AuthController : ControllerBase +{ + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public AuthController(UserManager userManager, SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + /// + /// Register a new user + /// + [HttpPost("register")] + public async Task Register([FromBody] RegisterRequest request) + { + var user = new IdentityUser { UserName = request.Email, Email = request.Email }; + var result = await _userManager.CreateAsync(user, request.Password); + + if (result.Succeeded) + return Ok(new { message = "User registered successfully" }); + + return BadRequest(new { errors = result.Errors.Select(e => e.Description) }); + } + + /// + /// Login user + /// + [HttpPost("login")] + public async Task Login([FromBody] LoginRequest request) + { + var result = await _signInManager.PasswordSignInAsync(request.Email, request.Password, false, false); + + if (result.Succeeded) + return Ok(new { message = "Login successful" }); + + return Unauthorized(new { message = "Invalid email or password" }); + } + + /// + /// Logout user + /// + [HttpPost("logout")] + public async Task Logout() + { + await _signInManager.SignOutAsync(); + return Ok(new { message = "Logout successful" }); + } +} + +public class RegisterRequest +{ + public string Email { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; +} + +public class LoginRequest +{ + public string Email { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Controllers/AuthViewController.cs b/Controllers/AuthViewController.cs new file mode 100644 index 0000000..0403a1e --- /dev/null +++ b/Controllers/AuthViewController.cs @@ -0,0 +1,35 @@ +using System.CodeDom.Compiler; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace OneForMe.Controllers; + +[Route("[controller]")] +public class AuthViewController : Controller +{ + private readonly SignInManager _signInManager; + + public AuthViewController(SignInManager signInManager) + { + _signInManager = signInManager; + } + + [HttpGet("Login")] + public IActionResult Login() + { + return View(); + } + + [HttpGet("Register")] + public IActionResult Register() + { + return View(); + } + + [HttpPost("Logout")] + public async Task Logout() + { + await _signInManager.SignOutAsync(); + return RedirectToAction("Index", "Home"); + } +} \ No newline at end of file diff --git a/Controllers/HomeController.cs b/Controllers/HomeController.cs index a3f8742..9d2c45c 100644 --- a/Controllers/HomeController.cs +++ b/Controllers/HomeController.cs @@ -1,14 +1,56 @@ using System.Diagnostics; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using OneForMe.Data; using OneForMe.Models; namespace OneForMe.Controllers; +[Authorize] public class HomeController : Controller { + private readonly ApplicationDbContext _context; + + public HomeController(ApplicationDbContext context) + { + _context = context; + } + public IActionResult Index() { - return View(); + if (User.Identity?.IsAuthenticated == true) + { + return RedirectToAction("Dashboard"); + } + return RedirectToAction("Login", "AuthViewController"); + } + + public async Task Dashboard() + { + var userEmail = User.Identity?.Name; + + var createdOrders = await _context.Orders + .Where(o => o.CreatorName == userEmail) + .Include(o => o.MenuItems) + .Include(o => o.OrderItems) + .ThenInclude(oi => oi.MenuItem) + .ToListAsync(); + + var joinedOrders = await _context.Orders + .Where(o => o.OrderItems.Any(oi => oi.ParticipantEmail == userEmail || oi.ParticipantName == userEmail)) + .Include(o => o.MenuItems) + .Include(o => o.OrderItems) + .ThenInclude(oi => oi.MenuItem) + .ToListAsync(); + + var viewModel = new DashboardViewModel + { + CreatedOrders = createdOrders, + JoinedOrders = joinedOrders + }; + + return View(viewModel); } public IActionResult Privacy() diff --git a/Controllers/OrderController.cs b/Controllers/OrderController.cs new file mode 100644 index 0000000..34b6a9c --- /dev/null +++ b/Controllers/OrderController.cs @@ -0,0 +1,118 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using OneForMe.Data; +using OneForMe.Models; + +namespace OneForMe.Controllers; + +[Authorize] +public class OrderController : Controller +{ + private readonly ApplicationDbContext _context; + + public OrderController(ApplicationDbContext context) + { + _context = context; + } + + // GET: Order/Create + public IActionResult Create() + { + return View(); + } + + // POST: Order/Create + [HttpPost] + public async Task Create(Order order, string[] itemNames, decimal[] itemPrices) + { + if (!ModelState.IsValid) + return View(); + + order.OrderCode = GenerateOrderCode(); + order.CreatorName = User.Identity?.Name ?? "Unknown"; + + _context.Orders.Add(order); + await _context.SaveChangesAsync(); + + // Add menu items + for (int i = 0; i < itemNames.Length; i++) + { + if (!string.IsNullOrEmpty(itemNames[i]) && itemPrices[i] > 0) + { + _context.MenuItems.Add(new MenuItem + { + OrderId = order.Id, + Name = itemNames[i], + Price = itemPrices[i] + }); + } + } + await _context.SaveChangesAsync(); + + return RedirectToAction("Details", new { code = order.OrderCode }); + } + + // GET: Order/Join + public async Task Join(string code) + { + var order = await _context.Orders + .Include(o => o.MenuItems) + .Include(o => o.OrderItems) + .FirstOrDefaultAsync(o => o.OrderCode == code); + + if (order == null) + return NotFound("Order not found"); + + if (order.IsClosed) + return BadRequest("This order is closed"); + + return View(order); + } + + // POST: Order/AddItem + [HttpPost] + public async Task AddItem(int orderId, int menuItemId, int quantity, string participantName, string? participantEmail) + { + var order = await _context.Orders.FindAsync(orderId); + if (order == null || order.IsClosed) + return BadRequest("Order not found or is closed"); + + var menuItem = await _context.MenuItems.FindAsync(menuItemId); + if (menuItem == null) + return BadRequest("Menu item not found"); + + var orderItem = new OrderItem + { + OrderId = orderId, + MenuItemId = menuItemId, + Quantity = quantity, + ParticipantName = participantName, + ParticipantEmail = participantEmail + }; + + _context.OrderItems.Add(orderItem); + await _context.SaveChangesAsync(); + + return RedirectToAction("Join", new { code = order.OrderCode }); + } + + // GET: Order/Details/{code} + public async Task Details(string code) + { + var order = await _context.Orders + .Include(o => o.MenuItems) + .Include(o => o.OrderItems) + .FirstOrDefaultAsync(o => o.OrderCode == code); + + if (order == null) + return NotFound(); + + return View(order); + } + + private string GenerateOrderCode() + { + return Guid.NewGuid().ToString().Substring(0, 8).ToUpper(); + } +} \ No newline at end of file diff --git a/Data/ApplicationDbContext.cs b/Data/ApplicationDbContext.cs index c1ad5b4..ee53226 100644 --- a/Data/ApplicationDbContext.cs +++ b/Data/ApplicationDbContext.cs @@ -1,9 +1,11 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using OneForMe.Models; namespace OneForMe.Data; -public class ApplicationDbContext : DbContext +public class ApplicationDbContext : IdentityDbContext { public ApplicationDbContext(DbContextOptions options) : base(options) { diff --git a/Migrations/20251129195023_InitialCreate.Designer.cs b/Migrations/20251129195023_InitialCreate.Designer.cs new file mode 100644 index 0000000..967dc5d --- /dev/null +++ b/Migrations/20251129195023_InitialCreate.Designer.cs @@ -0,0 +1,398 @@ +// +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("20251129195023_InitialCreate")] + partial class InitialCreate + { + /// + 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("ClosedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsClosed") + .HasColumnType("INTEGER"); + + 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/20251129195023_InitialCreate.cs b/Migrations/20251129195023_InitialCreate.cs new file mode 100644 index 0000000..ad977b0 --- /dev/null +++ b/Migrations/20251129195023_InitialCreate.cs @@ -0,0 +1,320 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace OneForMe.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + UserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + Email = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "TEXT", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "INTEGER", nullable: false), + PasswordHash = table.Column(type: "TEXT", nullable: true), + SecurityStamp = table.Column(type: "TEXT", nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true), + PhoneNumber = table.Column(type: "TEXT", nullable: true), + PhoneNumberConfirmed = table.Column(type: "INTEGER", nullable: false), + TwoFactorEnabled = table.Column(type: "INTEGER", nullable: false), + LockoutEnd = table.Column(type: "TEXT", nullable: true), + LockoutEnabled = table.Column(type: "INTEGER", nullable: false), + AccessFailedCount = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Orders", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + OrderCode = table.Column(type: "TEXT", nullable: false), + CreatorName = table.Column(type: "TEXT", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + ClosedAt = table.Column(type: "TEXT", nullable: true), + IsClosed = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Orders", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + RoleId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "TEXT", nullable: false), + ProviderKey = table.Column(type: "TEXT", nullable: false), + ProviderDisplayName = table.Column(type: "TEXT", nullable: true), + UserId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "TEXT", nullable: false), + RoleId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "TEXT", nullable: false), + LoginProvider = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Value = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "MenuItems", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + OrderId = table.Column(type: "INTEGER", nullable: false), + Name = table.Column(type: "TEXT", nullable: false), + Price = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MenuItems", x => x.Id); + table.ForeignKey( + name: "FK_MenuItems_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "OrderItems", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + OrderId = table.Column(type: "INTEGER", nullable: false), + MenuItemId = table.Column(type: "INTEGER", nullable: false), + ParticipantName = table.Column(type: "TEXT", nullable: false), + ParticipantEmail = table.Column(type: "TEXT", nullable: true), + Quantity = table.Column(type: "INTEGER", nullable: false), + OrderedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrderItems", x => x.Id); + table.ForeignKey( + name: "FK_OrderItems_MenuItems_MenuItemId", + column: x => x.MenuItemId, + principalTable: "MenuItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_OrderItems_Orders_OrderId", + column: x => x.OrderId, + principalTable: "Orders", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_MenuItems_OrderId", + table: "MenuItems", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderItems_MenuItemId", + table: "OrderItems", + column: "MenuItemId"); + + migrationBuilder.CreateIndex( + name: "IX_OrderItems_OrderId", + table: "OrderItems", + column: "OrderId"); + + migrationBuilder.CreateIndex( + name: "IX_Orders_OrderCode", + table: "Orders", + column: "OrderCode", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "OrderItems"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + + migrationBuilder.DropTable( + name: "MenuItems"); + + migrationBuilder.DropTable( + name: "Orders"); + } + } +} diff --git a/Migrations/ApplicationDbContextModelSnapshot.cs b/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..7f5382a --- /dev/null +++ b/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,395 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using OneForMe.Data; + +#nullable disable + +namespace OneForMe.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(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("ClosedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsClosed") + .HasColumnType("INTEGER"); + + 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/Models/DashboardViewModel.cs b/Models/DashboardViewModel.cs new file mode 100644 index 0000000..ceebaf6 --- /dev/null +++ b/Models/DashboardViewModel.cs @@ -0,0 +1,9 @@ +using OneForMe.Models; + +namespace OneForMe.Models; + +public class DashboardViewModel +{ + public List CreatedOrders { get; set; } = new(); + public List JoinedOrders { get; set; } = new(); +} \ No newline at end of file diff --git a/OneForMe.csproj b/OneForMe.csproj index 8228172..caa42a5 100644 --- a/OneForMe.csproj +++ b/OneForMe.csproj @@ -10,5 +10,12 @@ + + + + + + + diff --git a/Program.cs b/Program.cs index cfd3529..5ac7f84 100644 --- a/Program.cs +++ b/Program.cs @@ -1,36 +1,72 @@ +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using OneForMe.Data; var builder = WebApplication.CreateBuilder(args); // Add services to the container. +builder.Services.AddControllers(); builder.Services.AddControllersWithViews(); +// Add Swagger/OpenAPI +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => +{ + options.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo + { + Title = "OneForMe API", + Version = "v1", + Description = "Group Order Management API" + }); +}); + // Add Entity Framework Core with SQLite builder.Services.AddDbContext(options => options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection") ?? "Data Source=OneForMe.db")); +// Add Identity +builder.Services.AddIdentity(options => +{ + options.Password.RequiredLength = 6; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = false; + options.Password.RequireLowercase = false; + options.Password.RequireDigit = false; +}) +.AddEntityFrameworkStores() +.AddDefaultTokenProviders(); + var app = builder.Build(); -// Create database on startup +// Create and migrate database on startup using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); - db.Database.EnsureCreated(); + db.Database.Migrate(); } // Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/swagger/v1/swagger.json", "OneForMe API v1"); + options.RoutePrefix = "swagger"; + }); +} + if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); - // 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.UseHttpsRedirection(); app.UseRouting(); +app.UseAuthentication(); app.UseAuthorization(); app.MapStaticAssets(); @@ -40,5 +76,6 @@ app.MapControllerRoute( pattern: "{controller=Home}/{action=Index}/{id?}") .WithStaticAssets(); +app.MapControllers(); app.Run(); diff --git a/Views/AuthView/Login.cshtml b/Views/AuthView/Login.cshtml new file mode 100644 index 0000000..f660de0 --- /dev/null +++ b/Views/AuthView/Login.cshtml @@ -0,0 +1,67 @@ +@{ + ViewData["Title"] = "Login"; +} + +
+
+
+
+
+

Login

+ +
+
+ + +
+ +
+ + +
+ + +
+ +
+ +

Don't have an account? Register here

+ +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/Views/AuthView/Register.cshtml b/Views/AuthView/Register.cshtml new file mode 100644 index 0000000..1fdc5c3 --- /dev/null +++ b/Views/AuthView/Register.cshtml @@ -0,0 +1,79 @@ +@{ + ViewData["Title"] = "Register"; +} + +
+
+
+
+
+

Register

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ +
+ +

Already have an account? Login here

+ +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/Views/Home/Dashboard.cshtml b/Views/Home/Dashboard.cshtml new file mode 100644 index 0000000..32899f1 --- /dev/null +++ b/Views/Home/Dashboard.cshtml @@ -0,0 +1,130 @@ +@using OneForMe.Controllers +@model DashboardViewModel + +@{ + ViewData["Title"] = "Dashboard"; +} + +
+
+
+

Welcome to OneForMe

+

Logged in as: @User.Identity?.Name

+ Logout +
+
+ +
+
+
+
+
Create New Order
+

Start a new group order and invite friends

+ Create Order +
+
+
+ +
+
+
+
Join Order
+

Join an existing order using the order code

+
+
+ + +
+
+
+
+
+
+ +
+ + +
+
+

My Created Orders

+ @if (Model.CreatedOrders.Any()) + { +
+ @foreach (var order in Model.CreatedOrders) + { +
+
+
+
@order.CreatorName
+

+ Code: @order.OrderCode
+ Created: @order.CreatedAt.ToString("MMM dd, yyyy HH:mm")
+ Items: @order.MenuItems.Count | Orders: @order.OrderItems.Count +

+

Total: $@order.OrderItems.Sum(oi => oi.MenuItem.Price * oi.Quantity).ToString("F2")

+
+ View + @if (!order.IsClosed) + { + Close Order + } + else + { + Closed + } +
+
+
+
+ } +
+ } + else + { +
You haven't created any orders yet.
+ } +
+
+ +
+ + +
+
+

Orders I Joined

+ @if (Model.JoinedOrders.Any()) + { +
+ @foreach (var order in Model.JoinedOrders) + { + var myItems = order.OrderItems.Where(oi => oi.ParticipantEmail == User.Identity?.Name || oi.ParticipantName == User.Identity?.Name).ToList(); +
+
+
+
@order.CreatorName
+

+ Code: @order.OrderCode
+ Created by: @order.CreatorName
+ My items: @myItems.Count +

+

I owe: $@myItems.Sum(oi => oi.MenuItem.Price * oi.Quantity).ToString("F2")

+
+ View + @if (!order.IsClosed) + { + Add More + } +
+
+
+
+ } +
+ } + else + { +
You haven't joined any orders yet.
+ } +
+
+
\ No newline at end of file diff --git a/Views/Order/Create.cshtml b/Views/Order/Create.cshtml new file mode 100644 index 0000000..30115fa --- /dev/null +++ b/Views/Order/Create.cshtml @@ -0,0 +1,84 @@ +@{ + ViewData["Title"] = "Create Order"; +} + +
+
+
+
+
+

Create New Order

+ +
+
+ + + Give your order a name +
+ +
+ +
Menu Items
+

Add items that people can order

+ +
+
+
+
+ +
+
+
+ $ + +
+
+
+
+
+ + + +
+ +
+ + Cancel +
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/Views/Order/Details.cshtml b/Views/Order/Details.cshtml new file mode 100644 index 0000000..ef9da39 --- /dev/null +++ b/Views/Order/Details.cshtml @@ -0,0 +1,124 @@ +@model OneForMe.Models.Order + +@{ + ViewData["Title"] = "Order Details"; +} + +
+
+
+
+
+

@Model.CreatorName

+

Order Code: @Model.OrderCode

+

Created by: @Model.CreatorName

+

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

+
+
+ +
+
+
Menu Items
+
+
+ @if (Model.MenuItems.Any()) + { + + + + + + + + + @foreach (var item in Model.MenuItems) + { + + + + + } + +
ItemPrice
@item.Name$@item.Price.ToString("F2")
+ } + else + { +

No items added yet

+ } +
+
+ +
+
+
Orders (@Model.OrderItems.Count)
+
+
+ @if (Model.OrderItems.Any()) + { + + + + + + + + + + + @foreach (var orderItem in Model.OrderItems) + { + + + + + + + } + +
ParticipantItemQtyTotal
@orderItem.ParticipantName@orderItem.MenuItem?.Name@orderItem.Quantity$@(orderItem.MenuItem?.Price * orderItem.Quantity ?? 0).ToString("F2")
+ } + else + { +

No orders yet

+ } +
+
+
+ +
+
+
+
Share Order
+

Send this link to others:

+
+ + +
+
+
+ +
+
+
Quick Stats
+

Total Items: @Model.MenuItems.Count

+

Total Orders: @Model.OrderItems.Count

+

Total Revenue: $@Model.OrderItems.Sum(oi => oi.MenuItem.Price * oi.Quantity).ToString("F2")

+
+
+ + @if (!Model.IsClosed) + { + Close Order + } +
+
+
+ + \ No newline at end of file diff --git a/Views/Order/Join.cshtml b/Views/Order/Join.cshtml new file mode 100644 index 0000000..975c57f --- /dev/null +++ b/Views/Order/Join.cshtml @@ -0,0 +1,151 @@ +@model OneForMe.Models.Order + +@{ + ViewData["Title"] = "Join Order"; +} + +
+
+
+
+
+

@Model.CreatorName

+

Order Code: @Model.OrderCode

+

Created by: @Model.CreatorName

+
+
+ +
+
+
Available Items
+
+
+ @if (Model.MenuItems.Any()) + { +
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ Total: $0.00 +
+ + + Back to Dashboard +
+ } + else + { +

No items available for this order

+ } +
+
+
+ +
+
+
+
Current Orders (@Model.OrderItems.Count)
+
+
+ @if (Model.OrderItems.Any()) + { + + + + + + + + + + + @foreach (var orderItem in Model.OrderItems) + { + + + + + + + } + +
NameItemQtyTotal
@orderItem.ParticipantName@orderItem.MenuItem?.Name@orderItem.Quantity$@(orderItem.MenuItem?.Price * orderItem.Quantity ?? 0).ToString("F2")
+ +
+ +
+ + + + + + + + + @foreach (var person in Model.OrderItems.GroupBy(oi => oi.ParticipantName)) + { + + + + + } + +
PersonOwes
@person.Key$@person.Sum(oi => oi.MenuItem.Price * oi.Quantity).ToString("F2")
+
+ } + else + { +

No one has ordered yet

+ } +
+
+ +
+
+
Order Total
+

$@Model.OrderItems.Sum(oi => oi.MenuItem.Price * oi.Quantity).ToString("F2")

+
+
+
+
+
+ + \ No newline at end of file