Refactor New Transaction pages/modals to Razor components

This commit is contained in:
2023-12-03 19:41:06 +00:00
parent 74cb6109c9
commit 70a34b67d6
30 changed files with 576 additions and 550 deletions

View File

@ -1,32 +0,0 @@
@using Groceries.Data
@using Microsoft.EntityFrameworkCore
@model (Transaction Transaction, TransactionItem TransactionItem)
@inject AppDbContext dbContext
@{
ViewBag.Title = "Edit Transaction Item";
var store = await dbContext.Stores
.Where(store => store.Id == Model.Transaction.StoreId)
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
.SingleAsync();
}
<h1>Edit Transaction Item</h1>
<div class="form-field">
@Model.Transaction.CreatedAt.ToShortDateString() @Model.Transaction.CreatedAt.ToShortTimeString() &ndash; @store
</div>
<form id="editTransactionItem" method="post" asp-action="EditTransactionItem">
<partial name="_TransactionItemForm" model="Model.TransactionItem" />
</form>
<form id="deleteTransactionItem" method="post" asp-action="DeleteTransactionItem" asp-route-id="@Model.TransactionItem.ItemId"></form>
<div class="row">
<button class="button button--primary" type="submit" form="editTransactionItem">Update</button>
<a class="button" asp-action="NewTransactionItems">Cancel</a>
<span class="row__fill"></span>
<button class="button button--danger" type="submit" form="deleteTransactionItem">Remove</button>
</div>

View File

@ -0,0 +1,24 @@
@using Groceries.Data
@layout Modal
<SectionContent SectionName="modalTitle">Edit Transaction Item</SectionContent>
<TransactionItemForm TransactionItem="TransactionItem" class="card__content" id="editTransactionItem" data-action="turbo:submit-end->modal#close" />
<form method="post" action="/transactions/new/items/delete/@TransactionItem.ItemId" id="deleteTransactionItem" data-action="turbo:submit-end->modal#close"></form>
<footer class="card__footer card__footer--shaded row">
<button class="button button--primary" type="submit" form="editTransactionItem">Update</button>
<button class="button" data-action="modal#close">Cancel</button>
<span class="row__fill"></span>
<button class="button button--danger" type="submit" form="deleteTransactionItem">Remove</button>
</footer>
@code {
[Parameter]
public required Transaction Transaction { get; set; }
[Parameter]
public required TransactionItem TransactionItem { get; set; }
}

View File

@ -0,0 +1,43 @@
@using Groceries.Data
@using Microsoft.EntityFrameworkCore
@layout Layout
@inject AppDbContext DbContext
<PageTitle>Groceries &ndash; Edit Transaction Item</PageTitle>
<h1>Edit Transaction Item</h1>
<div class="form-field">
@Transaction.CreatedAt.ToShortDateString() @Transaction.CreatedAt.ToLongTimeString() &ndash; @store
</div>
<TransactionItemForm TransactionItem="TransactionItem" id="editTransactionItem" />
<form method="post" action="/transactions/new/items/delete/@TransactionItem.ItemId" id="deleteTransactionItem" data-action="turbo:submit-end->modal#close"></form>
<div class="row">
<button class="button button--primary" type="submit" form="editTransactionItem">Update</button>
<a class="button" href="/transaction/new/items">Cancel</a>
<span class="row__fill"></span>
<button class="button button--danger" type="submit" form="deleteTransactionItem">Remove</button>
</div>
@code {
[Parameter]
public required Transaction Transaction { get; set; }
[Parameter]
public required TransactionItem TransactionItem { get; set; }
private string store = string.Empty;
protected override async Task OnParametersSetAsync()
{
store = await DbContext.Stores
.Where(store => store.Id == Transaction.StoreId)
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
.SingleAsync();
}
}

View File

@ -1,20 +0,0 @@
@using Groceries.Data
@model (Transaction Transaction, TransactionItem TransactionItem)
@{
Layout = "_Modal";
ViewBag.Title = "Edit Transaction Item";
}
<form class="card__content" id="editTransactionItem" method="post" asp-action="EditTransactionItem" data-action="turbo:submit-end->modal#close">
<partial name="_TransactionItemForm" model="Model.TransactionItem" />
</form>
<form id="deleteTransactionItem" method="post" asp-action="DeleteTransactionItem" asp-route-id="@Model.TransactionItem.ItemId" data-action="turbo:submit-end->modal#close"></form>
<footer class="card__footer card__footer--shaded row">
<button class="button button--primary" type="submit" form="editTransactionItem">Update</button>
<button class="button" data-action="modal#close">Cancel</button>
<span class="row__fill"></span>
<button class="button button--danger" type="submit" form="deleteTransactionItem">Remove</button>
</footer>

View File

@ -1,32 +0,0 @@
@using Groceries.Data;
@using Microsoft.EntityFrameworkCore;
@model (Transaction Transaction, TransactionPromotion Promotion)
@inject AppDbContext dbContext
@{
ViewBag.Title = "Edit Transaction Promotion";
var store = await dbContext.Stores
.Where(store => store.Id == Model.Transaction.StoreId)
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
.SingleAsync();
}
<h1>Edit Transaction Promotion</h1>
<div class="form-field">
@Model.Transaction.CreatedAt.ToShortDateString() @Model.Transaction.CreatedAt.ToShortTimeString() &ndash; @store
</div>
<form id="editTransactionPromotion" method="post" asp-action="EditTransactionPromotion">
<partial name="_TransactionPromotionForm" model="Model" />
</form>
<form id="deleteTransactionPromotion" method="post" asp-action="DeleteTransactionPromotion" asp-route-id="@Model.Promotion.Id"></form>
<div class="row">
<button class="button button--primary" type="submit" form="editTransactionPromotion">Update</button>
<a class="button" asp-action="NewTransactionPromotions">Cancel</a>
<span class="row__fill"></span>
<button class="button button--danger" type="submit" form="deleteTransactionPromotion">Remove</button>
</div>

View File

