Compare commits
1 Commits
main
...
a568c3a48f
Author | SHA1 | Date | |
---|---|---|---|
a568c3a48f
|
@ -12,32 +12,32 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Setup Buildx
|
- name: Setup Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Login to Container Registry
|
- name: Login to Container Registry
|
||||||
if: gitea.event_name != 'pull_request'
|
if: gitea.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
registry: git.jamsch0.dev
|
registry: git.jamsch0.dev
|
||||||
username: ${{gitea.actor}}
|
username: ${{gitea.actor}}
|
||||||
password: ${{secrets.CI_TOKEN}}
|
password: ${{secrets.CI_TOKEN}}
|
||||||
|
|
||||||
- name: Extract metadata
|
- name: Extract metadata
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v4
|
||||||
id: meta
|
id: meta
|
||||||
with:
|
with:
|
||||||
images: git.jamsch0.dev/jamsch0/groceries
|
images: git.jamsch0.dev/jamsch0/groceries
|
||||||
tags: type=raw,value=latest,enable={{is_default_branch}}
|
tags: type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
cache-from: type=gha
|
# cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
# cache-to: type=gha,mode=max
|
||||||
push: ${{gitea.event_name != 'pull_request'}}
|
push: ${{gitea.event_name != 'pull_request'}}
|
||||||
tags: ${{steps.meta.outputs.tags}}
|
tags: ${{steps.meta.outputs.tags}}
|
||||||
labels: ${{steps.meta.outputs.labels}}
|
labels: ${{steps.meta.outputs.labels}}
|
12
Dockerfile
12
Dockerfile
@ -1,6 +1,4 @@
|
|||||||
# syntax=docker/dockerfile:1.7-labs
|
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build1
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:10.0-preview-alpine AS build1
|
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY ./.config ./
|
COPY ./.config ./
|
||||||
@ -10,20 +8,21 @@ WORKDIR Groceries
|
|||||||
COPY ./Groceries/libman.json ./
|
COPY ./Groceries/libman.json ./
|
||||||
RUN dotnet libman restore
|
RUN dotnet libman restore
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:10.0-preview-alpine AS build2
|
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build2
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY ./Groceries.sln ./
|
COPY ./Groceries.sln ./
|
||||||
COPY ./Directory.Build.props ./
|
COPY ./Directory.Build.props ./
|
||||||
|
|
||||||
COPY --parents */*.csproj .
|
COPY */*.csproj ./
|
||||||
|
RUN for file in $(ls *.csproj); do mkdir -p ${file%.*} && mv $file ${file%.*}; done
|
||||||
RUN dotnet restore
|
RUN dotnet restore
|
||||||
|
|
||||||
COPY . ./
|
COPY . ./
|
||||||
COPY --from=build1 /src ./
|
COPY --from=build1 /src ./
|
||||||
RUN dotnet publish --no-restore --output /out
|
RUN dotnet publish --no-restore --output /out
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0-preview-alpine-composite AS base
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine-composite AS base
|
||||||
WORKDIR /groceries
|
WORKDIR /groceries
|
||||||
|
|
||||||
COPY --from=build2 /out .
|
COPY --from=build2 /out .
|
||||||
@ -31,7 +30,6 @@ COPY --from=build2 /src/Groceries/config.ini /config/
|
|||||||
|
|
||||||
RUN apk add --no-cache icu-libs tzdata
|
RUN apk add --no-cache icu-libs tzdata
|
||||||
|
|
||||||
ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
|
|
||||||
ENV ASPNETCORE_HTTP_PORTS=80
|
ENV ASPNETCORE_HTTP_PORTS=80
|
||||||
ENV DOTNET_ENABLEDIAGNOSTICS=0
|
ENV DOTNET_ENABLEDIAGNOSTICS=0
|
||||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
|
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
|
||||||
|
@ -33,9 +33,6 @@ public class AppDbContext : DbContext
|
|||||||
|
|
||||||
entity.Property(e => e.Format)
|
entity.Property(e => e.Format)
|
||||||
.HasDefaultValueSql();
|
.HasDefaultValueSql();
|
||||||
|
|
||||||
entity.Property(e => e.LastScannedAt)
|
|
||||||
.HasDefaultValueSql();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<ItemPurchase>(entity =>
|
modelBuilder.Entity<ItemPurchase>(entity =>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<WarningsAsErrors>nullable</WarningsAsErrors>
|
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||||
@ -9,10 +9,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DbUp-PostgreSQL" Version="6.0.3" />
|
<PackageReference Include="DbUp-PostgreSQL" Version="5.0.37" />
|
||||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
<PackageReference Include="EFCore.NamingConventions" Version="8.0.2" />
|
||||||
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0-preview.1" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -12,5 +12,4 @@ public class ItemBarcode
|
|||||||
public Guid ItemId { get; init; }
|
public Guid ItemId { get; init; }
|
||||||
public long BarcodeData { get; init; }
|
public long BarcodeData { get; init; }
|
||||||
public string Format { get; init; }
|
public string Format { get; init; }
|
||||||
public DateTime LastScannedAt { get; set; }
|
|
||||||
}
|
}
|
||||||
|
@ -7,8 +7,7 @@ public class ItemPurchase
|
|||||||
public DateTime CreatedAt { get; init; }
|
public DateTime CreatedAt { get; init; }
|
||||||
public Guid StoreId { get; init; }
|
public Guid StoreId { get; init; }
|
||||||
public decimal Price { get; init; }
|
public decimal Price { get; init; }
|
||||||
public decimal Quantity { get; init; }
|
public int Quantity { get; init; }
|
||||||
public string? Unit { get; init; }
|
|
||||||
public bool IsLastPurchase { get; init; }
|
public bool IsLastPurchase { get; init; }
|
||||||
|
|
||||||
public Item? Item { get; init; }
|
public Item? Item { get; init; }
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
ALTER TABLE item_barcodes
|
|
||||||
ADD COLUMN IF NOT EXISTS last_scanned_at timestamptz NOT NULL DEFAULT current_timestamp;
|
|
||||||
|
|
||||||
UPDATE item_barcodes
|
|
||||||
SET last_scanned_at = created_at
|
|
||||||
FROM item_purchases
|
|
||||||
WHERE item_barcodes.item_id = item_purchases.item_id AND is_last_purchase = true;
|
|
@ -1,35 +0,0 @@
|
|||||||
DROP VIEW item_purchases;
|
|
||||||
DROP VIEW transaction_totals;
|
|
||||||
|
|
||||||
ALTER TABLE transaction_items
|
|
||||||
ALTER COLUMN quantity TYPE numeric(5, 3);
|
|
||||||
|
|
||||||
ALTER TABLE transaction_items
|
|
||||||
ADD COLUMN IF NOT EXISTS unit text;
|
|
||||||
|
|
||||||
CREATE VIEW item_purchases AS
|
|
||||||
SELECT
|
|
||||||
item_id,
|
|
||||||
transaction_id,
|
|
||||||
created_at,
|
|
||||||
store_id,
|
|
||||||
price,
|
|
||||||
quantity,
|
|
||||||
unit,
|
|
||||||
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);
|
|
||||||
|
|
||||||
CREATE VIEW transaction_totals AS
|
|
||||||
SELECT transaction_id, sum(amount) AS total
|
|
||||||
FROM (
|
|
||||||
SELECT transaction_id, price * quantity AS amount
|
|
||||||
FROM transaction_items
|
|
||||||
UNION ALL
|
|
||||||
SELECT transaction_id, -amount
|
|
||||||
FROM transaction_promotions
|
|
||||||
) AS transaction_amounts
|
|
||||||
GROUP BY transaction_id;
|
|
@ -5,24 +5,22 @@ using System.Text.Json.Serialization;
|
|||||||
public class TransactionItem
|
public class TransactionItem
|
||||||
{
|
{
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
public TransactionItem(Guid transactionId, Guid itemId, decimal price, decimal quantity, string? unit)
|
public TransactionItem(Guid transactionId, Guid itemId, decimal price, int quantity)
|
||||||
{
|
{
|
||||||
TransactionId = transactionId;
|
TransactionId = transactionId;
|
||||||
ItemId = itemId;
|
ItemId = itemId;
|
||||||
Price = price;
|
Price = price;
|
||||||
Quantity = quantity;
|
Quantity = quantity;
|
||||||
Unit = unit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public TransactionItem(Guid itemId, decimal price, decimal quantity, string? unit) : this(default, itemId, price, quantity, unit)
|
public TransactionItem(Guid itemId, decimal price, int quantity) : this(default, itemId, price, quantity)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid TransactionId { get; init; }
|
public Guid TransactionId { get; init; }
|
||||||
public Guid ItemId { get; set; }
|
public Guid ItemId { get; set; }
|
||||||
public decimal Price { get; set; }
|
public decimal Price { get; set; }
|
||||||
public decimal Quantity { get; set; }
|
public int Quantity { get; set; }
|
||||||
public string? Unit { get; set; }
|
|
||||||
|
|
||||||
public Item? Item { get; set; }
|
public Item? Item { get; set; }
|
||||||
|
|
||||||
|
@ -6,19 +6,11 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="color-scheme" content="light dark" />
|
<meta name="color-scheme" content="light dark" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="view-transition" content="same-origin" />
|
|
||||||
<meta name="turbo-prefetch" content="false" />
|
|
||||||
|
|
||||||
<base href="/" />
|
<link rel="stylesheet" type="text/css" href="/css/main.css" asp-append-version="true" data-turbo-track="reload" />
|
||||||
|
|
||||||
<link rel="preload" as="font" type="font/woff2" href="lib/inter/files/inter-latin-wght-normal.woff2" crossorigin />
|
<script type="module" src="/js/main.js" asp-append-version="true" data-turbo-track="reload"></script>
|
||||||
<link rel="stylesheet" type="text/css" href="@Assets["lib/inter/index.css"]" data-turbo-track="reload" />
|
<script type="module" src="/lib/hotwired/turbo/dist/turbo.es2017-esm.js"></script>
|
||||||
<link rel="stylesheet" type="text/css" href="@Assets["css/main.css"]" data-turbo-track="reload" />
|
|
||||||
|
|
||||||
<ImportMap />
|
|
||||||
|
|
||||||
<script type="module" src="@Assets["js/main.js"]" data-turbo-track="reload"></script>
|
|
||||||
<script type="module" src="@Assets["lib/hotwired/turbo/dist/turbo.es2017-esm.js"]"></script>
|
|
||||||
|
|
||||||
<HeadOutlet />
|
<HeadOutlet />
|
||||||
</head>
|
</head>
|
||||||
|
@ -20,9 +20,6 @@ public class PropertyTableColumn<TItem, TProp> : TableColumn<TItem>
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public string? Format { get; set; }
|
public string? Format { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public Func<TItem, string>? CompositeFormat { get; set; }
|
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public override bool Sortable { get; set; }
|
public override bool Sortable { get; set; }
|
||||||
|
|
||||||
@ -58,11 +55,7 @@ public class PropertyTableColumn<TItem, TProp> : TableColumn<TItem>
|
|||||||
|
|
||||||
if (ChildContent == null)
|
if (ChildContent == null)
|
||||||
{
|
{
|
||||||
if (CompositeFormat != null)
|
if (!string.IsNullOrEmpty(Format) &&
|
||||||
{
|
|
||||||
cellTextFunc = item => string.Format(CompositeFormat(item), compiledPropertyExpression(item));
|
|
||||||
}
|
|
||||||
else if (!string.IsNullOrEmpty(Format) &&
|
|
||||||
typeof(IFormattable).IsAssignableFrom(Nullable.GetUnderlyingType(typeof(TProp)) ?? typeof(TProp)))
|
typeof(IFormattable).IsAssignableFrom(Nullable.GetUnderlyingType(typeof(TProp)) ?? typeof(TProp)))
|
||||||
{
|
{
|
||||||
cellTextFunc = item => ((IFormattable?)compiledPropertyExpression(item))?.ToString(Format, null);
|
cellTextFunc = item => ((IFormattable?)compiledPropertyExpression(item))?.ToString(Format, null);
|
||||||
|
@ -176,12 +176,6 @@
|
|||||||
{
|
{
|
||||||
string?[] classes = [
|
string?[] classes = [
|
||||||
"table__header",
|
"table__header",
|
||||||
column.Align switch
|
|
||||||
{
|
|
||||||
Align.Center => "table__header--align-center",
|
|
||||||
Align.End => "table__header--align-end",
|
|
||||||
_ => null,
|
|
||||||
},
|
|
||||||
column.Sortable ? "table__header--sortable" : null,
|
column.Sortable ? "table__header--sortable" : null,
|
||||||
HeaderClass,
|
HeaderClass,
|
||||||
];
|
];
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<span>
|
<span>
|
||||||
Showing @FirstItem to @LastItem of @State.TotalItemCount results
|
Showing @FirstItem to @LastItem of @State.TotalItemCount results
|
||||||
</span>
|
</span>
|
||||||
<nav>
|
<nav class="button-group">
|
||||||
@if (State.CurrentPage == 1)
|
@if (State.CurrentPage == 1)
|
||||||
{
|
{
|
||||||
<span class="link link--disabled">Previous</span>
|
<span class="link link--disabled">Previous</span>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<WarningsAsErrors>nullable</WarningsAsErrors>
|
<WarningsAsErrors>nullable</WarningsAsErrors>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
@layout Layout
|
@layout Layout
|
||||||
|
|
||||||
@inject IDbContextFactory<AppDbContext> DbContextFactory
|
@inject AppDbContext DbContext
|
||||||
@inject IHttpContextAccessor HttpContextAccessor
|
@inject IHttpContextAccessor HttpContextAccessor
|
||||||
|
|
||||||
<HeadContent>
|
<HeadContent>
|
||||||
@ -52,8 +52,7 @@
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
using var dbContext = DbContextFactory.CreateDbContext();
|
model = await DbContext.ItemTagQuantities
|
||||||
model = await dbContext.ItemTagQuantities
|
|
||||||
.FromSqlRaw(@"
|
.FromSqlRaw(@"
|
||||||
SELECT tag, quantity, coalesce(unit_name, unit) AS unit, is_metric, is_divisible
|
SELECT tag, quantity, coalesce(unit_name, unit) AS unit, is_metric, is_divisible
|
||||||
FROM (
|
FROM (
|
||||||
|
@ -32,10 +32,10 @@ public static class HttpRequestExtensions
|
|||||||
return origin.IsBaseOf(uri);
|
return origin.IsBaseOf(uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Uri? GetReferrerIfSameOrigin(this HttpRequest request)
|
public static Uri? GetRefererIfSameOrigin(this HttpRequest request)
|
||||||
{
|
{
|
||||||
var referrer = request.GetTypedHeaders().Referer;
|
var referer = request.GetTypedHeaders().Referer;
|
||||||
return referrer != null && request.IsSameOrigin(referrer) ? referrer : null;
|
return referer != null && request.IsSameOrigin(referer) ? referer : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsTurboFrameRequest(this HttpRequest request, string frameId)
|
public static bool IsTurboFrameRequest(this HttpRequest request, string frameId)
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
@using Microsoft.EntityFrameworkCore
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
|
||||||
@layout Layout
|
@layout Layout
|
||||||
|
@inject AppDbContext DbContext
|
||||||
@implements IDisposable
|
|
||||||
@inject IDbContextFactory<AppDbContext> DbContextFactory
|
|
||||||
|
|
||||||
<PageTitle>Groceries – Items</PageTitle>
|
<PageTitle>Groceries – Items</PageTitle>
|
||||||
|
|
||||||
@ -46,7 +44,6 @@
|
|||||||
public DateTime? LastPurchasedAt { get; init; }
|
public DateTime? LastPurchasedAt { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private AppDbContext? dbContext;
|
|
||||||
private IQueryable<ItemModel> items = null!;
|
private IQueryable<ItemModel> items = null!;
|
||||||
private PaginationState pagination = new();
|
private PaginationState pagination = new();
|
||||||
|
|
||||||
@ -55,9 +52,7 @@
|
|||||||
|
|
||||||
protected override void OnParametersSet()
|
protected override void OnParametersSet()
|
||||||
{
|
{
|
||||||
dbContext ??= DbContextFactory.CreateDbContext();
|
var itemsQuery = DbContext.Items.AsQueryable();
|
||||||
|
|
||||||
var itemsQuery = dbContext.Items.AsQueryable();
|
|
||||||
if (!string.IsNullOrEmpty(Search))
|
if (!string.IsNullOrEmpty(Search))
|
||||||
{
|
{
|
||||||
var searchPattern = $"%{Search}%";
|
var searchPattern = $"%{Search}%";
|
||||||
@ -65,24 +60,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
items = itemsQuery
|
items = itemsQuery
|
||||||
.LeftJoin(
|
.GroupJoin(
|
||||||
dbContext.ItemPurchases.Where(purchase => purchase.IsLastPurchase),
|
DbContext.ItemPurchases.Where(purchase => purchase.IsLastPurchase),
|
||||||
item => item.Id,
|
item => item.Id,
|
||||||
purchase => purchase.ItemId,
|
purchase => purchase.ItemId,
|
||||||
(item, lastPurchase) => new ItemModel
|
(item, purchases) => new { item, purchases })
|
||||||
|
.SelectMany(
|
||||||
|
group => group.purchases.DefaultIfEmpty(),
|
||||||
|
(group, lastPurchase) => new ItemModel
|
||||||
{
|
{
|
||||||
Id = item.Id,
|
Id = group.item.Id,
|
||||||
Brand = item.Brand,
|
Brand = group.item.Brand,
|
||||||
Name = item.Name,
|
Name = group.item.Name,
|
||||||
HasBarcode = item.Barcodes.Count > 0,
|
HasBarcode = group.item.Barcodes.Count != 0,
|
||||||
LastPurchasedAt = lastPurchase != null ? lastPurchase.CreatedAt : null,
|
LastPurchasedAt = lastPurchase != null ? lastPurchase.CreatedAt : null,
|
||||||
})
|
})
|
||||||
.OrderBy(item => item.Brand)
|
.OrderBy(item => item.Brand)
|
||||||
.ThenBy(item => item.Name);
|
.ThenBy(item => item.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
dbContext?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
using DbUp;
|
using DbUp;
|
||||||
using DbUp.Engine.Output;
|
|
||||||
using Groceries.Data;
|
using Groceries.Data;
|
||||||
using Microsoft.AspNetCore.DataProtection;
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@ -14,6 +14,27 @@ builder.Configuration
|
|||||||
.AddIniFile(Path.Combine(dataDir, $"config_{env.EnvironmentName}.ini"), optional: true, reloadOnChange: true);
|
.AddIniFile(Path.Combine(dataDir, $"config_{env.EnvironmentName}.ini"), optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
var dbConn = builder.Configuration["Database"]!;
|
var dbConn = builder.Configuration["Database"]!;
|
||||||
|
EnsureDatabase.For.PostgresqlDatabase(dbConn);
|
||||||
|
|
||||||
|
var dbUpgradeResult = DeployChanges.To
|
||||||
|
.PostgresqlDatabase(dbConn)
|
||||||
|
.JournalToPostgresqlTable("public", "__dbup_migrations")
|
||||||
|
.WithScriptsEmbeddedInAssembly(typeof(AppDbContext).Assembly)
|
||||||
|
.WithTransactionPerScript()
|
||||||
|
.Build()
|
||||||
|
.PerformUpgrade();
|
||||||
|
|
||||||
|
if (!dbUpgradeResult.Successful)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||||
|
{
|
||||||
|
options.ForwardedHeaders = ForwardedHeaders.All;
|
||||||
|
options.KnownNetworks.Clear();
|
||||||
|
options.KnownProxies.Clear();
|
||||||
|
});
|
||||||
|
|
||||||
var dataProtection = builder.Services.AddDataProtection();
|
var dataProtection = builder.Services.AddDataProtection();
|
||||||
if (env.IsProduction())
|
if (env.IsProduction())
|
||||||
@ -29,7 +50,7 @@ builder.Services.AddDistributedMemoryCache();
|
|||||||
builder.Services.AddHttpContextAccessor();
|
builder.Services.AddHttpContextAccessor();
|
||||||
builder.Services.AddSession();
|
builder.Services.AddSession();
|
||||||
|
|
||||||
builder.Services.AddPooledDbContextFactory<AppDbContext>(options => options
|
builder.Services.AddDbContextPool<AppDbContext>(options => options
|
||||||
.EnableDetailedErrors(env.IsDevelopment())
|
.EnableDetailedErrors(env.IsDevelopment())
|
||||||
.EnableSensitiveDataLogging(env.IsDevelopment())
|
.EnableSensitiveDataLogging(env.IsDevelopment())
|
||||||
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
|
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)
|
||||||
@ -38,29 +59,13 @@ builder.Services.AddPooledDbContextFactory<AppDbContext>(options => options
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseForwardedHeaders();
|
||||||
|
app.UseStaticFiles();
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
app.UseSession();
|
app.UseSession();
|
||||||
|
|
||||||
app.MapStaticAssets();
|
app.MapControllers();
|
||||||
app.MapControllers()
|
|
||||||
.WithStaticAssets();
|
|
||||||
|
|
||||||
var dbUpgradeLogger = new MicrosoftUpgradeLog(app.Logger);
|
await app.RunAsync();
|
||||||
EnsureDatabase.For.PostgresqlDatabase(dbConn, dbUpgradeLogger);
|
|
||||||
|
|
||||||
var dbUpgradeResult = DeployChanges.To
|
return 0;
|
||||||
.PostgresqlDatabase(dbConn)
|
|
||||||
.JournalToPostgresqlTable("public", "__dbup_migrations")
|
|
||||||
.WithScriptsEmbeddedInAssembly(typeof(AppDbContext).Assembly)
|
|
||||||
.WithTransactionPerScript()
|
|
||||||
.LogTo(dbUpgradeLogger)
|
|
||||||
.Build()
|
|
||||||
.PerformUpgrade();
|
|
||||||
|
|
||||||
if (!dbUpgradeResult.Successful)
|
|
||||||
{
|
|
||||||
Environment.Exit(-1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
app.Run();
|
|
||||||
|
@ -24,9 +24,9 @@
|
|||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
var request = HttpContextAccessor.HttpContext!.Request;
|
var request = HttpContextAccessor.HttpContext!.Request;
|
||||||
if (request.GetReferrerIfSameOrigin() is Uri referrer && referrer != request.GetUri())
|
if (request.GetRefererIfSameOrigin() is Uri referer && referer != request.GetUri())
|
||||||
{
|
{
|
||||||
returnUrl = referrer.PathAndQuery;
|
returnUrl = referer.PathAndQuery;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,9 +19,9 @@
|
|||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
var request = HttpContextAccessor.HttpContext!.Request;
|
var request = HttpContextAccessor.HttpContext!.Request;
|
||||||
if (request.GetReferrerIfSameOrigin() is Uri referrer && referrer != request.GetUri())
|
if (request.GetRefererIfSameOrigin() is Uri referer && referer != request.GetUri())
|
||||||
{
|
{
|
||||||
returnUrl = referrer.PathAndQuery;
|
returnUrl = referer.PathAndQuery;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
@using Groceries.Data
|
@using Groceries.Data
|
||||||
@using Microsoft.EntityFrameworkCore
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
|
||||||
@inject IDbContextFactory<AppDbContext> DbContextFactory
|
@inject AppDbContext DbContext
|
||||||
|
|
||||||
<form method="post" @attributes="AdditionalAttributes">
|
<form method="post" @attributes="AdditionalAttributes">
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
@ -45,8 +45,7 @@
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
using var dbContext = DbContextFactory.CreateDbContext();
|
retailers = await DbContext.Retailers
|
||||||
retailers = await dbContext.Retailers
|
|
||||||
.OrderBy(retailer => retailer.Name)
|
.OrderBy(retailer => retailer.Name)
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
}
|
}
|
||||||
|
@ -8,11 +8,11 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
[Route("/stores")]
|
[Route("/stores")]
|
||||||
public class StoresController : Controller
|
public class StoresController : Controller
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<AppDbContext> dbContextFactory;
|
private readonly AppDbContext dbContext;
|
||||||
|
|
||||||
public StoresController(IDbContextFactory<AppDbContext> dbContextFactory)
|
public StoresController(AppDbContext dbContext)
|
||||||
{
|
{
|
||||||
this.dbContextFactory = dbContextFactory;
|
this.dbContext = dbContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@ -32,8 +32,6 @@ public class StoresController : Controller
|
|||||||
[HttpPost("new")]
|
[HttpPost("new")]
|
||||||
public async Task<IResult> NewStore(Guid retailerId, string name, string? address)
|
public async Task<IResult> NewStore(Guid retailerId, string name, string? address)
|
||||||
{
|
{
|
||||||
using var dbContext = dbContextFactory.CreateDbContext();
|
|
||||||
|
|
||||||
var store = new Store(retailerId, name, address);
|
var store = new Store(retailerId, name, address);
|
||||||
dbContext.Stores.Add(store);
|
dbContext.Stores.Add(store);
|
||||||
|
|
||||||
@ -47,8 +45,6 @@ public class StoresController : Controller
|
|||||||
[HttpGet("edit/{id}")]
|
[HttpGet("edit/{id}")]
|
||||||
public async Task<IResult> EditStore(Guid id)
|
public async Task<IResult> EditStore(Guid id)
|
||||||
{
|
{
|
||||||
using var dbContext = dbContextFactory.CreateDbContext();
|
|
||||||
|
|
||||||
var store = await dbContext.Stores
|
var store = await dbContext.Stores
|
||||||
.SingleOrDefaultAsync(store => store.Id == id, HttpContext.RequestAborted);
|
.SingleOrDefaultAsync(store => store.Id == id, HttpContext.RequestAborted);
|
||||||
|
|
||||||
@ -65,8 +61,6 @@ public class StoresController : Controller
|
|||||||
[HttpPost("edit/{id}")]
|
[HttpPost("edit/{id}")]
|
||||||
public async Task<IResult> EditStore(Guid id, Guid retailerId, string name, string? address, string? returnUrl)
|
public async Task<IResult> EditStore(Guid id, Guid retailerId, string name, string? address, string? returnUrl)
|
||||||
{
|
{
|
||||||
using var dbContext = dbContextFactory.CreateDbContext();
|
|
||||||
|
|
||||||
var store = new Store(id, retailerId, name, address);
|
var store = new Store(id, retailerId, name, address);
|
||||||
dbContext.Stores.Update(store);
|
dbContext.Stores.Update(store);
|
||||||
|
|
||||||
|
@ -2,9 +2,7 @@
|
|||||||
@using Microsoft.EntityFrameworkCore
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
|
||||||
@layout Layout
|
@layout Layout
|
||||||
|
@inject AppDbContext DbContext
|
||||||
@implements IDisposable
|
|
||||||
@inject IDbContextFactory<AppDbContext> DbContextFactory
|
|
||||||
|
|
||||||
<PageTitle>Groceries – Stores</PageTitle>
|
<PageTitle>Groceries – Stores</PageTitle>
|
||||||
|
|
||||||
@ -41,7 +39,6 @@
|
|||||||
public int TransactionsCount { get; init; }
|
public int TransactionsCount { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private AppDbContext? dbContext;
|
|
||||||
private IQueryable<StoreModel> stores = null!;
|
private IQueryable<StoreModel> stores = null!;
|
||||||
private PaginationState pagination = new();
|
private PaginationState pagination = new();
|
||||||
|
|
||||||
@ -50,9 +47,7 @@
|
|||||||
|
|
||||||
protected override void OnParametersSet()
|
protected override void OnParametersSet()
|
||||||
{
|
{
|
||||||
dbContext ??= DbContextFactory.CreateDbContext();
|
var storesQuery = DbContext.Stores.AsQueryable();
|
||||||
|
|
||||||
var storesQuery = dbContext.Stores.AsQueryable();
|
|
||||||
if (!string.IsNullOrEmpty(Search))
|
if (!string.IsNullOrEmpty(Search))
|
||||||
{
|
{
|
||||||
var searchPattern = $"%{Search}%";
|
var searchPattern = $"%{Search}%";
|
||||||
@ -71,9 +66,4 @@
|
|||||||
.OrderBy(store => store.Retailer)
|
.OrderBy(store => store.Retailer)
|
||||||
.ThenBy(store => store.Name);
|
.ThenBy(store => store.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
dbContext?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
@layout Layout
|
@layout Layout
|
||||||
|
|
||||||
@inject IDbContextFactory<AppDbContext> DbContextFactory
|
@inject AppDbContext DbContext
|
||||||
|
|
||||||
<PageTitle>Groceries – Edit Transaction Item</PageTitle>
|
<PageTitle>Groceries – Edit Transaction Item</PageTitle>
|
||||||
|
|
||||||
@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button class="button button--primary" type="submit" form="editTransactionItem">Update</button>
|
<button class="button button--primary" type="submit" form="editTransactionItem">Update</button>
|
||||||
<a class="button" href="/transactions/new/items">Cancel</a>
|
<a class="button" href="/transaction/new/items">Cancel</a>
|
||||||
<span class="row__fill"></span>
|
<span class="row__fill"></span>
|
||||||
<button class="button button--danger" type="submit" form="deleteTransactionItem">Remove</button>
|
<button class="button button--danger" type="submit" form="deleteTransactionItem">Remove</button>
|
||||||
</div>
|
</div>
|
||||||
@ -35,8 +35,7 @@
|
|||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
using var dbContext = DbContextFactory.CreateDbContext();
|
store = await DbContext.Stores
|
||||||
store = await dbContext.Stores
|
|
||||||
.Where(store => store.Id == Transaction.StoreId)
|
.Where(store => store.Id == Transaction.StoreId)
|
||||||
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
|
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
|
||||||
.SingleAsync();
|
.SingleAsync();
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
@layout Layout
|
@layout Layout
|
||||||
|
|
||||||
@inject IDbContextFactory<AppDbContext> DbContextFactory
|
@inject AppDbContext DbContext
|
||||||
|
|
||||||
<PageTitle>Groceries – Edit Transaction Promotion</PageTitle>
|
<PageTitle>Groceries – Edit Transaction Promotion</PageTitle>
|
||||||
|
|
||||||
@ -35,8 +35,7 @@
|
|||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
using var dbContext = DbContextFactory.CreateDbContext();
|
store = await DbContext.Stores
|
||||||
store = await dbContext.Stores
|
|
||||||
.Where(store => store.Id == Transaction.StoreId)
|
.Where(store => store.Id == Transaction.StoreId)
|
||||||
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
|
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
|
||||||
.SingleAsync();
|
.SingleAsync();
|
||||||
|
@ -16,5 +16,5 @@
|
|||||||
public required Transaction Transaction { get; set; }
|
public required Transaction Transaction { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public required TransactionItem TransactionItem { get; set; }
|
public TransactionItem? TransactionItem { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
@layout Layout
|
@layout Layout
|
||||||
|
|
||||||
@inject IDbContextFactory<AppDbContext> DbContextFactory
|
@inject AppDbContext DbContext
|
||||||
|
|
||||||
<PageTitle>Groceries – New Transaction Item</PageTitle>
|
<PageTitle>Groceries – New Transaction Item</PageTitle>
|
||||||
|
|
||||||
@ -16,7 +16,7 @@
|
|||||||
<TransactionItemForm TransactionItem="TransactionItem">
|
<TransactionItemForm TransactionItem="TransactionItem">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button class="button button--primary" type="submit">Add</button>
|
<button class="button button--primary" type="submit">Add</button>
|
||||||
<a class="button" href="/transactions/new/items">Cancel</a>
|
<a class="button" href="/transaction/new/items">Cancel</a>
|
||||||
</div>
|
</div>
|
||||||
</TransactionItemForm>
|
</TransactionItemForm>
|
||||||
|
|
||||||
@ -25,14 +25,13 @@
|
|||||||
public required Transaction Transaction { get; set; }
|
public required Transaction Transaction { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public required TransactionItem TransactionItem { get; set; }
|
public TransactionItem? TransactionItem { get; set; }
|
||||||
|
|
||||||
private string store = string.Empty;
|
private string store = string.Empty;
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
using var dbContext = DbContextFactory.CreateDbContext();
|
store = await DbContext.Stores
|
||||||
store = await dbContext.Stores
|
|
||||||
.Where(store => store.Id == Transaction.StoreId)
|
.Where(store => store.Id == Transaction.StoreId)
|
||||||
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
|
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
|
||||||
.SingleAsync();
|
.SingleAsync();
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
@using Microsoft.EntityFrameworkCore
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
|
||||||
@layout Layout
|
@layout Layout
|
||||||
|
@inject AppDbContext DbContext
|
||||||
@inject IDbContextFactory<AppDbContext> DbContextFactory
|
|
||||||
|
|
||||||
<PageTitle>Groceries – New Transaction</PageTitle>
|
<PageTitle>Groceries – New Transaction</PageTitle>
|
||||||
|
|
||||||
@ -17,29 +16,19 @@
|
|||||||
<section class="card form-field">
|
<section class="card form-field">
|
||||||
<div class="card__header row">
|
<div class="card__header row">
|
||||||
<h2 class="row__fill">Items</h2>
|
<h2 class="row__fill">Items</h2>
|
||||||
<div class="button-group dropdown">
|
|
||||||
<a class="button button--primary" href="/transactions/new/items/new" autofocus data-turbo-frame="modal">
|
<a class="button button--primary" href="/transactions/new/items/new" autofocus data-turbo-frame="modal">
|
||||||
New item
|
New item
|
||||||
</a>
|
</a>
|
||||||
<button class="button button--primary dropdown__toggle" type="button" popovertarget="newItemMenu"></button>
|
|
||||||
<ul class="dropdown__menu" id="newItemMenu" popover>
|
|
||||||
<li>
|
|
||||||
<a class="button dropdown__item" href="/transactions/new/items/new?unit=kg" data-turbo-frame="modal">
|
|
||||||
New loose item
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card__content card__content--table">
|
<div class="card__content card__content--table">
|
||||||
<Table Items="Transaction.Items.AsQueryable()" HeaderClass="table__header--compact" CellClass="table__cell--compact">
|
<Table Items="Transaction.Items.AsQueryable()" CellClass="table__cell--compact">
|
||||||
<ChildContent>
|
<ChildContent>
|
||||||
<TemplateTableColumn Title="Name" Fill="true" Context="item">
|
<TemplateTableColumn Title="Name" Fill="true" Context="item">
|
||||||
<div class="line-clamp-4">@itemNames.GetValueOrDefault(item.ItemId)</div>
|
@itemNames.GetValueOrDefault(item.ItemId)
|
||||||
</TemplateTableColumn>
|
</TemplateTableColumn>
|
||||||
<PropertyTableColumn Property="i => i.Price" CompositeFormat='i => i.Unit == null ? "{0:c}" : ("{0:c}/" + i.Unit)' />
|
<PropertyTableColumn Property="i => i.Price" Format="c" />
|
||||||
<PropertyTableColumn Property="i => i.Quantity" CompositeFormat='i => i.Unit == null ? "{0:f0}" : ("{0:f3}" + i.Unit)'>
|
<PropertyTableColumn Property="i => i.Quantity">
|
||||||
<HeaderContent>
|
<HeaderContent>
|
||||||
<abbr title="Quantity">Qty</abbr>
|
<abbr title="Quantity">Qty</abbr>
|
||||||
</HeaderContent>
|
</HeaderContent>
|
||||||
@ -79,15 +68,13 @@
|
|||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
using var dbContext = DbContextFactory.CreateDbContext();
|
store = await DbContext.Stores
|
||||||
|
|
||||||
store = await dbContext.Stores
|
|
||||||
.Where(store => store.Id == Transaction.StoreId)
|
.Where(store => store.Id == Transaction.StoreId)
|
||||||
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
|
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
|
||||||
.SingleAsync();
|
.SingleAsync();
|
||||||
|
|
||||||
var itemIds = Transaction.Items.Select(item => item.ItemId);
|
var itemIds = Transaction.Items.Select(item => item.ItemId);
|
||||||
itemNames = await dbContext.Items
|
itemNames = await DbContext.Items
|
||||||
.Where(item => itemIds.Contains(item.Id))
|
.Where(item => itemIds.Contains(item.Id))
|
||||||
.ToDictionaryAsync(item => item.Id, item => string.Concat(item.Brand, " ", item.Name));
|
.ToDictionaryAsync(item => item.Id, item => string.Concat(item.Brand, " ", item.Name));
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
@layout Layout
|
@layout Layout
|
||||||
|
|
||||||
@inject IDbContextFactory<AppDbContext> DbContextFactory
|
@inject AppDbContext DbContext
|
||||||
|
|
||||||
<PageTitle>Groceries – New Transaction</PageTitle>
|
<PageTitle>Groceries – New Transaction</PageTitle>
|
||||||
|
|
||||||
@ -41,8 +41,7 @@
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
using var dbContext = DbContextFactory.CreateDbContext();
|
stores = await DbContext.Stores
|
||||||
stores = await dbContext.Stores
|
|
||||||
.OrderBy(store => store.Retailer!.Name)
|
.OrderBy(store => store.Retailer!.Name)
|
||||||
.ThenBy(store => store.Name)
|
.ThenBy(store => store.Name)
|
||||||
.Select(store => new StoreModel(store.Id, string.Concat(store.Retailer!.Name, " ", store.Name)))
|
.Select(store => new StoreModel(store.Id, string.Concat(store.Retailer!.Name, " ", store.Name)))
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
@layout Layout
|
@layout Layout
|
||||||
|
|
||||||
@inject IDbContextFactory<AppDbContext> DbContextFactory
|
@inject AppDbContext DbContext
|
||||||
|
|
||||||
<PageTitle>Groceries – New Transaction Promotion</PageTitle>
|
<PageTitle>Groceries – New Transaction Promotion</PageTitle>
|
||||||
|
|
||||||
@ -28,8 +28,7 @@
|
|||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
using var dbContext = DbContextFactory.CreateDbContext();
|
store = await DbContext.Stores
|
||||||
store = await dbContext.Stores
|
|
||||||
.Where(store => store.Id == Transaction.StoreId)
|
.Where(store => store.Id == Transaction.StoreId)
|
||||||
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
|
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
|
||||||
.SingleAsync();
|
.SingleAsync();
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
@using Microsoft.EntityFrameworkCore
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
|
||||||
@layout Layout
|
@layout Layout
|
||||||
|
@inject AppDbContext DbContext
|
||||||
@inject IDbContextFactory<AppDbContext> DbContextFactory
|
|
||||||
|
|
||||||
<PageTitle>Groceries – New Transaction</PageTitle>
|
<PageTitle>Groceries – New Transaction</PageTitle>
|
||||||
|
|
||||||
@ -23,7 +22,7 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="card__content card__content--table">
|
<div class="card__content card__content--table">
|
||||||
<Table Items="Transaction.Promotions.AsQueryable()" HeaderClass="table__header--compact" CellClass="table__cell--compact">
|
<Table Items="Transaction.Promotions.AsQueryable()" CellClass="table__cell--compact">
|
||||||
<ChildContent>
|
<ChildContent>
|
||||||
<PropertyTableColumn Property="p => p.Name" Fill="true" />
|
<PropertyTableColumn Property="p => p.Name" Fill="true" />
|
||||||
<TemplateTableColumn Title="Items" Align="Align.End" Context="promotion">
|
<TemplateTableColumn Title="Items" Align="Align.End" Context="promotion">
|
||||||
@ -65,8 +64,7 @@
|
|||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
using var dbContext = DbContextFactory.CreateDbContext();
|
store = await DbContext.Stores
|
||||||
store = await dbContext.Stores
|
|
||||||
.Where(store => store.Id == Transaction.StoreId)
|
.Where(store => store.Id == Transaction.StoreId)
|
||||||
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
|
.Select(store => string.Concat(store.Retailer!.Name, " ", store.Name))
|
||||||
.SingleAsync();
|
.SingleAsync();
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
@using Groceries.Data
|
@using Groceries.Data
|
||||||
@using Microsoft.EntityFrameworkCore
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
|
||||||
@inject IDbContextFactory<AppDbContext> DbContextFactory
|
@inject AppDbContext DbContext
|
||||||
|
|
||||||
<form method="post" @attributes="AdditionalAttributes">
|
<form method="post" @attributes="AdditionalAttributes">
|
||||||
@* Ensure form action/method are used for implicit submission instead of barcode button *@
|
@* Ensure form action/method are used for implicit submission instead of barcode button *@
|
||||||
@ -36,41 +36,34 @@
|
|||||||
<datalist id="itemNames">
|
<datalist id="itemNames">
|
||||||
@foreach (var item in items)
|
@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="@(unit == null ? (int?)item.Quantity : item.Quantity)" />
|
<option value="@item.Name" data-transaction-item-form-target="option" data-brand="@item.Brand" data-price="@item.Price" data-quantity="@item.Quantity" />
|
||||||
}
|
}
|
||||||
</datalist>
|
</datalist>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="form-field__label" for="transactionItemPrice">
|
<label class="form-field__label" for="transactionItemPrice">Price</label>
|
||||||
Price @if (unit != null) { <text>(per @unit)</text> }
|
|
||||||
</label>
|
|
||||||
<div class="form-field__control input">
|
<div class="form-field__control input">
|
||||||
@*<span class="input__inset">@CultureInfo.CurrentCulture.NumberFormat.CurrencySymbol</span>*@
|
@*<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 />
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="form-field__label" for="transactionItemQuantity">
|
<label class="form-field__label" for="transactionItemQuantity">Quantity</label>
|
||||||
Quantity @if (unit != null) { <text>(@unit)</text> }
|
|
||||||
</label>
|
|
||||||
<div class="form-field__control input">
|
<div class="form-field__control input">
|
||||||
@{ var step = unit == null ? "1" : "0.001"; }
|
<input class="input__control" id="transactionItemQuantity" name="quantity" value="@quantity" type="number" min="1" required data-transaction-item-form-target="quantity" />
|
||||||
<input class="input__control" id="transactionItemQuantity" name="quantity" value="@(unit == null ? (int?)quantity : quantity)" type="number" min="@step" step="@step" required />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="hidden" name="unit" value="@unit" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ChildContent
|
@ChildContent
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter, EditorRequired]
|
[Parameter]
|
||||||
public required TransactionItem TransactionItem { get; set; }
|
public TransactionItem? TransactionItem { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public RenderFragment? ChildContent { get; set; }
|
public RenderFragment? ChildContent { get; set; }
|
||||||
@ -78,7 +71,7 @@
|
|||||||
[Parameter(CaptureUnmatchedValues = true)]
|
[Parameter(CaptureUnmatchedValues = true)]
|
||||||
public Dictionary<string, object>? AdditionalAttributes { get; set; }
|
public Dictionary<string, object>? AdditionalAttributes { get; set; }
|
||||||
|
|
||||||
private record ItemModel(Guid Id, string Brand, string Name, decimal? Price, decimal? Quantity);
|
private record ItemModel(Guid Id, string Brand, string Name, decimal? Price, int? Quantity);
|
||||||
|
|
||||||
private ItemBarcode? barcode;
|
private ItemBarcode? barcode;
|
||||||
|
|
||||||
@ -86,38 +79,33 @@
|
|||||||
private ItemModel? selectedItem;
|
private ItemModel? selectedItem;
|
||||||
|
|
||||||
private decimal? price;
|
private decimal? price;
|
||||||
private decimal? quantity;
|
private int quantity;
|
||||||
private string? unit;
|
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
barcode = TransactionItem.Item?.Barcodes.FirstOrDefault();
|
barcode = TransactionItem?.Item?.Barcodes.FirstOrDefault();
|
||||||
|
|
||||||
using var dbContext = DbContextFactory.CreateDbContext();
|
items = await DbContext.Items
|
||||||
items = await dbContext.Items
|
|
||||||
.OrderBy(item => item.Brand)
|
.OrderBy(item => item.Brand)
|
||||||
.ThenBy(item => item.Name)
|
.ThenBy(item => item.Name)
|
||||||
.LeftJoin(
|
.GroupJoin(
|
||||||
dbContext.ItemPurchases.Where(purchase => purchase.IsLastPurchase),
|
DbContext.ItemPurchases.Where(purchase => purchase.IsLastPurchase),
|
||||||
item => item.Id,
|
item => item.Id,
|
||||||
lastPurchase => lastPurchase.ItemId,
|
lastPurchase => lastPurchase.ItemId,
|
||||||
(item, lastPurchase) => new ItemModel(
|
(item, purchases) => new { item, purchases })
|
||||||
item.Id,
|
.SelectMany(
|
||||||
item.Brand,
|
group => group.purchases.DefaultIfEmpty(),
|
||||||
item.Name,
|
(group, lastPurchase) => new ItemModel(
|
||||||
|
group.item.Id,
|
||||||
|
group.item.Brand,
|
||||||
|
group.item.Name,
|
||||||
lastPurchase != null ? lastPurchase.Price : null,
|
lastPurchase != null ? lastPurchase.Price : null,
|
||||||
lastPurchase != null ? lastPurchase.Quantity : null))
|
lastPurchase != null ? lastPurchase.Quantity : null))
|
||||||
.ToArrayAsync();
|
.ToArrayAsync();
|
||||||
|
|
||||||
selectedItem = items.SingleOrDefault(item => item.Id == TransactionItem.ItemId);
|
selectedItem = items.SingleOrDefault(item => item.Id == TransactionItem?.ItemId);
|
||||||
|
|
||||||
price = TransactionItem.Price >= 0 ? TransactionItem.Price : selectedItem?.Price;
|
price = TransactionItem?.Price >= 0 ? TransactionItem.Price : selectedItem?.Price;
|
||||||
quantity = TransactionItem.Quantity >= 0 ? TransactionItem.Quantity : selectedItem?.Quantity;
|
quantity = TransactionItem?.Quantity >= 1 ? TransactionItem.Quantity : (selectedItem?.Quantity ?? 1);
|
||||||
unit = TransactionItem.Unit;
|
|
||||||
|
|
||||||
if (unit == null)
|
|
||||||
{
|
|
||||||
quantity ??= 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
@using Groceries.Data
|
@using Groceries.Data
|
||||||
@using Microsoft.EntityFrameworkCore
|
@using Microsoft.EntityFrameworkCore
|
||||||
|
|
||||||
@inject IDbContextFactory<AppDbContext> DbContextFactory
|
@inject AppDbContext DbContext
|
||||||
|
|
||||||
<form method="post" @attributes="AdditionalAttributes">
|
<form method="post" @attributes="AdditionalAttributes">
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
@ -56,9 +56,8 @@
|
|||||||
{
|
{
|
||||||
selectedItemIds = Promotion?.Items.Select(item => item.Id).ToArray() ?? [];
|
selectedItemIds = Promotion?.Items.Select(item => item.Id).ToArray() ?? [];
|
||||||
|
|
||||||
using var dbContext = DbContextFactory.CreateDbContext();
|
|
||||||
var itemIds = Transaction.Items.Select(item => item.ItemId);
|
var itemIds = Transaction.Items.Select(item => item.ItemId);
|
||||||
itemNames = await dbContext.Items
|
itemNames = await DbContext.Items
|
||||||
.Where(item => itemIds.Contains(item.Id))
|
.Where(item => itemIds.Contains(item.Id))
|
||||||
.ToDictionaryAsync(item => item.Id, item => string.Concat(item.Brand, " ", item.Name));
|
.ToDictionaryAsync(item => item.Id, item => string.Concat(item.Brand, " ", item.Name));
|
||||||
}
|
}
|
||||||
|
@ -9,11 +9,11 @@ using System.Text.Json;
|
|||||||
[Route("/transactions")]
|
[Route("/transactions")]
|
||||||
public class TransactionsController : Controller
|
public class TransactionsController : Controller
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<AppDbContext> dbContextFactory;
|
private readonly AppDbContext dbContext;
|
||||||
|
|
||||||
public TransactionsController(IDbContextFactory<AppDbContext> dbContextFactory)
|
public TransactionsController(AppDbContext dbContext)
|
||||||
{
|
{
|
||||||
this.dbContextFactory = dbContextFactory;
|
this.dbContext = dbContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@ -60,38 +60,27 @@ public class TransactionsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("new/items/new")]
|
[HttpGet("new/items/new")]
|
||||||
public async Task<IResult> NewTransactionItem(string? unit, 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)
|
if (TempData.Peek("NewTransaction") is not string json || JsonSerializer.Deserialize<Transaction>(json) is not Transaction transaction)
|
||||||
{
|
{
|
||||||
return Results.LocalRedirect("/transactions/new");
|
return Results.LocalRedirect("/transactions/new");
|
||||||
}
|
}
|
||||||
|
|
||||||
Item? item = null;
|
TransactionItem? transactionItem = null;
|
||||||
if (barcodeData != null && barcodeFormat != null)
|
if (barcodeData != null && barcodeFormat != null)
|
||||||
{
|
{
|
||||||
using var dbContext = dbContextFactory.CreateDbContext();
|
var item = await dbContext.Items
|
||||||
|
|
||||||
item = await dbContext.Items
|
|
||||||
.Where(item => item.Barcodes.Any(barcode => barcode.BarcodeData == barcodeData))
|
.Where(item => item.Barcodes.Any(barcode => barcode.BarcodeData == barcodeData))
|
||||||
.OrderByDescending(item => item.UpdatedAt)
|
.OrderByDescending(item => item.UpdatedAt)
|
||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
item ??= new Item(id: default);
|
item ??= new Item(id: default);
|
||||||
|
item.Barcodes.Add(new ItemBarcode(item.Id, barcodeData.Value, barcodeFormat));
|
||||||
var barcode = new ItemBarcode(item.Id, barcodeData.Value, barcodeFormat);
|
|
||||||
item.Barcodes.Add(barcode);
|
|
||||||
|
|
||||||
if (item.Id != default)
|
|
||||||
{
|
|
||||||
barcode.LastScannedAt = DateTime.UtcNow;
|
|
||||||
dbContext.Update(barcode);
|
|
||||||
await dbContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Fix `MinValue` hack - view models?
|
// TODO: Fix `MinValue` hack - view models?
|
||||||
var transactionItem = new TransactionItem(item?.Id ?? default, decimal.MinValue, decimal.MinValue, unit) { Item = item };
|
transactionItem = new TransactionItem(item.Id, decimal.MinValue, int.MinValue) { Item = item };
|
||||||
|
}
|
||||||
|
|
||||||
var parameters = new { Transaction = transaction, TransactionItem = transactionItem };
|
var parameters = new { Transaction = transaction, TransactionItem = transactionItem };
|
||||||
return Request.IsTurboFrameRequest("modal")
|
return Request.IsTurboFrameRequest("modal")
|
||||||
@ -100,15 +89,13 @@ public class TransactionsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("new/items/new")]
|
[HttpPost("new/items/new")]
|
||||||
public async Task<IResult> NewTransactionItem(string brand, string name, decimal price, decimal quantity, string? unit, 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)
|
if (TempData.Peek("NewTransaction") is not string json || JsonSerializer.Deserialize<Transaction>(json) is not Transaction transaction)
|
||||||
{
|
{
|
||||||
return Results.LocalRedirect("/transactions/new");
|
return Results.LocalRedirect("/transactions/new");
|
||||||
}
|
}
|
||||||
|
|
||||||
using var dbContext = dbContextFactory.CreateDbContext();
|
|
||||||
|
|
||||||
var itemId = await dbContext.Items
|
var itemId = await dbContext.Items
|
||||||
.Where(item => EF.Functions.ILike(item.Brand, brand) && EF.Functions.ILike(item.Name, name))
|
.Where(item => EF.Functions.ILike(item.Brand, brand) && EF.Functions.ILike(item.Name, name))
|
||||||
.Select(item => item.Id)
|
.Select(item => item.Id)
|
||||||
@ -131,7 +118,7 @@ public class TransactionsController : Controller
|
|||||||
|
|
||||||
// TODO: Handle item already in transaction - merge, replace, error?
|
// TODO: Handle item already in transaction - merge, replace, error?
|
||||||
|
|
||||||
var transactionItem = new TransactionItem(item.Id, price, quantity, unit) { Item = item };
|
var transactionItem = new TransactionItem(item.Id, price, quantity) { Item = item };
|
||||||
transaction.Items.Add(transactionItem);
|
transaction.Items.Add(transactionItem);
|
||||||
|
|
||||||
TempData["NewTransaction"] = JsonSerializer.Serialize(transaction);
|
TempData["NewTransaction"] = JsonSerializer.Serialize(transaction);
|
||||||
@ -175,8 +162,6 @@ public class TransactionsController : Controller
|
|||||||
return Results.LocalRedirect("/transactions/new/items");
|
return Results.LocalRedirect("/transactions/new/items");
|
||||||
}
|
}
|
||||||
|
|
||||||
using var dbContext = dbContextFactory.CreateDbContext();
|
|
||||||
|
|
||||||
var itemId = await dbContext.Items
|
var itemId = await dbContext.Items
|
||||||
.Where(item => EF.Functions.ILike(item.Brand, brand) && EF.Functions.ILike(item.Name, name))
|
.Where(item => EF.Functions.ILike(item.Brand, brand) && EF.Functions.ILike(item.Name, name))
|
||||||
.Select(item => item.Id)
|
.Select(item => item.Id)
|
||||||
@ -241,15 +226,11 @@ public class TransactionsController : Controller
|
|||||||
return Results.LocalRedirect("/transactions/new");
|
return Results.LocalRedirect("/transactions/new");
|
||||||
}
|
}
|
||||||
|
|
||||||
using var dbContext = dbContextFactory.CreateDbContext();
|
|
||||||
|
|
||||||
foreach (var item in transaction.Items)
|
|
||||||
{
|
|
||||||
item.Item = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Work around EF trying to insert items by explicitly tracking them as unchanged
|
// Work around EF trying to insert items by explicitly tracking them as unchanged
|
||||||
dbContext.Items.AttachRange(transaction.Promotions.SelectMany(promotion => promotion.Items));
|
dbContext.Items.AttachRange(
|
||||||
|
transaction.Items
|
||||||
|
.Select(item => item.Item!)
|
||||||
|
.Concat(transaction.Promotions.SelectMany(promotion => promotion.Items)));
|
||||||
|
|
||||||
dbContext.Transactions.Add(transaction);
|
dbContext.Transactions.Add(transaction);
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
@using Groceries.Data
|
@using Groceries.Data
|
||||||
@using Microsoft.EntityFrameworkCore
|
|
||||||
|
|
||||||
@layout Layout
|
@layout Layout
|
||||||
|
@inject AppDbContext DbContext
|
||||||
@implements IDisposable
|
|
||||||
@inject IDbContextFactory<AppDbContext> DbContextFactory
|
|
||||||
|
|
||||||
<PageTitle>Groceries – Transactions</PageTitle>
|
<PageTitle>Groceries – Transactions</PageTitle>
|
||||||
|
|
||||||
@ -38,16 +35,14 @@
|
|||||||
public int TotalItems { get; init; }
|
public int TotalItems { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private AppDbContext? dbContext;
|
|
||||||
private IQueryable<TransactionModel> transactions = null!;
|
private IQueryable<TransactionModel> transactions = null!;
|
||||||
private PaginationState pagination = new();
|
private PaginationState pagination = new();
|
||||||
|
|
||||||
protected override void OnParametersSet()
|
protected override void OnParametersSet()
|
||||||
{
|
{
|
||||||
dbContext ??= DbContextFactory.CreateDbContext();
|
transactions = DbContext.Transactions
|
||||||
transactions = dbContext.Transactions
|
|
||||||
.Join(
|
.Join(
|
||||||
dbContext.TransactionTotals,
|
DbContext.TransactionTotals,
|
||||||
transaction => transaction.Id,
|
transaction => transaction.Id,
|
||||||
transactionTotal => transactionTotal.TransactionId,
|
transactionTotal => transactionTotal.TransactionId,
|
||||||
(transaction, transactionTotal) => new TransactionModel
|
(transaction, transactionTotal) => new TransactionModel
|
||||||
@ -56,13 +51,8 @@
|
|||||||
CreatedAt = transaction.CreatedAt,
|
CreatedAt = transaction.CreatedAt,
|
||||||
Store = string.Concat(transaction.Store!.Retailer!.Name, " ", transaction.Store.Name),
|
Store = string.Concat(transaction.Store!.Retailer!.Name, " ", transaction.Store.Name),
|
||||||
TotalAmount = transactionTotal.Total,
|
TotalAmount = transactionTotal.Total,
|
||||||
TotalItems = transaction.Items.Sum(item => item.Unit == null ? (int)item.Quantity : 1),
|
TotalItems = transaction.Items.Sum(item => item.Quantity),
|
||||||
})
|
})
|
||||||
.OrderByDescending(transaction => transaction.CreatedAt);
|
.OrderByDescending(transaction => transaction.CreatedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
dbContext?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,11 +3,7 @@
|
|||||||
"defaultProvider": "unpkg",
|
"defaultProvider": "unpkg",
|
||||||
"libraries": [
|
"libraries": [
|
||||||
{
|
{
|
||||||
"library": "@fontsource-variable/inter@5.2.5",
|
"library": "@hotwired/turbo@7.3.0",
|
||||||
"destination": "wwwroot/lib/inter/"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"library": "@hotwired/turbo@8.0.13",
|
|
||||||
"destination": "wwwroot/lib/hotwired/turbo/"
|
"destination": "wwwroot/lib/hotwired/turbo/"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,8 +1,16 @@
|
|||||||
|
@import url("https://rsms.me/inter/inter.css");
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: "Inter Variable", sans-serif;
|
font-family: "Inter", sans-serif;
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@supports (font-variation-settings: normal) {
|
||||||
|
:root {
|
||||||
|
font-family: "Inter var", sans-serif;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -69,14 +77,6 @@ h1, h2, h3, h4, h5, h6 {
|
|||||||
flex: 5;
|
flex: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-clamp-4 {
|
|
||||||
overflow: hidden;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-line-clamp: 4;
|
|
||||||
white-space: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
@ -257,7 +257,6 @@ html:has(.modal[open]) {
|
|||||||
|
|
||||||
/* HACK: should probably be a .button--icon */
|
/* HACK: should probably be a .button--icon */
|
||||||
.modal__close-button {
|
.modal__close-button {
|
||||||
justify-content: center;
|
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
margin-block: -1rem;
|
margin-block: -1rem;
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
@ -320,12 +319,6 @@ html:has(.modal[open]) {
|
|||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 40rem) {
|
|
||||||
.card__header, .card__footer {
|
|
||||||
padding-inline: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table */
|
/* Table */
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
@ -388,15 +381,15 @@ html:has(.modal[open]) {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table__header--align-center, .table__cell--align-center {
|
.table__cell--align-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table__header--align-end, .table__cell--align-end {
|
.table__cell--align-end {
|
||||||
text-align: end;
|
text-align: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table__header--compact, .table__cell--compact {
|
.table__cell--compact {
|
||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
padding-block: 0.75rem;
|
padding-block: 0.75rem;
|
||||||
}
|
}
|
||||||
@ -417,29 +410,6 @@ html:has(.modal[open]) {
|
|||||||
padding: 0.75rem 1.5rem;
|
padding: 0.75rem 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table__paginator > nav {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 40rem) {
|
|
||||||
.table__header, .table__cell {
|
|
||||||
padding-inline: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table__header--compact, .table__cell--compact {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table__header--compact:not(:first-child), .table__cell--compact:not(:first-child) {
|
|
||||||
padding-inline-start: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table__header--compact:not(:last-child), .table__cell--compact:not(:last-child) {
|
|
||||||
padding-inline-end: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*@media (prefers-color-scheme: dark) {
|
/*@media (prefers-color-scheme: dark) {
|
||||||
.table__header {
|
.table__header {
|
||||||
background-color: rgb(55, 65, 81);
|
background-color: rgb(55, 65, 81);
|
||||||
@ -485,8 +455,7 @@ html:has(.modal[open]) {
|
|||||||
/* Button */
|
/* Button */
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
display: inline-flex;
|
display: inline-block;
|
||||||
align-items: center;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background-color: rgb(255, 255, 255);
|
background-color: rgb(255, 255, 255);
|
||||||
@ -496,7 +465,7 @@ html:has(.modal[open]) {
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.25rem;
|
line-height: 1.25rem;
|
||||||
padding: 0.5rem 0.75rem;
|
padding: 0.5rem 1rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -522,64 +491,9 @@ html:has(.modal[open]) {
|
|||||||
opacity: 50%;
|
opacity: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Button group */
|
|
||||||
|
|
||||||
.button-group {
|
.button-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
gap: 1rem;
|
||||||
|
|
||||||
.button-group > .button:not(:nth-child(1 of .button)) {
|
|
||||||
border-start-start-radius: 0;
|
|
||||||
border-end-start-radius: 0;
|
|
||||||
border-inline-start: 1px solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group > .button:not(:nth-last-child(1 of .button)) {
|
|
||||||
border-start-end-radius: 0;
|
|
||||||
border-end-end-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dropdown */
|
|
||||||
|
|
||||||
.dropdown__toggle {
|
|
||||||
anchor-name: --dropdown-toggle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown__toggle::after {
|
|
||||||
content: "";
|
|
||||||
display: inline-block;
|
|
||||||
border-block-start: 0.3rem solid;
|
|
||||||
border-block-end: 0;
|
|
||||||
border-inline: 0.3rem solid transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown__toggle::after:not(:empty) {
|
|
||||||
margin-inline-start: 0.375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown:has(> :popover-open) > .dropdown__toggle[popovertarget] {
|
|
||||||
outline: none;
|
|
||||||
filter: brightness(0.85);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown__menu {
|
|
||||||
position-anchor: --dropdown-toggle;
|
|
||||||
inset: calc(anchor(end) + 0.125rem) anchor(end) auto auto;
|
|
||||||
padding-block: 0.5rem;
|
|
||||||
border: 1px solid rgb(209, 213, 219);
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown__item {
|
|
||||||
border-block-start: 1px solid rgb(229, 231, 235);
|
|
||||||
border-block-end: none;
|
|
||||||
border-inline: none;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown__item:first-child {
|
|
||||||
border-block-start: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form field */
|
/* Form field */
|
||||||
|
@ -13,7 +13,7 @@ export default class ModalController extends Controller {
|
|||||||
if (!this.element.open) {
|
if (!this.element.open) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.type === "turbo:submit-end" && (event.detail.formSubmission.method === "GET" || !event.detail.success)) {
|
if (event.type === "turbo:submit-end" && (event.detail.formSubmission.method === 0 || !event.detail.success)) {
|
||||||
// Don't close modal if form method was GET or submission failed
|
// Don't close modal if form method was GET or submission failed
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Controller } from "/lib/hotwired/stimulus/dist/stimulus.js";
|
import { Controller } from "/lib/hotwired/stimulus/dist/stimulus.js";
|
||||||
|
|
||||||
export default class TransactionItemFormController extends Controller {
|
export default class TransactionItemFormController extends Controller {
|
||||||
static targets = ["barcodeButton", "barcodeData", "barcodeFormat", "barcodeFormField", "brand", "option"];
|
static targets = ["barcodeButton", "barcodeData", "barcodeFormat", "barcodeFormField", "brand", "option", "price", "quantity"];
|
||||||
|
|
||||||
#scanning = false;
|
#scanning = false;
|
||||||
#scanIntervalId;
|
#scanIntervalId;
|
||||||
@ -35,11 +35,10 @@ export default class TransactionItemFormController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setPriceAndQuantity(event) {
|
setPriceAndQuantity(event) {
|
||||||
const { brand, name, price, quantity, unit } = event.target.form.elements;
|
const { brand, name } = event.target.form.elements;
|
||||||
|
|
||||||
if (!brand.value || !name.value) {
|
if (!brand.value || !name.value) {
|
||||||
price.value = "";
|
this.priceTarget.value = "";
|
||||||
quantity.value = !unit.value ? "1" : "";
|
this.quantityTarget.value = "1";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,11 +47,11 @@ export default class TransactionItemFormController extends Controller {
|
|||||||
option.value === name.value);
|
option.value === name.value);
|
||||||
|
|
||||||
if (option != null) {
|
if (option != null) {
|
||||||
if (!price.value) {
|
if (!this.priceTarget.value) {
|
||||||
price.value = option.getAttribute("data-price");
|
this.priceTarget.value = option.getAttribute("data-price");
|
||||||
}
|
}
|
||||||
if (!quantity.value || (!unit.value && quantity.value === "1")) {
|
if (!this.quantityTarget.value || this.quantityTarget.value === "1") {
|
||||||
quantity.value = option.getAttribute("data-quantity") || (!unit.value ? "1" : "");
|
this.quantityTarget.value = option.getAttribute("data-quantity") || "1";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,3 +19,19 @@ document.addEventListener("turbo:render", () => {
|
|||||||
timeout = setTimeout(() => document.getElementById("sidebarToggle").checked = false, 500);
|
timeout = setTimeout(() => document.getElementById("sidebarToggle").checked = false, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let transition;
|
||||||
|
document.addEventListener("turbo:before-render", async event => {
|
||||||
|
if (document.startViewTransition) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (transition == undefined) {
|
||||||
|
transition = document.startViewTransition(() => event.detail.resume());
|
||||||
|
await transition.finished;
|
||||||
|
transition = undefined;
|
||||||
|
} else {
|
||||||
|
await transition.finished;
|
||||||
|
event.detail.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@ -2,12 +2,12 @@
|
|||||||
An application for (manually) tracking your grocery shopping habits.
|
An application for (manually) tracking your grocery shopping habits.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
A pre-built image is available at [git.jamsch0.dev/jamsch0/groceries](https://git.jamsch0.dev/jamsch0/-/packages/container/groceries).
|
A pre-built image is available at [ghcr.io/jamsch0/groceries](https://ghcr.io/jamsch0/groceries).
|
||||||
The default configuration can be found in `config.ini` once the volume has been created.
|
The default configuration can be found in `config.ini` once the volume has been created.
|
||||||
|
|
||||||
Example usage:
|
Example usage:
|
||||||
```bash
|
```bash
|
||||||
$ docker run -d -p 8080:80 -e LANG=en_GB TZ=Europe/London -v ./groceries:/config git.jamsch0.dev/jamsch0/groceries
|
$ docker run -d -p 8080:80 -e LANG=en_GB TZ=Europe/London -v ./groceries:/config ghcr.io/jamsch0/groceries
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
Reference in New Issue
Block a user