Skip to content

Commit

Permalink
Merge pull request #1 from aforesti/feature/cashierUI
Browse files Browse the repository at this point in the history
Cashier UI remodel
  • Loading branch information
fredimachado authored May 25, 2024
2 parents 2f5bbdb + 3138c4e commit b30cc27
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 123 deletions.
1 change: 1 addition & 0 deletions src/UI/NCafe.Web/App.razor
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
</LayoutView>
</NotFound>
</Router>
<script src="https://cdn.tailwindcss.com"></script>

<AntContainer />
16 changes: 16 additions & 0 deletions src/UI/NCafe.Web/Models/Order.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
namespace NCafe.Web.Models;

public class Order
{
public Guid Id { get; set; }
public string CustomerName { get; set; }
public List<OrderItem> Items { get; set; } = new();
public decimal Total => Items.Sum(x => x.Total);
public void Clear()
{
Items.Clear();
Id = Guid.Empty;
CustomerName = string.Empty;
}
}

public class OrderItem
{
public Guid ProductId { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
public decimal Total => Quantity * Price;
}
224 changes: 101 additions & 123 deletions src/UI/NCafe.Web/Pages/Cashier/Index.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@page "/cashier"
@page "/cashier"
@using NCafe.Web.Models
@inject HttpClient Http
@inject IConfiguration Configuration
Expand All @@ -7,181 +7,172 @@

<PageTitle>Cashier - NCafe</PageTitle>

<h2>New Order</h2>

@if (products == null)
@if (_products == null)
{
<Spin />
return;
}
else if (!products.Any())

@if (!_products.Any())
{
<Result Title="You need products in order to place an order.">
<Extra>
<Button Type="@ButtonType.Primary" OnClick="GoAdmin">Go Admin</Button>
</Extra>
</Result>
return;
}
else
{
if (cashierState == CashierState.Idle)
{
<Button Type="@ButtonType.Primary" OnClick="CreateOrder">Create Order</Button>
}
else if (cashierState == CashierState.OrderCreated)
{
<Form Loading="isLoading"
Model="@orderItem"
OnFinish="AddOrderItem"
LabelColSpan="8"
WrapperColSpan="16">
<FormItem Label="Product">
<AutoComplete @ref="productAutoComplete"
TOption="Product"
@bind-Value="@value"
OnSelectionChange="OnSelectionChange"
Options="products"
Placeholder="Product..." />
</FormItem>
<FormItem Label="Quantity">
<AntDesign.InputNumber @bind-Value="@context.Quantity" />
</FormItem>
<FormItem WrapperColOffset="8" WrapperColSpan="16">
<Button Type="@ButtonType.Primary" HtmlType="submit">
Add Item
</Button>
</FormItem>
</Form>

<Card>
<Input Placeholder="Customer Name" @bind-Value="@customer" />
<Button Type="@ButtonType.Primary" HtmlType="submit" OnClick="PlaceOrder">
Place Order
</Button>
</Card>
}
}
<div class="grid grid-cols-[1fr,2fr] h-full">
<div class="grid grid-rows-[3rem,auto,3rem,5rem]">
<h2 class="font-bold text-xl">Order Summary</h2>
<ul>
@foreach (var item in _order.Items)
{
<li class="grid grid-cols-2 p-2 border-b-dashed border-b border-dashed last:border-b-0 text-lg">
<span>@item.Quantity x @(_products.FirstOrDefault(p => p.Id == item.ProductId)?.Name)</span>
<span class="text-right">@(item.Total.ToString("C"))</span>
</li>
}
</ul>
@if (_order.Items.Any()) {
<div class="grid grid-cols-2 p-2 text-right font-bold text-xl">
<span>Total</span>
<span>@(_order.Total.ToString("C"))</span>
</div>

<button class="m-2 shadow border-black border-1 rounded bg-sky-800 text-white text-xl font-bold active:bg-sky-600"
type="@ButtonType.Primary" @onclick="ShowModal">Place Order</button>
}
</div>
<div class="grid grid-cols-4 grid-rows-4 gap-2 p-4">
@foreach (var product in _products)
{
<button type="button" @onclick="() => AddItemToOrder(product.Id)"
class="shadow hover:shadow-lg p-2 text-xl border-solid border-black border-1 active:bg-sky-600 hover:bg-sky-800 hover:text-white hover:border-slate-500 rounded">
<strong>@product.Name<br/>@product.Price.ToString("C")</strong>
</button>
}
</div>

@code {
private bool isLoading;

private CashierState cashierState = CashierState.Idle;
</div>

private Product[] products;
<NCafe.Web.Shared.Modal IsVisible="@_isModalVisible" OnClose="HideModal" OnCancel="CancelOrder" Title="Confirm order" OnConfirm="PlaceOrder">
<Form @onsubmit="PlaceOrder">
<Form.Item Label="Customer name">

Check failure on line 62 in src/UI/NCafe.Web/Pages/Cashier/Index.razor

View workflow job for this annotation

GitHub Actions / build

Found markup element with unexpected name 'Form.Item'. If this is intended to be a component, add a @using directive for its namespace.

Check failure on line 62 in src/UI/NCafe.Web/Pages/Cashier/Index.razor

View workflow job for this annotation

GitHub Actions / build

Found markup element with unexpected name 'Form.Item'. If this is intended to be a component, add a @using directive for its namespace.
<Input required @bind-Value="_order.CustomerName" />
</Form.Item>
</Form>
<label for="customer-name" class="block text">Customer name</label>
<input autofocus type="text" @bind="_order.CustomerName" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"/>

private AutoComplete<Product> productAutoComplete;
private string value;
<div class="grid grid-cols-2 p-2 text-right font-bold text-xl">
<span>Total</span>
<span>@(_order.Total.ToString("C"))</span>
</div>
</NCafe.Web.Shared.Modal>

private Guid orderId;
private string customer = string.Empty;
private OrderItem orderItem = new OrderItem { Quantity = 1 };
@code {

private string baseAddress;
private Product[] _products;
private readonly Order _order = new();
private string _baseAddress;
private bool _isModalVisible;

protected override async Task OnInitializedAsync()
{
baseAddress = Configuration["CashierBaseAddress"];
_baseAddress = Configuration["CashierBaseAddress"];

products = await Http.GetFromJsonAsync<Product[]>($"{baseAddress}/products");
_products = await Http.GetFromJsonAsync<Product[]>($"{_baseAddress}/products");
}

void OnSelectionChange(AutoCompleteOption item)
void ShowModal() => _isModalVisible = true;
void HideModal() => _isModalVisible = false;

void CancelOrder()
{
if (item.Value is Product product)
{
orderItem.ProductId = product.Id;
}
_order.Clear();
_isModalVisible = false;
}

async Task CreateOrder()
async Task AddItemToOrder(Guid productId)
{
isLoading = true;
if (_order.Id == Guid.Empty)
{
await CreateOrder();
}

var product = _products.FirstOrDefault(p => p.Id == productId);
var item = _order.Items.FirstOrDefault(i => i.ProductId == productId);
if (item == null)
{
item = new OrderItem { ProductId = productId, Price = product?.Price ?? 0, Quantity = 1 };
_order.Items.Add(item);
} else
{
item.Quantity++;
}

try
{
var response = await Http.PostAsJsonAsync($"{baseAddress}/orders", new { CreatedBy = "cashier-1" });
var response = await Http.PostAsJsonAsync(
$"{_baseAddress}/orders/add-item",
new { OrderId = _order.Id, item.ProductId, item.Quantity });
response.EnsureSuccessStatusCode();

orderId = await response.Content.ReadFromJsonAsync<Guid>();

cashierState = CashierState.OrderCreated;
}
catch (Exception ex)
{
_ = NotificationService.Open(new NotificationConfig
{
Message = "Error",
Duration = 0,
Description = $"There was an error while attempting to save the product: {ex.Message}.",
NotificationType = NotificationType.Error
});
}
finally
{
isLoading = false;
{
Message = "Error",
Duration = 0,
Description = $"There was an error while attempting to add item: {ex.Message}.",
NotificationType = NotificationType.Error
});
}
}

async Task AddOrderItem()
async Task CreateOrder()
{
isLoading = true;

try
{
var response = await Http.PostAsJsonAsync($"{baseAddress}/orders/add-item", new { OrderId = orderId, orderItem.ProductId, orderItem.Quantity });
var response = await Http.PostAsJsonAsync(
$"{_baseAddress}/orders", new { CreatedBy = "cashier-1" });
response.EnsureSuccessStatusCode();

_ = NotificationService.Open(new NotificationConfig
{
Message = "Item Added",
Description = $"{orderItem.Quantity}x {productAutoComplete.SelectedValue} was added.",
NotificationType = NotificationType.Success
});

orderItem.Quantity = 1;

// The next 2 lines will reset the product selection
value = "";
productAutoComplete.SelectedValue = null;
_order.Id = await response.Content.ReadFromJsonAsync<Guid>();
}
catch (Exception ex)
{
_ = NotificationService.Open(new NotificationConfig
{
Message = "Error",
Duration = 0,
Description = $"There was an error while attempting to add item: {ex.Message}.",
Description = $"There was an error while attempting to create the order: {ex.Message}.",
NotificationType = NotificationType.Error
});
}
finally
{
isLoading = false;
}
}

async Task PlaceOrder()
{
isLoading = true;

try
{
var response = await Http.PostAsJsonAsync($"{baseAddress}/orders/place", new { OrderId = orderId, Customer = new { Name = customer } });
var response = await Http.PostAsJsonAsync(
$"{_baseAddress}/orders/place",
new { OrderId = _order.Id, Customer = new { Name = _order.CustomerName } });
response.EnsureSuccessStatusCode();

_ = NotificationService.Open(new NotificationConfig
{
Message = "Order Created",
Description = $"Order created successfully.",
Description = "Order created successfully.",
NotificationType = NotificationType.Success
});

orderId = Guid.Empty;
customer = string.Empty;
cashierState = CashierState.Idle;
_order.Clear();
_isModalVisible = false;

// The next 2 lines will reset the product selection
value = "";
productAutoComplete.SelectedValue = null;
}
catch (Exception ex)
{
Expand All @@ -193,20 +184,7 @@ else
NotificationType = NotificationType.Error
});
}
finally
{
isLoading = false;
}
}