@ -0,0 +1,24 @@
@using Groceries.Data
@layout Modal
<SectionContent SectionName="modalTitle">Edit Transaction Promotion</SectionContent>
<TransactionPromotionForm Transaction="Transaction" Promotion="Promotion" class="card__content" id="editTransactionPromotion" data-action="turbo:submit-end->modal#close" />
<form method="post" action="/transactions/new/promotions/delete/@Promotion.Id" id="deleteTransactionPromotion" data-action="turbo:submit-end->modal#close"></form>
<footer class="card__footer card__footer--shaded row">
<button class="button button--primary" type="submit" form="editTransactionPromotion">Update</button>
<button class="button" data-action="modal#close">Cancel</button>
<span class="row__fill"></span>
<button class="button button--danger" type="submit" form="deleteTransactionPromotion">Remove</button>
</footer>
@code {
[Parameter]
public required Transaction Transaction { get; set; }
[Parameter]
public required TransactionPromotion Promotion { get; set; }
}

View File

@ -0,0 +1,43 @@
@using Groceries.Data
@using Microsoft.EntityFrameworkCore
@layout Layout
@inject AppDbContext DbContext
<PageTitle>Groceries &ndash; Edit Transaction Promotion</PageTitle>
<h1>Edit Transaction Promotion</h1>
<div class="form-field">
@Transaction.CreatedAt.ToShortDateString() @Transaction.CreatedAt.ToLongTimeString() &ndash; @store
</div>
<TransactionPromotionForm Transaction="Transaction" Promotion="Promotion" id="editTransactionPromotion" />
<form method="post" action="/transactions/new/promotions/delete/@Promotion.Id" id="deleteTransactionPromotion"></form>
<div class="row">
<button class="button button--primary" type="submit" form="editTransactionPromotion">Update</button>
<a class="button" href="/transactions/new/promotions">Cancel</a>
<span class="row__fill"></span>
<button class="button button--danger" type="submit" form="deleteTransactionPromotion">Remove</button>
</div>
@code {
[Parameter]
public required Transaction Transaction { get; set; }
[Parameter]
public required TransactionPromotion Promotion { get; set; }
private string store = string.Empty;
protected override async Task OnParametersSetAsync()
{
store = await DbContext.Stores
.Where(store => store.Id == Transaction.StoreId)
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
.SingleAsync();
}
}

View File

@ -1,20 +0,0 @@
@using Groceries.Data
@model (Transaction Transaction, TransactionPromotion Promotion)
@{
Layout = "_Modal";
ViewBag.Title = "Edit Transaction Promotion";
}
<form class="card__content" id="editTransactionPromotion" method="post" asp-action="EditTransactionPromotion" data-action="turbo:submit-end->modal#close">
<partial name="_TransactionPromotionForm" model="Model" />
</form>
<form id="deleteTransactionPromotion" method="post" asp-action="DeleteTransactionPromotion" asp-route-id="@Model.Promotion.Id" data-action="turbo:submit-end->modal#close"></form>
<footer class="card__footer card__footer--shaded row">
<button class="button button--primary" type="submit" form="editTransactionPromotion">Update</button>
<button class="button" data-action="modal#close">Cancel</button>
<span class="row__fill"></span>
<button class="button button--danger" type="submit" form="deleteTransactionPromotion">Remove</button>
</footer>

View File

@ -1,28 +0,0 @@
@using Groceries.Data
@using Microsoft.EntityFrameworkCore
@model (Transaction Transaction, TransactionItem? TransactionItem)
@inject AppDbContext dbContext
@{
ViewBag.Title = "New Transaction Item";
var store = await dbContext.Stores
.Where(store => store.Id == Model.Transaction.StoreId)
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
.SingleAsync();
}
<h1>New Transaction Item</h1>
<div class="form-field">
@Model.Transaction.CreatedAt.ToShortDateString() @Model.Transaction.CreatedAt.ToShortTimeString() &ndash; @store
</div>
<form method="post" asp-action="NewTransactionItem">
<partial name="_TransactionItemForm" model="Model.TransactionItem" />
<div class="row">
<button class="button button--primary" type="submit">Add</button>
<a class="button" asp-action="NewTransactionItems">Cancel</a>
</div>
</form>

View File

@ -0,0 +1,20 @@
@using Groceries.Data
@layout Modal
<SectionContent SectionName="modalTitle">New Transaction Item</SectionContent>
<TransactionItemForm TransactionItem="TransactionItem" class="card__content" id="newTransactionItem" data-action="turbo:submit-end->modal#close" />
<footer class="card__footer card__footer--shaded row">
<button class="button button--primary" type="submit" form="newTransactionItem">Add</button>
<button class="button" data-action="modal#close">Cancel</button>
</footer>
@code {
[Parameter]
public required Transaction Transaction { get; set; }
[Parameter]
public TransactionItem? TransactionItem { get; set; }
}

View File

@ -0,0 +1,39 @@
@using Groceries.Data
@using Microsoft.EntityFrameworkCore
@layout Layout
@inject AppDbContext DbContext
<PageTitle>Groceries &ndash; New Transaction Item</PageTitle>
<h1>New Transaction Item</h1>
<div class="form-field">
@Transaction.CreatedAt.ToShortDateString() @Transaction.CreatedAt.ToLongTimeString() &ndash; @store
</div>
<TransactionItemForm TransactionItem="TransactionItem">
<div class="row">
<button class="button button--primary" type="submit">Add</button>
<a class="button" href="/transaction/new/items">Cancel</a>
</div>
</TransactionItemForm>
@code {
[Parameter]
public required Transaction Transaction { get; set; }
[Parameter]
public TransactionItem? TransactionItem { get; set; }
private string store = string.Empty;
protected override async Task OnParametersSetAsync()
{
store = await DbContext.Stores
.Where(store => store.Id == Transaction.StoreId)
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
.SingleAsync();
}
}

View File

@ -1,16 +0,0 @@
@using Groceries.Data
@model (Transaction Transaction, TransactionItem? TransactionItem)
@{
Layout = "_Modal";
ViewBag.Title = "New Transaction Item";
}
<form class="card__content" id="newTransactionItem" method="post" asp-action="NewTransactionItem" data-action="turbo:submit-end->modal#close">
<partial name="_TransactionItemForm" model="Model.TransactionItem" />
</form>
<footer class="card__footer card__footer--shaded row">
<button class="button button--primary" type="submit" form="newTransactionItem">Add</button>
<button class="button" data-action="modal#close">Cancel</button>
</footer>

View File

