Initial commit
This commit is contained in:
36
Groceries/Stores/EditStore.cshtml
Normal file
36
Groceries/Stores/EditStore.cshtml
Normal file
@ -0,0 +1,36 @@
|
||||
@using Microsoft.AspNetCore.Http
|
||||
@using Microsoft.AspNetCore.Http.Extensions;
|
||||
|
||||
@model Groceries.Data.Store
|
||||
@{
|
||||
ViewBag.Title = "Edit Store";
|
||||
|
||||
var returnUrl = Url.Action("Index", new { page = 1 });
|
||||
if (Context.Request.GetTypedHeaders().Referer is Uri referer && referer.Host == Context.Request.Host.Host)
|
||||
{
|
||||
var requestUrl = new UriBuilder
|
||||
{
|
||||
Scheme = Context.Request.Scheme,
|
||||
Host = Context.Request.Host.Host,
|
||||
Port = Context.Request.Host.Port.GetValueOrDefault(-1),
|
||||
Path = Context.Request.Path.ToString(),
|
||||
Query = Context.Request.QueryString.ToString(),
|
||||
}.Uri;
|
||||
|
||||
if (referer != requestUrl)
|
||||
{
|
||||
returnUrl = referer.PathAndQuery;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<h1>Edit Store</h1>
|
||||
|
||||
<form method="post" asp-action="EditStore">
|
||||
<partial name="_StoreForm" />
|
||||
|
||||
<div class="row">
|
||||
<button class="button button--primary" type="submit">Save</button>
|
||||
<a class="button" href="@returnUrl">Cancel</a>
|
||||
</div>
|
||||
</form>
|
14
Groceries/Stores/EditStore_Modal.cshtml
Normal file
14
Groceries/Stores/EditStore_Modal.cshtml
Normal file
@ -0,0 +1,14 @@
|
||||
@model Groceries.Data.Store
|
||||
@{
|
||||
Layout = "_Modal";
|
||||
ViewBag.Title = "Edit Store";
|
||||
}
|
||||
|
||||
<form class="card__content" id="editStore" method="post" asp-action="EditStore" data-action="turbo:submit-end->modal#close">
|
||||
<partial name="_StoreForm" />
|
||||
</form>
|
||||
|
||||
<footer class="card__footer card__footer--shaded row">
|
||||
<button class="button button--primary" type="submit" form="editStore">Save</button>
|
||||
<button class="button" data-action="modal#close">Cancel</button>
|
||||
</footer>
|
52
Groceries/Stores/Index.cshtml
Normal file
52
Groceries/Stores/Index.cshtml
Normal file
@ -0,0 +1,52 @@
|
||||
@using Groceries.Stores
|
||||
@model StoreListModel
|
||||
@{
|
||||
ViewBag.Title = "Stores";
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<h1 class="row__fill">Stores</h1>
|
||||
<form method="get" data-controller="search-form" data-turbo-frame="table" data-turbo-action="advance">
|
||||
<input type="hidden" name="page" value="1" />
|
||||
<div class="form-field">
|
||||
<div class="form-field__control input input--leading-inset input--trailing-addon">
|
||||
<div class="input__inset">
|
||||
@* Search icon *@
|
||||
<svg class="icon icon--sm" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none" /><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z" /></svg>
|
||||
</div>
|
||||
<input class="input__control" type="search" name="search" value="@Model.Search" placeholder="Search" autocomplete="off" data-action="search-form#input" />
|
||||
<button class="input__addon button" data-search-form-target="button">Search</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<a class="button button--primary" asp-action="NewStore" data-turbo-frame="modal">New store</a>
|
||||
</div>
|
||||
|
||||
<turbo-frame id="table" target="_top">
|
||||
<section class="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="table__header">Retailer</th>
|
||||
<th scope="col" class="table__header" style="width: 100%">Name</th>
|
||||
<th scope="col" class="table__header">Transactions</th>
|
||||
<th scope="col" class="table__header"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var store in Model.Stores)
|
||||
{
|
||||
<tr>
|
||||
<td class="table__cell">@store.Retailer</td>
|
||||
<td class="table__cell">@store.Name</td>
|
||||
<td class="table__cell table__cell--numeric">@store.TotalTransactions</td>
|
||||
<td class="table__cell">
|
||||
<a class="link" asp-action="EditStore" asp-route-id="@store.Id" data-turbo-frame="modal">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<partial name="_TablePaginator" model="Model.Stores" />
|
||||
</section>
|
||||
</turbo-frame>
|
35
Groceries/Stores/NewStore.cshtml
Normal file
35
Groceries/Stores/NewStore.cshtml
Normal file
@ -0,0 +1,35 @@
|
||||
@using Microsoft.AspNetCore.Http
|
||||
@using Microsoft.AspNetCore.Http.Extensions;
|
||||
|
||||
@{
|
||||
ViewBag.Title = "New Store";
|
||||
|
||||
var returnUrl = Url.Action("Index", new { page = 1 });
|
||||
if (Context.Request.GetTypedHeaders().Referer is Uri referer && referer.Host == Context.Request.Host.Host)
|
||||
{
|
||||
var requestUrl = new UriBuilder
|
||||
{
|
||||
Scheme = Context.Request.Scheme,
|
||||
Host = Context.Request.Host.Host,
|
||||
Port = Context.Request.Host.Port.GetValueOrDefault(-1),
|
||||
Path = Context.Request.Path.ToString(),
|
||||
Query = Context.Request.QueryString.ToString(),
|
||||
}.Uri;
|
||||
|
||||
if (referer != requestUrl)
|
||||
{
|
||||
returnUrl = referer.PathAndQuery;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<h1>New Store</h1>
|
||||
|
||||
<form method="post" asp-action="NewStore">
|
||||
<partial name="_StoreForm" />
|
||||
|
||||
<div class="row">
|
||||
<button class="button button--primary">Save</button>
|
||||
<a class="button" href="@returnUrl">Cancel</a>
|
||||
</div>
|
||||
</form>
|
13
Groceries/Stores/NewStore_Modal.cshtml
Normal file
13
Groceries/Stores/NewStore_Modal.cshtml
Normal file
@ -0,0 +1,13 @@
|
||||
@{
|
||||
Layout = "_Modal";
|
||||
ViewBag.Title = "New Store";
|
||||
}
|
||||
|
||||
<form class="card__content" id="newStore" method="post" asp-action="NewStore" data-action="turbo:submit-end->modal#close">
|
||||
<partial name="_StoreForm" />
|
||||
</form>
|
||||
|
||||
<footer class="card__footer card__footer--shaded row">
|
||||
<button class="button button--primary" type="submit" form="newStore">Save</button>
|
||||
<button class="button" data-action="modal#close">Cancel</button>
|
||||
</footer>
|
9
Groceries/Stores/StoreListModel.cs
Normal file
9
Groceries/Stores/StoreListModel.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Groceries.Stores;
|
||||
|
||||
public record StoreListModel(string? Search, ListPageModel<StoreListModel.Store> Stores)
|
||||
{
|
||||
public record Store(Guid Id, string Retailer, string Name)
|
||||
{
|
||||
public int TotalTransactions { get; init; }
|
||||
}
|
||||
}
|
95
Groceries/Stores/StoresController.cs
Normal file
95
Groceries/Stores/StoresController.cs
Normal file
@ -0,0 +1,95 @@
|
||||
namespace Groceries.Stores;
|
||||
|
||||
using Groceries.Common;
|
||||
using Groceries.Data;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
[Route("/stores")]
|
||||
public class StoresController : Controller
|
||||
{
|
||||
private readonly AppDbContext dbContext;
|
||||
|
||||
public StoresController(AppDbContext dbContext)
|
||||
{
|
||||
this.dbContext = dbContext;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Index(int page, string? search)
|
||||
{
|
||||
var storesQuery = dbContext.Stores.AsQueryable();
|
||||
if (!string.IsNullOrEmpty(search))
|
||||
{
|
||||
var searchPattern = $"%{search}%";
|
||||
storesQuery = storesQuery.Where(store => EF.Functions.ILike(store.Retailer!.Name + ' ' + store.Name, searchPattern));
|
||||
}
|
||||
|
||||
var stores = await storesQuery
|
||||
.OrderBy(store => store.Retailer!.Name)
|
||||
.ThenBy(store => store.Name)
|
||||
.Select(store => new StoreListModel.Store(store.Id, store.Retailer!.Name, store.Name)
|
||||
{
|
||||
TotalTransactions = store.Transactions!.Count(),
|
||||
})
|
||||
.ToListPageModelAsync(page, cancellationToken: HttpContext.RequestAborted);
|
||||
|
||||
if (stores.Page != page)
|
||||
{
|
||||
return RedirectToAction(nameof(Index), new { page = stores.Page, search });
|
||||
}
|
||||
|
||||
var model = new StoreListModel(search, stores);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpGet("new")]
|
||||
public IActionResult NewStore()
|
||||
{
|
||||
return Request.IsTurboFrameRequest("modal")
|
||||
? View($"{nameof(NewStore)}_Modal")
|
||||
: View();
|
||||
}
|
||||
|
||||
[HttpPost("new")]
|
||||
public async Task<IActionResult> NewStore(Guid retailerId, string name, string? address)
|
||||
{
|
||||
var store = new Store(retailerId, name, address);
|
||||
dbContext.Stores.Add(store);
|
||||
|
||||
await dbContext.SaveChangesAsync(HttpContext.RequestAborted);
|
||||
|
||||
return Request.IsTurboFrameRequest("modal")
|
||||
? NoContent()
|
||||
: RedirectToAction(nameof(Index), new { page = 1 });
|
||||
}
|
||||
|
||||
[HttpGet("edit/{id}")]
|
||||
public async Task<IActionResult> EditStore(Guid id)
|
||||
{
|
||||
var store = await dbContext.Stores
|
||||
.SingleOrDefaultAsync(store => store.Id == id, HttpContext.RequestAborted);
|
||||
|
||||
if (store == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Request.IsTurboFrameRequest("modal")
|
||||
? View($"{nameof(EditStore)}_Modal", store)
|
||||
: View(store);
|
||||
}
|
||||
|
||||
[HttpPost("edit/{id}")]
|
||||
public async Task<IActionResult> EditStore(Guid id, Guid retailerId, string name, string? address)
|
||||
{
|
||||
var store = new Store(id, retailerId, name, address);
|
||||
dbContext.Stores.Update(store);
|
||||
|
||||
await dbContext.SaveChangesAsync(HttpContext.RequestAborted);
|
||||
|
||||
return Request.IsTurboFrameRequest("modal")
|
||||
? NoContent()
|
||||
: RedirectToAction(nameof(EditStore));
|
||||
}
|
||||
}
|
35
Groceries/Stores/_StoreForm.cshtml
Normal file
35
Groceries/Stores/_StoreForm.cshtml
Normal file
@ -0,0 +1,35 @@
|
||||
@using Groceries.Data
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
|
||||
@model Store?
|
||||
@inject AppDbContext dbContext
|
||||
@{
|
||||
var retailers = await dbContext.Retailers
|
||||
.OrderBy(retailer => retailer.Name)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
<div class="form-field">
|
||||
<label class="form-field__label" for="storeRetailerId">Retailer</label>
|
||||
<select class="form-field__control select" id="storeRetailerId" name="retailerId" required autofocus>
|
||||
@foreach (var retailer in retailers)
|
||||
{
|
||||
<option value="@retailer.Id" selected="@(retailer.Id == Model?.RetailerId)">@retailer.Name</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label class="form-field__label" for="storeName">Name</label>
|
||||
<div class="form-field__control input">
|
||||
<input class="input__control" id="storeName" name="name" value="@Model?.Name" required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label class="form-field__label" for="storeAddress">
|
||||
Address
|
||||
<span class="form-field__corner-hint">Optional</span>
|
||||
</label>
|
||||
<textarea class="form-field__control textarea" id="storeAddress" name="address" rows="4">@Model?.Address</textarea>
|
||||
</div>
|
Reference in New Issue
Block a user