Refactor New Transaction pages/modals to Razor components

This commit is contained in:
James Chapman 2023-12-03 19:41:06 +00:00
parent 74cb6109c9
commit 70a34b67d6
Signed by: jamsch0
GPG Key ID: 765FE58130277547
30 changed files with 576 additions and 550 deletions

View File

@ -1,27 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="color-scheme" content="light dark" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Groceries@(ViewBag.Title != null ? Html.Raw($" &ndash; {ViewBag.Title}") : "")</title>
<link rel="stylesheet" type="text/css" href="/css/main.css" asp-append-version="true" data-turbo-track="reload" />
<script type="module" src="/js/main.js" asp-append-version="true" data-turbo-track="reload"></script>
<script type="module" src="/lib/hotwired/turbo/dist/turbo.es2017-esm.js"></script>
@RenderSection("head", required: false)
</head>
<body>
<partial name="_LayoutSidebar" />
<main class="main-content">
@*<turbo-frame id="main" target="_top">*@
@RenderBody()
@*</turbo-frame>*@
</main>
<dialog class="modal" data-controller="modal" data-action="turbo:frame-load->modal#open turbo:before-cache@document->modal#close popstate@window->modal#close">
<turbo-frame id="modal" data-modal-target="frame"></turbo-frame>
</dialog>
</body>
</html>

View File