@ -1,31 +1,23 @@
@using Groceries.Data;
@using Microsoft.EntityFrameworkCore;
@using Groceries.Data
@using Microsoft.EntityFrameworkCore
@model Transaction
@inject AppDbContext dbContext
@{
ViewBag.Title = "New Transaction";
@layout Layout
var store = await dbContext.Stores
.Where(store => store.Id == Model.StoreId)
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
.SingleAsync();
@inject AppDbContext DbContext
var itemIds = Model.Items.Select(item => item.ItemId);
var itemNames = await dbContext.Items
.Where(item => itemIds.Contains(item.Id))
.ToDictionaryAsync(item => item.Id, item => string.Concat(item.Brand, " ", item.Name));
}
<PageTitle>Groceries &ndash; New Transaction</PageTitle>
<h1>New Transaction</h1>
<div class="form-field">@Model.CreatedAt.ToShortDateString() @Model.CreatedAt.ToLongTimeString() &ndash; @store</div>
<div class="form-field">
@Transaction.CreatedAt.ToShortDateString() @Transaction.CreatedAt.ToLongTimeString() &ndash; @store
</div>
<form method="post" asp-action="NewTransactionItems">
<form method="post">
<div class="card form-field">
<div class="card__header row">
<h2 class="row__fill">Items</h2>
<a class="button button--primary" asp-action="NewTransactionItem" autofocus data-turbo-frame="modal">New item</a>
<a class="button button--primary" href="/transactions/new/items/new" autofocus data-turbo-frame="modal">New item</a>
</div>
<div class="card__content card__content--table">
@ -40,11 +32,11 @@
</tr>
</thead>
<tbody>
@foreach (var item in Model.Items)
@foreach (var item in Transaction.Items)
{
<tr>
<td class="table__cell table__cell--compact">
@itemNames[item.ItemId]
@itemNames.GetValueOrDefault(item.ItemId)
</td>
<td class="table__cell table__cell--compact table__cell--numeric">
@item.Price.ToString("c")
@ -56,7 +48,7 @@
@item.Amount.ToString("c")
</td>
<td class="table__cell table__cell--compact">
<a class="link" asp-action="EditTransactionItem" asp-route-id="@item.ItemId" data-turbo-frame="modal">Edit</a>
<a class="link" href="/transactions/new/items/edit/@item.ItemId" data-turbo-frame="modal">Edit</a>
</td>
</tr>
}
@ -65,7 +57,7 @@
<tr>
<td class="table__cell table__cell--compact table__cell--total" colspan="3">Subtotal</td>
<td class="table__cell table__cell--compact table__cell--numeric table__cell--total">
@Model.Items.Sum(item => item.Amount).ToString("c")
@Transaction.Items.Sum(item => item.Amount).ToString("c")
</td>
<td class="table__cell table__cell--compact"></td>
</tr>
@ -75,7 +67,28 @@
</div>
<div class="row">
<button class="button button--primary" type="submit" @(Model.Items.Count == 0 ? "disabled" : "")>Next</button>
<a class="button" asp-action="NewTransaction">Back</a>
<button class="button button--primary" type="submit" disabled="@(Transaction.Items.Count == 0)">Next</button>
<a class="button" href="/transactions/new">Back</a>
</div>
</form>
@code {
[Parameter]
public required Transaction Transaction { get; set; }
private string store = string.Empty;
private Dictionary<Guid, string> itemNames = new();
protected override async Task OnParametersSetAsync()
{
store = await DbContext.Stores
.Where(store => store.Id == Transaction.StoreId)
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
.SingleAsync();
var itemIds = Transaction.Items.Select(item => item.ItemId);
itemNames = await DbContext.Items
.Where(item => itemIds.Contains(item.Id))
.ToDictionaryAsync(item => item.Id, item => string.Concat(item.Brand, " ", item.Name));
}
}

View File

@ -1,22 +1,15 @@
@using Groceries.Data;
@using Microsoft.EntityFrameworkCore;
@using Groceries.Data
@using Microsoft.EntityFrameworkCore
@inject AppDbContext dbContext
@{
ViewBag.Title = "New Transaction";
@layout Layout
var datetime = DateTime.Now.ToString("s");
@inject AppDbContext DbContext
var stores = await dbContext.Stores
.OrderBy(store => store.Retailer!.Name)
.ThenBy(store => store.Name)
.Select(store => new { store.Id, Name = string.Concat(store.Retailer!.Name, " ", store.Name) })
.ToListAsync();
}
<PageTitle>Groceries &ndash; New Transaction</PageTitle>
<h1>New Transaction</h1>
<form method="post" asp-action="NewTransaction">
<form method="post">
<div class="form-field">
<label class="form-field__label" for="transactionCreatedAt">Date</label>
<div class="form-field__control input">
@ -36,6 +29,22 @@
<div class="row">
<button class="button button--primary" type="submit">Next</button>
<a class="button" asp-action="Index">Cancel</a>
<a class="button" href="/transactions?page=1">Cancel</a>
</div>
</form>
@code {
private record StoreModel(Guid Id, string Name);
private string datetime = DateTime.Now.ToString("s");
private StoreModel[] stores = [];
protected override async Task OnInitializedAsync()
{
stores = await DbContext.Stores
.OrderBy(store => store.Retailer!.Name)
.ThenBy(store => store.Name)
.Select(store => new StoreModel(store.Id, string.Concat(store.Retailer!.Name, " ", store.Name)))
.ToArrayAsync();
}
}

View File

@ -1,26 +0,0 @@
@using Groceries.Data;
@using Microsoft.EntityFrameworkCore;
@model Transaction
@inject AppDbContext dbContext
@{
ViewBag.Title = "New Transaction Promotion";
var store = await dbContext.Stores
.Where(store => store.Id == Model.StoreId)
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
.SingleAsync();
}
<h1>New Transaction Promotion</h1>
<div class="form-field">@Model.CreatedAt.ToShortDateString() @Model.CreatedAt.ToShortTimeString() &ndash; @store</div>
<form method="post" asp-action="NewTransactionPromotion">
<partial name="_TransactionPromotionForm" model="(Model, (TransactionPromotion?)null)" />
<div class="row">
<button class="button button--primary" type="submit">Add</button>
<a class="button" asp-action="NewTransactionPromotions">Cancel</a>
</div>
</form>