void GoAdmin()
{
NavigationManager.NavigateTo("admin");
}

public enum CashierState
{
Idle,
OrderCreated
}
void GoAdmin() => NavigationManager.NavigateTo("admin");
}
42 changes: 42 additions & 0 deletions src/UI/NCafe.Web/Shared/Modal.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@code {
[Parameter]
public bool IsVisible { get; set; }

[Parameter]
public EventCallback OnClose { get; set; }

[Parameter]
public EventCallback OnConfirm { get; set; }

[Parameter]
public EventCallback OnCancel { get; set; }

[Parameter]
public string Title { get; set; }

[Parameter]
public RenderFragment ChildContent { get; set; }

private void Close() => OnClose.InvokeAsync();
private void Confirm() => OnConfirm.InvokeAsync();
private void Cancel() => OnCancel.InvokeAsync();
}

@if (IsVisible)
{
<div class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50">
<div class="bg-white rounded-lg shadow-lg w-1/3">
<div class="border-b px-4 py-2 flex justify-between items-center">
<h5 class="font-bold">@Title</h5>
<button type="button" class="text-black close" @onclick="Close">&times;</button>
</div>
<div class="p-4">
@ChildContent
</div>
<div class="border-t px-4 py-2 flex justify-end gap-2">
<button type="button" class="bg-red-300 text-white px-4 py-2 rounded" @onclick="Cancel">Cancel</button>
<button type="button" class="bg-gray-500 text-white px-4 py-2 rounded" @onclick="Confirm">Confirm</button>
</div>
</div>
</div>
}

0 comments on commit b30cc27

Please sign in to comment.