@ -1,63 +0,0 @@
@{
var controller = ViewContext.RouteData.Values["controller"]?.ToString() ?? string.Empty;
}
<input class="sidebar__toggle" id="sidebarToggle" type="checkbox" role="button" data-turbo-permanent />
<label for="sidebarToggle">Menu</label>
<section class="sidebar">
<header class="sidebar__header">
Groceries
</header>
<nav class="sidebar__body">
<ul>
<li>
<a class="sidebar__item @(controller == "Home" ? "sidebar__item--active" : "")" asp-controller="Home" asp-action="Index">
@* dashboard squares icon *@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 5v2h-4V5h4M9 5v6H5V5h4m10 8v6h-4v-6h4M9 17v2H5v-2h4M21 3h-8v6h8V3zM11 3H3v10h8V3zm10 8h-8v10h8V11zm-10 4H3v6h8v-6z"/></svg>
Dashboard
</a>
</li>
<li>
<a class="sidebar__item @(controller == "Lists" ? "sidebar__item--active" : "")" asp-controller="Lists" asp-action="Index" asp-route-page="1">
@* receipt long icon *@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24"><path d="M0,0h24v24H0V0z" fill="none"/><g><path d="M19.5,3.5L18,2l-1.5,1.5L15,2l-1.5,1.5L12,2l-1.5,1.5L9,2L7.5,3.5L6,2v14H3v3c0,1.66,1.34,3,3,3h12c1.66,0,3-1.34,3-3V2 L19.5,3.5z M15,20H6c-0.55,0-1-0.45-1-1v-1h10V20z M19,19c0,0.55-0.45,1-1,1s-1-0.45-1-1v-3H8V5h11V19z"/><rect height="2" width="6" x="9" y="7"/><rect height="2" width="2" x="16" y="7"/><rect height="2" width="6" x="9" y="10"/><rect height="2" width="2" x="16" y="10"/></g></svg>
Lists
</a>
</li>
<li>
<a class="sidebar__item @(controller == "Transactions" ? "sidebar__item--active" : "")" asp-controller="Transactions" asp-action="Index" asp-route-page="1">
@* shopping cart icon *@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M15.55 13c.75 0 1.41-.41 1.75-1.03l3.58-6.49c.37-.66-.11-1.48-.87-1.48H5.21l-.94-2H1v2h2l3.6 7.59-1.35 2.44C4.52 15.37 5.48 17 7 17h12v-2H7l1.1-2h7.45zM6.16 6h12.15l-2.76 5H8.53L6.16 6zM7 18c-1.1 0-1.99.9-1.99 2S5.9 22 7 22s2-.9 2-2-.9-2-2-2zm10 0c-1.1 0-1.99.9-1.99 2s.89 2 1.99 2 2-.9 2-2-.9-2-2-2z"/></svg>
Transactions
</a>
</li>
<li>
<a class="sidebar__item @(controller == "Items" ? "sidebar__item--active" : "")" asp-controller="Items" asp-action="Index" asp-route-page="1">
@* category shapes icon *@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 2l-5.5 9h11L12 2zm0 3.84L13.93 9h-3.87L12 5.84zM17.5 13c-2.49 0-4.5 2.01-4.5 4.5s2.01 4.5 4.5 4.5 4.5-2.01 4.5-4.5-2.01-4.5-4.5-4.5zm0 7c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5zM3 21.5h8v-8H3v8zm2-6h4v4H5v-4z"/></svg>
Items
</a>
</li>
<li>
<a class="sidebar__item" href="/stores?page=1">
@* store building icon *@
<svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M18.36 9l.6 3H5.04l.6-3h12.72M20 4H4v2h16V4zm0 3H4l-1 5v2h1v6h10v-6h4v6h2v-6h1v-2l-1-5zM6 18v-4h6v4H6z"/></svg>
Stores
</a>
</li>
</ul>
</nav>
@*<div class="slide-toggle">
<label class="slide-toggle__option">
<svg class="icon icon--sm" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24"><rect fill="none" height="24" width="24"/><path d="M12,9c1.65,0,3,1.35,3,3s-1.35,3-3,3s-3-1.35-3-3S10.35,9,12,9 M12,7c-2.76,0-5,2.24-5,5s2.24,5,5,5s5-2.24,5-5 S14.76,7,12,7L12,7z M2,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S1.45,13,2,13z M20,13l2,0c0.55,0,1-0.45,1-1 s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S19.45,13,20,13z M11,2v2c0,0.55,0.45,1,1,1s1-0.45,1-1V2c0-0.55-0.45-1-1-1S11,1.45,11,2z M11,20v2c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1C11.45,19,11,19.45,11,20z M5.99,4.58c-0.39-0.39-1.03-0.39-1.41,0 c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0s0.39-1.03,0-1.41L5.99,4.58z M18.36,16.95 c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0c0.39-0.39,0.39-1.03,0-1.41 L18.36,16.95z M19.42,5.99c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41 s1.03,0.39,1.41,0L19.42,5.99z M7.05,18.36c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06 c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L7.05,18.36z"/></svg>
Light
<input class="slide-toggle__control" type="radio" name="colorScheme" value="light" />
</label>
<label class="slide-toggle__option">
<svg class="icon icon--sm" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24"><rect fill="none" height="24" width="24"/><path d="M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"/></svg>
Dark
<input class="slide-toggle__control" type="radio" name="colorScheme" value="dark" />
</label>
</div>*@
</section>

View File

@ -1,9 +0,0 @@
<turbo-frame id="modal" data-modal-target="frame">
<article class="card">
<header class="card__header row">
<h2 class="row__fill">@ViewBag.Title</h2>
<button class="button modal__close-button" data-action="modal#close">&#x1F5D9;</button>
</header>
@RenderBody()
</article>
</turbo-frame>

View File

@ -2,7 +2,6 @@ using DbUp;
using Groceries.Data;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
@ -44,13 +43,7 @@ if (env.IsProduction())
}
builder.Services
.AddControllersWithViews()
.AddRazorOptions(options =>
{
options.ViewLocationFormats.Clear();
options.ViewLocationFormats.Add("/{1}/{0}" + RazorViewEngine.ViewExtension);
options.ViewLocationFormats.Add("/Common/{0}" + RazorViewEngine.ViewExtension);
})
.AddControllers()
.AddSessionStateTempDataProvider();
builder.Services.AddDistributedMemoryCache();

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>

View File

@ -1,3 +0,0 @@
@using Groceries
@namespace Groceries.Views
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@ -1,3 +0,0 @@
@{
Layout = ViewBag.RenderingToTurboStream == true ? null : "_Layout";
}