View File

@ -0,0 +1,17 @@
@using Groceries.Data
@layout Modal
<SectionContent SectionName="modalTitle">New Transaction Promotion</SectionContent>
<TransactionPromotionForm Transaction="Transaction" class="card__content" id="newTransactionPromotion" data-action="turbo:submit-end->modal#close" />
<footer class="card__footer card__footer--shaded row">
<button class="button button--primary" type="submit" form="newTransactionPromotion">Add</button>
<button class="button" data-action="modal#close">Cancel</button>
</footer>
@code {
[Parameter]
public required Transaction Transaction { get; set; }
}

View File

@ -0,0 +1,36 @@
@using Groceries.Data
@using Microsoft.EntityFrameworkCore
@layout Layout
@inject AppDbContext DbContext
<PageTitle>Groceries &ndash; New Transaction Promotion</PageTitle>
<h1>New Transaction Promotion</h1>
<div class="form-field">
@Transaction.CreatedAt.ToShortDateString() @Transaction.CreatedAt.ToLongTimeString() &ndash; @store
</div>
<TransactionPromotionForm Transaction="Transaction">
<div class="row">
<button class="button button--primary" type="submit">Add</button>
<a class="button" href="/transactions/new/promotions">Cancel</a>
</div>
</TransactionPromotionForm>
@code {
[Parameter]
public required Transaction Transaction { get; set; }
private string store = string.Empty;
protected override async Task OnParametersSetAsync()
{
store = await DbContext.Stores
.Where(store => store.Id == Transaction.StoreId)
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
.SingleAsync();
}
}

View File

@ -1,16 +0,0 @@
@using Groceries.Data
@model Transaction
@{
Layout = "_Modal";
ViewBag.Title = "New Transaction Promotion";
}
<form class="card__content" id="newTransactionPromotion" method="post" asp-action="NewTransactionPromotion" data-action="turbo:submit-end->modal#close">
<partial name="_TransactionPromotionForm" model="(Model, (TransactionPromotion?)null)" />
</form>
<footer class="card__footer card__footer--shaded row">
<button class="button button--primary" type="submit" form="newTransactionPromotion">Add</button>
<button class="button" data-action="modal#close">Cancel</button>
</footer>

View File

@ -1,26 +1,23 @@
@using Groceries.Data;
@using Microsoft.EntityFrameworkCore;
@using Groceries.Data
@using Microsoft.EntityFrameworkCore
@model Transaction
@inject AppDbContext dbContext
@{
ViewBag.Title = "New Transaction";
@layout Layout
var store = await dbContext.Stores
.Where(store => store.Id == Model.StoreId)
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
.SingleAsync();
}
@inject AppDbContext DbContext
<PageTitle>Groceries &ndash; New Transaction</PageTitle>
<h1>New Transaction</h1>
<div class="form-field">@Model.CreatedAt.ToShortDateString() @Model.CreatedAt.ToLongTimeString() &ndash; @store</div>
<div class="form-field">
@Transaction.CreatedAt.ToShortDateString() @Transaction.CreatedAt.ToLongTimeString() &ndash; @store
</div>
<form method="post" asp-action="NewTransactionPromotions">
<form method="post">
<div class="card form-field">
<div class="card__header row">
<h2 class="row__fill">Promotions</h2>
<a class="button button--primary" asp-action="NewTransactionPromotion" autofocus data-turbo-frame="modal">
<a class="button button--primary" href="/transactions/new/promotions/new" autofocus data-turbo-frame="modal">
New promotion
</a>
</div>
@ -36,20 +33,20 @@
</tr>
</thead>
<tbody>
@foreach (var promotion in Model.Promotions)
@foreach (var promotion in Transaction.Promotions)
{
<tr>
<td class="table__cell table__cell--compact">
@promotion.Name
</td>
<td class="table__cell table__cell--compact table__cell--numeric">
@promotion.Items.Sum(item => Model.Items.Single(i => i.ItemId == item.Id).Quantity)
@promotion.Items.Sum(item => Transaction.Items.Single(i => i.ItemId == i.ItemId).Quantity)
</td>
<td class="table__cell table__cell--compact table__cell--numeric">
@((-promotion.Amount).ToString("c"))
</td>
<td class="table__cell table__cell--compact">
<a class="link" asp-action="EditTransactionPromotion" asp-route-id="@promotion.Id" data-turbo-frame="modal">Edit</a>
<a class="link" href="/transactions/new/promotions/edit/@promotion.Id" data-turbo-frame="modal">Edit</a>
</td>
</tr>
}
@ -58,7 +55,7 @@
<tr>
<td class="table__cell table__cell--compact table__cell--total" colspan="3">Total</td>
<td class="table__cell table__cell--compact table__cell--numeric table__cell--total">
@Model.Total.ToString("c")
@Transaction.Total.ToString("c")
</td>
<td class="table__cell table__cell--compact"></td>
</tr>
@ -68,7 +65,22 @@
</div>
<div class="row">
<button class="button button--primary" type="submit" @(Model.Items.Count == 0 ? "disabled" : "")>Save</button>
<a class="button" asp-action="NewTransactionItems">Back</a>
<button class="button button--primary" type="submit" disabled="@(Transaction.Items.Count == 0)">Save</button>
<a class="button" href="/transactions/new/items">Back</a>
</div>
</form>
@code {
[Parameter]
public required Transaction Transaction { get; set; }
private string store = string.Empty;
protected override async Task OnParametersSetAsync()
{
store = await DbContext.Stores
.Where(store => store.Id == Transaction.StoreId)
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
.SingleAsync();
}
}

View File

