Default to last purchased price and quantity when adding transaction item

This commit is contained in:
James Chapman 2023-07-28 20:23:31 +01:00
parent 4fc1052829
commit 560c162ee1
Signed by: jamsch0
GPG Key ID: 765FE58130277547
7 changed files with 104 additions and 58 deletions

View File

@ -8,6 +8,7 @@ public class ItemPurchase
public Guid StoreId { get; init; }
public decimal Price { get; init; }
public int Quantity { get; init; }
public bool IsLastPurchase { get; init; }
public Item? Item { get; init; }
public Transaction? Transaction { get; init; }

View File

@ -0,0 +1,14 @@
CREATE OR REPLACE VIEW item_purchases AS
SELECT
item_id,
transaction_id,
created_at,
store_id,
price,
quantity,
CASE ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY created_at DESC)
WHEN 1 THEN true
ELSE false
END AS is_last_purchase
FROM transaction_items
JOIN transactions USING (transaction_id);

View File

@ -25,19 +25,11 @@ public class ItemsController : Controller
itemsQuery = itemsQuery.Where(item => EF.Functions.ILike(item.Brand + ' ' + item.Name, searchPattern));
}
var lastPurchasesQuery = dbContext.ItemPurchases
.GroupBy(purchase => purchase.ItemId)
.Select(purchases => new
{
ItemId = purchases.Key,
CreatedAt = purchases.Max(purchase => purchase.CreatedAt),
});
var items = await itemsQuery
.OrderBy(item => item.Brand)
.ThenBy(item => item.Name)
.GroupJoin(
lastPurchasesQuery,
dbContext.ItemPurchases.Where(purchase => purchase.IsLastPurchase),
item => item.Id,
lastPurchase => lastPurchase.ItemId,
(item, lastPurchase) => new { item, lastPurchase })

View File

@ -7,44 +7,61 @@
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);
}
<fieldset class="form-field">
<legend class="form-field__label">Item</legend>
<div class="form-field__control input" data-controller="list-filter">
<input class="input__control flex-2" name="brand" value="@selectedItem?.Brand" placeholder="Brand" list="itemBrands" autocomplete="off" required autofocus data-action="list-filter#filter" />
<input class="input__control flex-5" name="name" value="@selectedItem?.Name" placeholder="Name" list="itemNames" autocomplete="off" required />
<div data-controller="transaction-item-form">
<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" />
<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="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-list-filter-target="option" data-list-filter-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="@Model?.Price" type="number" min="0" step="0.01" required data-transaction-item-form-target="price" />
</div>
</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="@Model?.Price" type="number" min="0" step="0.01" required />
</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="@(Model?.Quantity ?? 1)" type="number" min="1" required />
<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="@(Model?.Quantity ?? 1)" type="number" min="1" required data-transaction-item-form-target="quantity" />
</div>
</div>
</div>

View File

@ -1,17 +0,0 @@
import { Controller } from "/lib/hotwired/stimulus/dist/stimulus.js";
export default class ListFilterController extends Controller {
static targets = ["option"];
filter(event) {
for (const option of this.optionTargets) {
if (!event.target.value) {
option.disabled = false;
continue;
}
const value = option.getAttribute("data-list-filter-value");
option.disabled = value !== event.target.value;
}
}
}

View File

@ -0,0 +1,39 @@
import { Controller } from "/lib/hotwired/stimulus/dist/stimulus.js";
export default class TransactionItemFormController extends Controller {
static targets = ["option", "price", "quantity"];
filterNames(event) {
for (const option of this.optionTargets) {
if (!event.target.value) {
option.disabled = false;
continue;
}
const value = option.getAttribute("data-brand");
option.disabled = value !== event.target.value;
}
}
setPriceAndQuantity(event) {
const { brand, name } = event.target.form.elements;
if (!brand.value || !name.value) {
this.priceTarget.value = "";
this.quantityTarget.value = "1";
return;
}
const option = this.optionTargets.find(option =>
option.getAttribute("data-brand") === brand.value &&
option.value === name.value);
if (option != null) {
if (!this.priceTarget.value) {
this.priceTarget.value = option.getAttribute("data-price");
}
if (!this.quantityTarget.value || this.quantityTarget.value === "1") {
this.quantityTarget.value = option.getAttribute("data-quantity");
}
}
}
}

View File

@ -1,13 +1,13 @@
import { Application } from "/lib/hotwired/stimulus/dist/stimulus.js";
import ListFilterController from "./controllers/list-filter.js";
import ModalController from "./controllers/modal.js";
import SearchFormController from "./controllers/search-form.js";
import TransactionItemFormController from "./controllers/transaction-item-form.js";
const app = Application.start();
app.register("list-filter", ListFilterController);
app.register("modal", ModalController);
app.register("search-form", SearchFormController);
app.register("transaction-item-form", TransactionItemFormController);
let timeout;
document.addEventListener("turbo:visit", () => {