Browse Source
* working on creating and viewing orders. * Working on wiring up listing of orders * List orders page works as expected. Needed to support ThenInclude scenarios. Currently using strings.main
committed by
GitHub
39 changed files with 698 additions and 60 deletions
@ -0,0 +1,22 @@ |
|||
using Microsoft.eShopWeb.ApplicationCore.Entities; |
|||
|
|||
namespace ApplicationCore.Entities.OrderAggregate |
|||
{ |
|||
|
|||
public class OrderItem : BaseEntity |
|||
{ |
|||
public CatalogItemOrdered ItemOrdered { get; private set; } |
|||
public decimal UnitPrice { get; private set; } |
|||
public int Units { get; private set; } |
|||
|
|||
protected OrderItem() |
|||
{ |
|||
} |
|||
public OrderItem(CatalogItemOrdered itemOrdered, decimal unitPrice, int units) |
|||
{ |
|||
ItemOrdered = itemOrdered; |
|||
UnitPrice = unitPrice; |
|||
Units = units; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
using ApplicationCore.Entities.OrderAggregate; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace ApplicationCore.Interfaces |
|||
{ |
|||
|
|||
public interface IOrderRepository : IRepository<Order>, IAsyncRepository<Order> |
|||
{ |
|||
Order GetByIdWithItems(int id); |
|||
Task<Order> GetByIdWithItemsAsync(int id); |
|||
} |
|||
} |
|||
@ -0,0 +1,10 @@ |
|||
using ApplicationCore.Entities.OrderAggregate; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace ApplicationCore.Interfaces |
|||
{ |
|||
public interface IOrderService |
|||
{ |
|||
Task CreateOrderAsync(int basketId, Address shippingAddress); |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
using ApplicationCore.Interfaces; |
|||
using System; |
|||
using System.Linq.Expressions; |
|||
using System.Collections.Generic; |
|||
using ApplicationCore.Entities.OrderAggregate; |
|||
|
|||
namespace ApplicationCore.Specifications |
|||
{ |
|||
public class CustomerOrdersWithItemsSpecification : ISpecification<Order> |
|||
{ |
|||
private readonly string _buyerId; |
|||
|
|||
public CustomerOrdersWithItemsSpecification(string buyerId) |
|||
{ |
|||
_buyerId = buyerId; |
|||
AddInclude(o => o.OrderItems); |
|||
AddInclude("OrderItems.ItemOrdered"); |
|||
} |
|||
|
|||
public Expression<Func<Order, bool>> Criteria => o => o.BuyerId == _buyerId; |
|||
|
|||
public List<Expression<Func<Order, object>>> Includes { get; } = new List<Expression<Func<Order, object>>>(); |
|||
public List<string> IncludeStrings { get; } = new List<string>(); |
|||
|
|||
public void AddInclude(Expression<Func<Order, object>> includeExpression) |
|||
{ |
|||
Includes.Add(includeExpression); |
|||
} |
|||
|
|||
public void AddInclude(string includeString) |
|||
{ |
|||
IncludeStrings.Add(includeString); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
using ApplicationCore.Entities.OrderAggregate; |
|||
using ApplicationCore.Interfaces; |
|||
using Microsoft.EntityFrameworkCore; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace Infrastructure.Data |
|||
{ |
|||
public class OrderRepository : EfRepository<Order>, IOrderRepository |
|||
{ |
|||
public OrderRepository(CatalogContext dbContext) : base(dbContext) |
|||
{ |
|||
} |
|||
|
|||
public Order GetByIdWithItems(int id) |
|||
{ |
|||
return _dbContext.Orders |
|||
.Include(o => o.OrderItems) |
|||
.Include("OrderItems.ItemOrdered") |
|||
.FirstOrDefault(); |
|||
} |
|||
|
|||
public Task<Order> GetByIdWithItemsAsync(int id) |
|||
{ |
|||
return _dbContext.Orders |
|||
.Include(o => o.OrderItems) |
|||
.Include("OrderItems.ItemOrdered") |
|||
.FirstOrDefaultAsync(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
using ApplicationCore.Interfaces; |
|||
using ApplicationCore.Entities.OrderAggregate; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.eShopWeb.ApplicationCore.Entities; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Infrastructure.Services |
|||
{ |
|||
public class OrderService : IOrderService |
|||
{ |
|||
private readonly IAsyncRepository<Order> _orderRepository; |
|||
private readonly IAsyncRepository<Basket> _basketRepository; |
|||
private readonly IAsyncRepository<CatalogItem> _itemRepository; |
|||
|
|||
public OrderService(IAsyncRepository<Basket> basketRepository, |
|||
IAsyncRepository<CatalogItem> itemRepository, |
|||
IAsyncRepository<Order> orderRepository) |
|||
{ |
|||
_orderRepository = orderRepository; |
|||
_basketRepository = basketRepository; |
|||
_itemRepository = itemRepository; |
|||
} |
|||
|
|||
public async Task CreateOrderAsync(int basketId, Address shippingAddress) |
|||
{ |
|||
var basket = await _basketRepository.GetByIdAsync(basketId); |
|||
var items = new List<OrderItem>(); |
|||
foreach (var item in basket.Items) |
|||
{ |
|||
var catalogItem = await _itemRepository.GetByIdAsync(item.CatalogItemId); |
|||
var itemOrdered = new CatalogItemOrdered(catalogItem.Id, catalogItem.Name, catalogItem.PictureUri); |
|||
var orderItem = new OrderItem(itemOrdered, item.UnitPrice, item.Quantity); |
|||
items.Add(orderItem); |
|||
} |
|||
var order = new Order(basket.BuyerId, shippingAddress, items); |
|||
|
|||
await _orderRepository.AddAsync(order); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Authorization; |
|||
using Microsoft.eShopWeb.ViewModels; |
|||
using System; |
|||
using ApplicationCore.Entities.OrderAggregate; |
|||
using ApplicationCore.Interfaces; |
|||
using System.Linq; |
|||
using ApplicationCore.Specifications; |
|||
|
|||
namespace Microsoft.eShopWeb.Controllers |
|||
{ |
|||
[Authorize] |
|||
[Route("[controller]/[action]")]
|
|||
public class OrderController : Controller |
|||
{ |
|||
private readonly IOrderRepository _orderRepository; |
|||
|
|||
public OrderController(IOrderRepository orderRepository) { |
|||
_orderRepository = orderRepository; |
|||
} |
|||
|
|||
public async Task<IActionResult> Index() |
|||
{ |
|||
var orders = await _orderRepository.ListAsync(new CustomerOrdersWithItemsSpecification(User.Identity.Name)); |
|||
|
|||
var viewModel = orders |
|||
.Select(o => new OrderViewModel() |
|||
{ |
|||
OrderDate = o.OrderDate, |
|||
OrderItems = o.OrderItems?.Select(oi => new OrderItemViewModel() |
|||
{ |
|||
Discount = 0, |
|||
PictureUrl = oi.ItemOrdered.PictureUri, |
|||
ProductId = oi.ItemOrdered.CatalogItemId, |
|||
ProductName = oi.ItemOrdered.ProductName, |
|||
UnitPrice = oi.UnitPrice, |
|||
Units = oi.Units |
|||
}).ToList(), |
|||
OrderNumber = o.Id, |
|||
ShippingAddress = o.ShipToAddress, |
|||
Status = "Pending", |
|||
Total = o.Total() |
|||
|
|||
}); |
|||
return View(viewModel); |
|||
} |
|||
|
|||
[HttpGet("{orderId}")] |
|||
public async Task<IActionResult> Detail(int orderId) |
|||
{ |
|||
var order = await _orderRepository.GetByIdWithItemsAsync(orderId); |
|||
var viewModel = new OrderViewModel() |
|||
{ |
|||
OrderDate = order.OrderDate, |
|||
OrderItems = order.OrderItems.Select(oi => new OrderItemViewModel() |
|||
{ |
|||
Discount = 0, |
|||
PictureUrl = oi.ItemOrdered.PictureUri, |
|||
ProductId = oi.ItemOrdered.CatalogItemId, |
|||
ProductName = oi.ItemOrdered.ProductName, |
|||
UnitPrice = oi.UnitPrice, |
|||
Units = oi.Units |
|||
}).ToList(), |
|||
OrderNumber = order.Id, |
|||
ShippingAddress = order.ShipToAddress, |
|||
Status = "Pending", |
|||
Total = order.Total() |
|||
}; |
|||
return View(viewModel); |
|||
} |
|||
|
|||
private OrderViewModel GetOrder() |
|||
{ |
|||
var order = new OrderViewModel() |
|||
{ |
|||
OrderDate = DateTimeOffset.Now.AddDays(-1), |
|||
OrderNumber = 12354, |
|||
Status = "Submitted", |
|||
Total = 123.45m, |
|||
ShippingAddress = new Address("123 Main St.", "Kent", "OH", "United States", "44240") |
|||
}; |
|||
|
|||
order.OrderItems.Add(new OrderItemViewModel() |
|||
{ |
|||
ProductId = 1, |
|||
PictureUrl = "", |
|||
ProductName = "Something", |
|||
UnitPrice = 5.05m, |
|||
Units = 2 |
|||
}); |
|||
|
|||
return order; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.ComponentModel.DataAnnotations; |
|||
|
|||
namespace Microsoft.eShopWeb.ViewModels |
|||
{ |
|||
|
|||
public class OrderItemViewModel |
|||
{ |
|||
public int ProductId { get; set; } |
|||
|
|||
public string ProductName { get; set; } |
|||
|
|||
public decimal UnitPrice { get; set; } |
|||
|
|||
public decimal Discount { get; set; } |
|||
|
|||
public int Units { get; set; } |
|||
|
|||
public string PictureUrl { get; set; } |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
using ApplicationCore.Entities.OrderAggregate; |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Microsoft.eShopWeb.ViewModels |
|||
{ |
|||
|
|||
public class OrderViewModel |
|||
{ |
|||
public int OrderNumber { get; set; } |
|||
public DateTimeOffset OrderDate { get; set; } |
|||
public decimal Total { get; set; } |
|||
public string Status { get; set; } |
|||
|
|||
public Address ShippingAddress { get; set; } |
|||
|
|||
public List<OrderItemViewModel> OrderItems { get; set; } = new List<OrderItemViewModel>(); |
|||
|
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
@using Microsoft.eShopWeb.ViewModels |
|||
@model OrderViewModel |
|||
@{ |
|||
ViewData["Title"] = "My Order History"; |
|||
} |
|||
@{ |
|||
ViewData["Title"] = "Order Detail"; |
|||
} |
|||
|
|||
<div class="esh-orders_detail"> |
|||
<div class="container"> |
|||
<section class="esh-orders_detail-section"> |
|||
<article class="esh-orders_detail-titles row"> |
|||
<section class="esh-orders_detail-title col-xs-3">Order number</section> |
|||
<section class="esh-orders_detail-title col-xs-3">Date</section> |
|||
<section class="esh-orders_detail-title col-xs-3">Total</section> |
|||
<section class="esh-orders_detail-title col-xs-3">Status</section> |
|||
</article> |
|||
|
|||
<article class="esh-orders_detail-items row"> |
|||
<section class="esh-orders_detail-item col-xs-3">@Model.OrderNumber</section> |
|||
<section class="esh-orders_detail-item col-xs-3">@Model.OrderDate</section> |
|||
<section class="esh-orders_detail-item col-xs-3">$@Model.Total</section> |
|||
<section class="esh-orders_detail-title col-xs-3">@Model.Status</section> |
|||
</article> |
|||
</section> |
|||
|
|||
@*<section class="esh-orders_detail-section"> |
|||
<article class="esh-orders_detail-titles row"> |
|||
<section class="esh-orders_detail-title col-xs-12">Description</section> |
|||
</article> |
|||
|
|||
<article class="esh-orders_detail-items row"> |
|||
<section class="esh-orders_detail-item col-xs-12">@Model.Description</section> |
|||
</article> |
|||
</section>*@ |
|||
|
|||
<section class="esh-orders_detail-section"> |
|||
<article class="esh-orders_detail-titles row"> |
|||
<section class="esh-orders_detail-title col-xs-12">Shipping Address</section> |
|||
</article> |
|||
|
|||
<article class="esh-orders_detail-items row"> |
|||
<section class="esh-orders_detail-item col-xs-12">@Model.ShippingAddress.Street</section> |
|||
</article> |
|||
|
|||
<article class="esh-orders_detail-items row"> |
|||
<section class="esh-orders_detail-item col-xs-12">@Model.ShippingAddress.City</section> |
|||
</article> |
|||
|
|||
<article class="esh-orders_detail-items row"> |
|||
<section class="esh-orders_detail-item col-xs-12">@Model.ShippingAddress.Country</section> |
|||
</article> |
|||
</section> |
|||
|
|||
<section class="esh-orders_detail-section"> |
|||
<article class="esh-orders_detail-titles row"> |
|||
<section class="esh-orders_detail-title col-xs-12">ORDER DETAILS</section> |
|||
</article> |
|||
|
|||
@for (int i = 0; i < Model.OrderItems.Count; i++) |
|||
{ |
|||
var item = Model.OrderItems[i]; |
|||
<article class="esh-orders_detail-items esh-orders_detail-items--border row"> |
|||
<section class="esh-orders_detail-item col-md-4 hidden-md-down"> |
|||
<img class="esh-orders_detail-image" src="@item.PictureUrl"> |
|||
</section> |
|||
<section class="esh-orders_detail-item esh-orders_detail-item--middle col-xs-4">@item.ProductName</section> |
|||
<section class="esh-orders_detail-item esh-orders_detail-item--middle col-xs-1">$ @item.UnitPrice.ToString("N2")</section> |
|||
<section class="esh-orders_detail-item esh-orders_detail-item--middle col-xs-1">@item.Units</section> |
|||
<section class="esh-orders_detail-item esh-orders_detail-item--middle col-xs-2">$ @Math.Round(item.Units * item.UnitPrice, 2).ToString("N2")</section> |
|||
</article> |
|||
} |
|||
</section> |
|||
|
|||
<section class="esh-orders_detail-section esh-orders_detail-section--right"> |
|||
<article class="esh-orders_detail-titles esh-basket-titles--clean row"> |
|||
<section class="esh-orders_detail-title col-xs-9"></section> |
|||
<section class="esh-orders_detail-title col-xs-2">TOTAL</section> |
|||
</article> |
|||
|
|||
<article class="esh-orders_detail-items row"> |
|||
<section class="esh-orders_detail-item col-xs-9"></section> |
|||
<section class="esh-orders_detail-item esh-orders_detail-item--mark col-xs-2">$ @Model.Total</section> |
|||
</article> |
|||
</section> |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,39 @@ |
|||
@using Microsoft.eShopWeb.ViewModels |
|||
@model IEnumerable<OrderViewModel> |
|||
@{ |
|||
ViewData["Title"] = "My Order History"; |
|||
} |
|||
|
|||
<div class="esh-orders"> |
|||
<div class="container"> |
|||
<h1>@ViewData["Title"]</h1> |
|||
<article class="esh-orders-titles row"> |
|||
<section class="esh-orders-title col-xs-2">Order number</section> |
|||
<section class="esh-orders-title col-xs-4">Date</section> |
|||
<section class="esh-orders-title col-xs-2">Total</section> |
|||
<section class="esh-orders-title col-xs-2">Status</section> |
|||
<section class="esh-orders-title col-xs-2"></section> |
|||
</article> |
|||
@if (Model != null && Model.Any()) |
|||
{ |
|||
@foreach (var item in Model) |
|||
{ |
|||
<article class="esh-orders-items row"> |
|||
<section class="esh-orders-item col-xs-2">@Html.DisplayFor(modelItem => item.OrderNumber)</section> |
|||
<section class="esh-orders-item col-xs-4">@Html.DisplayFor(modelItem => item.OrderDate)</section> |
|||
<section class="esh-orders-item col-xs-2">$ @Html.DisplayFor(modelItem => item.Total)</section> |
|||
<section class="esh-orders-item col-xs-2">@Html.DisplayFor(modelItem => item.Status)</section> |
|||
<section class="esh-orders-item col-xs-1"> |
|||
<a class="esh-orders-link" asp-controller="Order" asp-action="Detail" asp-route-orderId="@item.OrderNumber">Detail</a> |
|||
</section> |
|||
<section class="esh-orders-item col-xs-1"> |
|||
@if (item.Status.ToLower() == "submitted") |
|||
{ |
|||
<a class="esh-orders-link" asp-controller="Order" asp-action="cancel" asp-route-orderId="@item.OrderNumber">Cancel</a> |
|||
} |
|||
</section> |
|||
</article> |
|||
} |
|||
} |
|||
</div> |
|||
</div> |
|||
@ -0,0 +1,50 @@ |
|||
.esh-orders { |
|||
min-height: 80vh; |
|||
overflow-x: hidden; } |
|||
.esh-orders-header { |
|||
background-color: #00A69C; |
|||
height: 4rem; } |
|||
.esh-orders-back { |
|||
color: rgba(255, 255, 255, 0.4); |
|||
line-height: 4rem; |
|||
text-decoration: none; |
|||
text-transform: uppercase; |
|||
transition: color 0.35s; } |
|||
.esh-orders-back:hover { |
|||
color: #FFFFFF; |
|||
transition: color 0.35s; } |
|||
.esh-orders-titles { |
|||
padding-bottom: 1rem; |
|||
padding-top: 2rem; } |
|||
.esh-orders-title { |
|||
text-transform: uppercase; } |
|||
.esh-orders-items { |
|||
height: 2rem; |
|||
line-height: 2rem; |
|||
position: relative; } |
|||
.esh-orders-items:nth-of-type(2n + 1):before { |
|||
background-color: #EEEEFF; |
|||
content: ''; |
|||
height: 100%; |
|||
left: 0; |
|||
margin-left: -100vw; |
|||
position: absolute; |
|||
top: 0; |
|||
width: 200vw; |
|||
z-index: -1; } |
|||
.esh-orders-item { |
|||
font-weight: 300; } |
|||
.esh-orders-item--hover { |
|||
opacity: 0; |
|||
pointer-events: none; } |
|||
.esh-orders-items:hover .esh-orders-item--hover { |
|||
opacity: 1; |
|||
pointer-events: all; } |
|||
.esh-orders-link { |
|||
color: #83D01B; |
|||
text-decoration: none; |
|||
transition: color 0.35s; } |
|||
.esh-orders-link:hover { |
|||
color: #75b918; |
|||
transition: color 0.35s; } |
|||
|
|||
@ -0,0 +1 @@ |
|||
.esh-orders{min-height:80vh;overflow-x:hidden}.esh-orders-header{background-color:#00a69c;height:4rem}.esh-orders-back{color:rgba(255,255,255,.4);line-height:4rem;text-decoration:none;text-transform:uppercase;transition:color .35s}.esh-orders-back:hover{color:#fff;transition:color .35s}.esh-orders-titles{padding-bottom:1rem;padding-top:2rem}.esh-orders-title{text-transform:uppercase}.esh-orders-items{height:2rem;line-height:2rem;position:relative}.esh-orders-items:nth-of-type(2n+1):before{background-color:#eef;content:'';height:100%;left:0;margin-left:-100vw;position:absolute;top:0;width:200vw;z-index:-1}.esh-orders-item{font-weight:300}.esh-orders-item--hover{opacity:0;pointer-events:none}.esh-orders-items:hover .esh-orders-item--hover{opacity:1;pointer-events:all}.esh-orders-link{color:#83d01b;text-decoration:none;transition:color .35s}.esh-orders-link:hover{color:#75b918;transition:color .35s} |
|||
@ -0,0 +1,91 @@ |
|||
@import '../variables'; |
|||
|
|||
.esh-orders { |
|||
min-height: 80vh; |
|||
overflow-x: hidden; |
|||
$header-height: 4rem; |
|||
&-header |
|||
|
|||
{ |
|||
background-color: #00A69C; |
|||
height: $header-height; |
|||
} |
|||
|
|||
&-back { |
|||
color: rgba($color-foreground-brighter, .4); |
|||
line-height: $header-height; |
|||
text-decoration: none; |
|||
text-transform: uppercase; |
|||
transition: color $animation-speed-default; |
|||
&:hover |
|||
|
|||
{ |
|||
color: $color-foreground-brighter; |
|||
transition: color $animation-speed-default; |
|||
} |
|||
|
|||
} |
|||
|
|||
&-titles { |
|||
padding-bottom: 1rem; |
|||
padding-top: 2rem; |
|||
} |
|||
|
|||
&-title { |
|||
text-transform: uppercase; |
|||
} |
|||
|
|||
&-items { |
|||
$height: 2rem; |
|||
height: $height; |
|||
line-height: $height; |
|||
position: relative; |
|||
&:nth-of-type(2n + 1) |
|||
|
|||
{ |
|||
&:before |
|||
|
|||
{ |
|||
background-color: $color-background-bright; |
|||
content: ''; |
|||
height: 100%; |
|||
left: 0; |
|||
margin-left: -100vw; |
|||
position: absolute; |
|||
top: 0; |
|||
width: 200vw; |
|||
z-index: -1; |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
&-item { |
|||
font-weight: $font-weight-semilight; |
|||
&--hover |
|||
|
|||
{ |
|||
opacity: 0; |
|||
pointer-events: none; |
|||
} |
|||
|
|||
} |
|||
|
|||
&-items:hover &-item--hover { |
|||
opacity: 1; |
|||
pointer-events: all; |
|||
} |
|||
|
|||
&-link { |
|||
color: $color-secondary; |
|||
text-decoration: none; |
|||
transition: color $animation-speed-default; |
|||
&:hover |
|||
|
|||
{ |
|||
color: $color-secondary-dark; |
|||
transition: color $animation-speed-default; |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
Before Width: | Height: | Size: 5.1 KiB |
Loading…
Reference in new issue