@ -0,0 +1,108 @@
@using Groceries.Data
@using Microsoft.EntityFrameworkCore
@inject AppDbContext DbContext
<form method="post" @attributes="AdditionalAttributes">
<div data-controller="transaction-item-form">
<div class="form-field" data-transaction-item-form-target="barcodeFormField" hidden>
<label class="form-field__label" for="transactionItemBarcode">Barcode</label>
<div class="form-field__control input">
<input type="hidden" name="barcodeFormat" value="@barcode?.Format" data-transaction-item-form-target="barcodeFormat" />
<input class="input__control" id="transactionItemBarcode" name="barcodeData" value="@barcode?.BarcodeData" data-transaction-item-form-target="barcodeData" />
<button class="input__addon button" formmethod="get" formnovalidate data-action="transaction-item-form#scanBarcode" data-transaction-item-form-target="barcodeButton">
@* Barcode scanner icon *@
<svg class="icon icon--sm" xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M40-120v-200h80v120h120v80H40Zm680 0v-80h120v-120h80v200H720ZM160-240v-480h80v480h-80Zm120 0v-480h40v480h-40Zm120 0v-480h80v480h-80Zm120 0v-480h120v480H520Zm160 0v-480h40v480h-40Zm80 0v-480h40v480h-40ZM40-640v-200h200v80H120v120H40Zm800 0v-120H720v-80h200v200h-80Z" /></svg>
</button>
</div>
</div>
<fieldset class="form-field">
<legend class="form-field__label">Item</legend>
<div class="form-field__control input">
<input class="input__control flex-2" name="brand" value="@selectedItem?.Brand" placeholder="Brand" list="itemBrands" autocomplete="off" required autofocus data-action="transaction-item-form#filterNames transaction-item-form#setPriceAndQuantity" data-transaction-item-form-target="brand" />
<input class="input__control flex-5" name="name" value="@selectedItem?.Name" placeholder="Name" list="itemNames" autocomplete="off" required data-action="transaction-item-form#filterNames transaction-item-form#setPriceAndQuantity" />
<datalist id="itemBrands">
@foreach (var item in items.DistinctBy(item => item.Brand))
{
<option value="@item.Brand" />
}
</datalist>
<datalist id="itemNames">
@foreach (var item in items)
{
<option value="@item.Name" data-transaction-item-form-target="option" data-brand="@item.Brand" data-price="@item.Price" data-quantity="@item.Quantity" />
}
</datalist>
</div>
</fieldset>
<div class="form-field">
<label class="form-field__label" for="transactionItemPrice">Price</label>
<div class="form-field__control input">
@*<span class="input__inset">@CultureInfo.CurrentCulture.NumberFormat.CurrencySymbol</span>*@
<input class="input__control" id="transactionItemPrice" name="price" value="@price" type="number" min="0" step="0.01" required data-transaction-item-form-target="price" />
</div>
</div>
<div class="form-field">
<label class="form-field__label" for="transactionItemQuantity">Quantity</label>
<div class="form-field__control input">
<input class="input__control" id="transactionItemQuantity" name="quantity" value="@quantity" type="number" min="1" required data-transaction-item-form-target="quantity" />
</div>
</div>
</div>
@ChildContent
</form>
@code {
[Parameter]
public TransactionItem? TransactionItem { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? AdditionalAttributes { get; set; }
private record ItemModel(Guid Id, string Brand, string Name, decimal? Price, int? Quantity);
private ItemBarcode? barcode;
private ItemModel[] items = [];
private ItemModel? selectedItem;
private decimal? price;
private int quantity;
protected override async Task OnParametersSetAsync()
{
barcode = TransactionItem?.Item?.Barcodes.FirstOrDefault();
items = await DbContext.Items
.OrderBy(item => item.Brand)
.ThenBy(item => item.Name)
.GroupJoin(
DbContext.ItemPurchases.Where(purchase => purchase.IsLastPurchase),
item => item.Id,
lastPurchase => lastPurchase.ItemId,
(item, purchases) => new { item, purchases })
.SelectMany(
group => group.purchases.DefaultIfEmpty(),
(group, lastPurchase) => new ItemModel(
group.item.Id,
group.item.Brand,
group.item.Name,
lastPurchase != null ? lastPurchase.Price : null,
lastPurchase != null ? lastPurchase.Quantity : null))
.ToArrayAsync();
selectedItem = items.SingleOrDefault(item => item.Id == TransactionItem?.ItemId);
price = TransactionItem?.Price >= 0 ? TransactionItem.Price : selectedItem?.Price;
quantity = TransactionItem?.Quantity >= 1 ? TransactionItem.Quantity : (selectedItem?.Quantity ?? 1);
}
}

View File

@ -0,0 +1,64 @@
@using Groceries.Data
@using Microsoft.EntityFrameworkCore
@inject AppDbContext DbContext
<form method="post" @attributes="AdditionalAttributes">
<div class="form-field">
<label class="form-field__label" for="transactionPromotionName">Name</label>
<div class="form-field__control input">
<input class="input__control" id="transactionPromotionName" name="name" value="@Promotion?.Name" required autofocus />
</div>
</div>
<div class="form-field">
<label class="form-field__label" for="transactionPromotionAmount">Amount</label>
<div class="form-field__control input">
@*<span class="input__inset">@CultureInfo.CurrentCulture.NumberFormat.CurrencySymbol</span>*@
<input class="input__control" id="transactionPromotionAmount" name="amount" value="@Promotion?.Amount" type="number" min="0" step="0.01" required />
</div>
</div>
<div class="form-field">
<label class="form-field__label" for="transactionPromotionItemIds">
Items <span class="form-field__corner-hint">Optional</span>
</label>
<select class="form-field__control select" id="transactionPromotionItemIds" name="itemIds" multiple>
@foreach (var item in Transaction.Items)
{
<option value="@item.ItemId" selected="@selectedItemIds.Contains(item.ItemId)">
@itemNames.GetValueOrDefault(item.ItemId)
</option>
}
</select>
</div>
@ChildContent
</form>
@code {
[Parameter]
public required Transaction Transaction { get; set; }
[Parameter]
public TransactionPromotion? Promotion { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? AdditionalAttributes { get; set; }
private Guid[] selectedItemIds = [];
private Dictionary<Guid, string> itemNames = new();
protected override async Task OnParametersSetAsync()
{
selectedItemIds = Promotion?.Items.Select(item => item.Id).ToArray() ?? [];
var itemIds = Transaction.Items.Select(item => item.ItemId);
itemNames = await DbContext.Items
.Where(item => itemIds.Contains(item.Id))
.ToDictionaryAsync(item => item.Id, item => string.Concat(item.Brand, " ", item.Name));
}
}

View File

@ -23,48 +23,48 @@ public class TransactionsController : Controller
}
[HttpGet("new")]
public IActionResult NewTransaction()
public IResult NewTransaction()
{
return View();
return new RazorComponentResult<NewTransactionPage>();
}
[HttpPost("new")]
public IActionResult NewTransaction(DateTime createdAt, Guid storeId)
public IResult NewTransaction(DateTime createdAt, Guid storeId)
{
var transaction = new Transaction(createdAt.ToUniversalTime(), storeId);
TempData["NewTransaction"] = JsonSerializer.Serialize(transaction);
return RedirectToAction(nameof(NewTransactionItems));
return Results.LocalRedirect("/transactions/new/items");
}
[HttpGet("new/items")]
public IActionResult NewTransactionItems()
public IResult NewTransactionItems()
{
if (TempData.Peek("NewTransaction") is not string json || JsonSerializer.Deserialize<Transaction>(json) is not Transaction transaction)
{
return RedirectToAction(nameof(NewTransaction));
return Results.LocalRedirect("/transactions/new");
}
return View(transaction);
return new RazorComponentResult<NewTransactionItemsPage>(new { Transaction = transaction });
}
[HttpPost("new/items")]
public IActionResult PostNewTransactionItems()
public IResult PostNewTransactionItems()
{
if (TempData.Peek("NewTransaction") is not string json || JsonSerializer.Deserialize<Transaction>(json) is not Transaction transaction)
if (TempData.Peek("NewTransaction") is not string json || JsonSerializer.Deserialize<Transaction>(json) is null)
{
return RedirectToAction(nameof(NewTransaction));
return Results.LocalRedirect("/transactions/new");
}
return RedirectToAction(nameof(NewTransactionPromotions));
return Results.LocalRedirect("/transactions/new/promotions");
}
[HttpGet("new/items/new")]
public async Task<IActionResult> NewTransactionItem(long? barcodeData, string? barcodeFormat)
public async Task<IResult> NewTransactionItem(long? barcodeData, string? barcodeFormat)
{
if (TempData.Peek("NewTransaction") is not string json || JsonSerializer.Deserialize<Transaction>(json) is not Transaction transaction)
{
return RedirectToAction(nameof(NewTransaction));
return Results.LocalRedirect("/transactions/new");
}
TransactionItem? transactionItem = null;
@ -82,18 +82,18 @@ public class TransactionsController : Controller
transactionItem = new TransactionItem(item.Id, decimal.MinValue, int.MinValue) { Item = item };
}
var model = (transaction, transactionItem);
var parameters = new { Transaction = transaction, TransactionItem = transactionItem };
return Request.IsTurboFrameRequest("modal")
? View($"{nameof(NewTransactionItem)}_Modal", model)
: View(model);
? new RazorComponentResult<NewTransactionItemModal>(parameters)
: new RazorComponentResult<NewTransactionItemPage>(parameters);
}
[HttpPost("new/items/new")]
public async Task<IActionResult> NewTransactionItem(string brand, string name, decimal price, int quantity, long? barcodeData, string? barcodeFormat)
public async Task<IResult> NewTransactionItem(string brand, string name, decimal price, int quantity, long? barcodeData, string? barcodeFormat)
{
if (TempData.Peek("NewTransaction") is not string json || JsonSerializer.Deserialize<Transaction>(json) is not Transaction transaction)
{
return RedirectToAction(nameof(NewTransaction));
return Results.LocalRedirect("/transactions/new");
}
var itemId = await dbContext.Items
@ -124,42 +124,42 @@ public class TransactionsController : Controller
TempData["NewTransaction"] = JsonSerializer.Serialize(transaction);
return Request.IsTurboFrameRequest("modal")
? NoContent()
: RedirectToAction(nameof(NewTransactionItems));
? Results.NoContent()
: Results.LocalRedirect("/transactions/new/items");
}
[HttpGet("new/items/edit/{id}")]
public IActionResult EditTransactionItem(Guid id)
public IResult EditTransactionItem(Guid id)
{
if (TempData.Peek("NewTransaction") is not string json || JsonSerializer.Deserialize<Transaction>(json) is not Transaction transaction)
{
return RedirectToAction(nameof(NewTransaction));
return Results.LocalRedirect("/transactions/new");
}
var transactionItem = transaction.Items.SingleOrDefault(item => item.ItemId == id);
if (transactionItem == null)
{
return RedirectToAction(nameof(NewTransactionItems));
return Results.LocalRedirect("/transactions/new/items");
}
var model = (transaction, transactionItem);
var parameters = new { Transaction = transaction, TransactionItem = transactionItem };
return Request.IsTurboFrameRequest("modal")
? View($"{nameof(EditTransactionItem)}_Modal", model)
: View(model);
? new RazorComponentResult<EditTransactionItemModal>(parameters)
: new RazorComponentResult<EditTransactionItemPage>(parameters);
}
[HttpPost("new/items/edit/{id}")]
public async Task<IActionResult> EditTransactionItem(Guid id, string brand, string name, decimal price, int quantity)
public async Task<IResult> EditTransactionItem(Guid id, string brand, string name, decimal price, int quantity)
{
if (TempData.Peek("NewTransaction") is not string json || JsonSerializer.Deserialize<Transaction>(json) is not Transaction transaction)
{
return RedirectToAction(nameof(NewTransaction));
return Results.LocalRedirect("/transactions/new");
}
var transactionItem = transaction.Items.SingleOrDefault(item => item.ItemId == id);
if (transactionItem == null)
{
return RedirectToAction(nameof(NewTransactionItems));
return Results.LocalRedirect("/transactions/new/items");
}
var itemId = await dbContext.Items
@ -182,16 +182,16 @@ public class TransactionsController : Controller
TempData["NewTransaction"] = JsonSerializer.Serialize(transaction);
return Request.IsTurboFrameRequest("modal")
? NoContent()
: RedirectToAction(nameof(NewTransactionItems));
? Results.NoContent()
: Results.LocalRedirect("/transactions/new/items");
}
[HttpPost("new/items/delete/{id}")]
public IActionResult DeleteTransactionItem(Guid id)
public IResult DeleteTransactionItem(Guid id)
{
if (TempData.Peek("NewTransaction") is not string json || JsonSerializer.Deserialize<Transaction>(json) is not Transaction transaction)
{
return RedirectToAction(nameof(NewTransaction));
return Results.LocalRedirect("/transactions/new");
}
var transactionItem = transaction.Items.SingleOrDefault(item => item.ItemId == id);
@ -203,27 +203,27 @@ public class TransactionsController : Controller
TempData["NewTransaction"] = JsonSerializer.Serialize(transaction);
return Request.IsTurboFrameRequest("modal")
? NoContent()
: RedirectToAction(nameof(NewTransactionItems));
? Results.NoContent()
: Results.LocalRedirect("/transactions/new/items");
}
[HttpGet("new/promotions")]
public IActionResult NewTransactionPromotions()
public IResult NewTransactionPromotions()
{
if (TempData.Peek("NewTransaction") is not string json || JsonSerializer.Deserialize<Transaction>(json) is not Transaction transaction)
{
return RedirectToAction(nameof(NewTransaction));
return Results.LocalRedirect("/transactions/new");
}
return View(transaction);
return new RazorComponentResult<NewTransactionPromotionsPage>(new { Transaction = transaction });
}
[HttpPost("new/promotions")]
public async Task<IActionResult> PostNewTransactionPromotions()
public async Task<IResult> PostNewTransactionPromotions()
{
if (TempData["NewTransaction"] is not string json || JsonSerializer.Deserialize<Transaction>(json) is not Transaction transaction)
{
return RedirectToAction(nameof(NewTransaction));
return Results.LocalRedirect("/transactions/new");
}
// Work around EF trying to insert items by explicitly tracking them as unchanged
@ -235,28 +235,29 @@ public class TransactionsController : Controller
dbContext.Transactions.Add(transaction);
await dbContext.SaveChangesAsync();
return RedirectToAction(nameof(Index), new { page = 1 });
return Results.LocalRedirect("/transactions?page=1");
}
[HttpGet("new/promotions/new")]
public IActionResult NewTransactionPromotion()
public IResult NewTransactionPromotion()
{
if (TempData.Peek("NewTransaction") is not string json || JsonSerializer.Deserialize<Transaction>(json) is not Transaction transaction)
{
return RedirectToAction(nameof(NewTransaction));
return Results.LocalRedirect("/transactions/new");
}
var parameters = new { Transaction = transaction };
return Request.IsTurboFrameRequest("modal")
? View($"{nameof(NewTransactionPromotion)}_Modal", transaction)
: View(transaction);
? new RazorComponentResult<NewTransactionPromotionModal>(parameters)
: new RazorComponentResult<NewTransactionPromotionPage>(parameters);
}
[HttpPost("new/promotions/new")]
public IActionResult NewTransactionPromotion(string name, decimal amount, Guid[] itemIds)
public IResult NewTransactionPromotion(string name, decimal amount, Guid[] itemIds)
{
if (TempData.Peek("NewTransaction") is not string json || JsonSerializer.Deserialize<Transaction>(json) is not Transaction transaction)
{
return RedirectToAction(nameof(NewTransaction));
return Results.LocalRedirect("/transactions/new");
}
// TODO: Handle promotion already in transaction - merge, replace, error?
@ -267,42 +268,42 @@ public class TransactionsController : Controller
TempData["NewTransaction"] = JsonSerializer.Serialize(transaction);
return Request.IsTurboFrameRequest("modal")
? NoContent()
: RedirectToAction(nameof(NewTransactionPromotions));
? Results.NoContent()
: Results.LocalRedirect("/transactions/new/promotions");
}
[HttpGet("new/promotions/edit/{id}")]
public IActionResult EditTransactionPromotion(Guid id)
public IResult EditTransactionPromotion(Guid id)
{
if (TempData.Peek("NewTransaction") is not string json || JsonSerializer.Deserialize<Transaction>(json) is not Transaction transaction)
{
return RedirectToAction(nameof(NewTransaction));
return Results.LocalRedirect("/transactions/new");
}
var promotion = transaction.Promotions.SingleOrDefault(promotion => promotion.Id == id);
if (promotion == null)
{
return RedirectToAction(nameof(NewTransactionPromotions));
return Results.LocalRedirect("/transactions/new/promotions");
}
var model = (transaction, promotion);
var parameters = new { Transaction = transaction, Promotion = promotion };
return Request.IsTurboFrameRequest("modal")
? View($"{nameof(EditTransactionPromotion)}_Modal", model)
: View(model);
? new RazorComponentResult<EditTransactionPromotionModal>(parameters)
: new RazorComponentResult<EditTransactionPromotionPage>(parameters);
}
[HttpPost("new/promotions/edit/{id}")]
public IActionResult EditTransactionPromotion(Guid id, string name, decimal amount, Guid[] itemIds)
public IResult EditTransactionPromotion(Guid id, string name, decimal amount, Guid[] itemIds)
{
if (TempData.Peek("NewTransaction") is not string json || JsonSerializer.Deserialize<Transaction>(json) is not Transaction transaction)
{
return RedirectToAction(nameof(NewTransaction));
return Results.LocalRedirect("/transactions/new");
}
var promotion = transaction.Promotions.SingleOrDefault(promotion => promotion.Id == id);
if (promotion == null)
{
return RedirectToAction(nameof(NewTransactionPromotions));
return Results.LocalRedirect("/transactions/new/promotions");
}
promotion.Name = name;
@ -312,16 +313,16 @@ public class TransactionsController : Controller
TempData["NewTransaction"] = JsonSerializer.Serialize(transaction);
return Request.IsTurboFrameRequest("modal")
? NoContent()
: RedirectToAction(nameof(NewTransactionPromotions));
? Results.NoContent()
: Results.LocalRedirect("/transactions/new/promotions");
}
[HttpPost("new/promotions/delete/{id}")]
public IActionResult DeleteTransactionPromotion(Guid id)
public IResult DeleteTransactionPromotion(Guid id)
{
if (TempData.Peek("NewTransaction") is not string json || JsonSerializer.Deserialize<Transaction>(json) is not Transaction transaction)
{
return RedirectToAction(nameof(NewTransaction));
return Results.LocalRedirect("/transactions/new");
}
var promotion = transaction.Promotions.SingleOrDefault(promotion => promotion.Id == id);
@ -333,7 +334,7 @@ public class TransactionsController : Controller
TempData["NewTransaction"] = JsonSerializer.Serialize(transaction);
return Request.IsTurboFrameRequest("modal")
? NoContent()
: RedirectToAction(nameof(NewTransactionPromotions));
? Results.NoContent()
: Results.LocalRedirect("/transactions/new/promotions");
}
}

View File

@ -1,84 +0,0 @@
@using Groceries.Data
@using Microsoft.EntityFrameworkCore
@model TransactionItem?
@inject AppDbContext dbContext
@{
var items = await dbContext.Items
.OrderBy(item => item.Brand)
.ThenBy(item => item.Name)
.GroupJoin(
dbContext.ItemPurchases.Where(purchase => purchase.IsLastPurchase),
item => item.Id,
lastPurchase => lastPurchase.ItemId,
(item, lastPurchase) => new { item, lastPurchase })
.SelectMany(
group => group.lastPurchase.DefaultIfEmpty(),
(group, lastPurchase) => new
{
group.item.Id,
group.item.Brand,
group.item.Name,
Price = lastPurchase != null ? lastPurchase.Price : (decimal?)null,
Quantity = lastPurchase != null ? lastPurchase.Quantity : (int?)null,
})
.ToArrayAsync();
var selectedItem = items.SingleOrDefault(item => item.Id == Model?.ItemId);
var barcode = Model?.Item?.Barcodes.FirstOrDefault();
var price = Model?.Price >= 0 ? Model.Price : selectedItem?.Price;
var quantity = Model?.Quantity >= 1 ? Model.Quantity : (selectedItem?.Quantity ?? 1);
}
<div data-controller="transaction-item-form">
<div class="form-field" data-transaction-item-form-target="barcodeFormField" hidden>
<label class="form-field__label" for="transactionItemBarcode">Barcode</label>
<div class="form-field__control input">
<input type="hidden" name="barcodeFormat" value="@barcode?.Format" data-transaction-item-form-target="barcodeFormat" />
<input class="input__control" id="transactionItemBarcode" name="barcodeData" value="@barcode?.BarcodeData" data-transaction-item-form-target="barcodeData" />
<button class="input__addon button" formmethod="get" formnovalidate data-action="transaction-item-form#scanBarcode" data-transaction-item-form-target="barcodeButton">
@* Barcode scanner icon *@
<svg class="icon icon--sm" xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M40-120v-200h80v120h120v80H40Zm680 0v-80h120v-120h80v200H720ZM160-240v-480h80v480h-80Zm120 0v-480h40v480h-40Zm120 0v-480h80v480h-80Zm120 0v-480h120v480H520Zm160 0v-480h40v480h-40Zm80 0v-480h40v480h-40ZM40-640v-200h200v80H120v120H40Zm800 0v-120H720v-80h200v200h-80Z" /></svg>
</button>
</div>
</div>
<fieldset class="form-field">
<legend class="form-field__label">Item</legend>
<div class="form-field__control input">
<input class="input__control flex-2" name="brand" value="@selectedItem?.Brand" placeholder="Brand" list="itemBrands" autocomplete="off" required autofocus data-action="transaction-item-form#filterNames transaction-item-form#setPriceAndQuantity" data-transaction-item-form-target="brand" />
<input class="input__control flex-5" name="name" value="@selectedItem?.Name" placeholder="Name" list="itemNames" autocomplete="off" required data-action="transaction-item-form#setPriceAndQuantity" />
<datalist id="itemBrands">
@foreach (var item in items.DistinctBy(item => item.Brand))
{
<option value="@item.Brand" />
}
</datalist>
<datalist id="itemNames">
@foreach (var item in items)
{
<option value="@item.Name" data-transaction-item-form-target="option" data-brand="@item.Brand" data-price="@item.Price" data-quantity="@item.Quantity" />
}
</datalist>
</div>
</fieldset>
<div class="form-field">
<label class="form-field__label" for="transactionItemPrice">Price</label>
<div class="form-field__control input">
@*<span class="input__inset">@CultureInfo.CurrentCulture.NumberFormat.CurrencySymbol</span>*@
<input class="input__control" id="transactionItemPrice" name="price" value="@price" type="number" min="0" step="0.01" required data-transaction-item-form-target="price" />
</div>
</div>
<div class="form-field">
<label class="form-field__label" for="transactionItemQuantity">Quantity</label>
<div class="form-field__control input">
<input class="input__control" id="transactionItemQuantity" name="quantity" value="@quantity" type="number" min="1" required data-transaction-item-form-target="quantity" />
</div>
</div>
</div>

View File

@ -1,41 +0,0 @@
@using Groceries.Data
@using Microsoft.EntityFrameworkCore
@model (Transaction Transaction, TransactionPromotion? Promotion)
@inject AppDbContext dbContext
@{
var selectedItemIds = Model.Promotion?.Items.Select(item => item.Id).ToArray() ?? Array.Empty<Guid>();
var itemIds = Model.Transaction.Items.Select(item => item.ItemId);
var itemNames = await dbContext.Items
.Where(item => itemIds.Contains(item.Id))
.ToDictionaryAsync(item => item.Id, item => string.Concat(item.Brand, " ", item.Name));
}
<div class="form-field">
<label class="form-field__label" for="transactionPromotionName">Name</label>
<div class="form-field__control input">
<input class="input__control" id="transactionPromotionName" name="name" value="@Model.Promotion?.Name" required autofocus />
</div>
</div>
<div class="form-field">
<label class="form-field__label" for="transactionPromotionAmount">Amount</label>
<div class="form-field__control input">
@*<span class="input__inset">@CultureInfo.CurrentCulture.NumberFormat.CurrencySymbol</span>*@
<input class="input__control" id="transactionPromotionAmount" name="amount" value="@Model.Promotion?.Amount" type="number" min="0" step="0.01" required />
</div>
</div>
<div class="form-field">
<label class="form-field__label" for="transactionPromotionItemIds">
Items
<span class="form-field__corner-hint">Optional</span>
</label>
<select class="form-field__control select" id="transactionPromotionItemIds" name="itemIds" multiple>
@foreach (var item in Model.Transaction.Items)
{
<option value="@item.ItemId" selected="@selectedItemIds.Contains(item.ItemId)">@itemNames[item.ItemId]</option>
}
</select>
</div>