From 4848dda4a4e2ea068d421458382ef4a669cf5ded Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Thu, 22 Oct 2026 05:38:49 +0200 Subject: [PATCH] Migrate inventory and reports to the SPA Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 1 - bbs/api/mutations/inventory.py | 63 ++++++++ bbs/api/queries/inventory.py | 106 +++++++++++++- bbs/api/schema.py | 3 +- bbs/api/types/inventory.py | 41 +++++- bbs/api/types/shop.py | 6 + bbs/tests/test_mutations.py | 138 +++++++++++++++++- bbs/tests/test_spa.py | 10 ++ bbs/urls.py | 12 +- bbs/views/spa.py | 11 ++ frontend/src/Routes.tsx | 6 + frontend/src/components/Header.tsx | 10 ++ .../features/inventory/Inventory.module.css | 71 +++++++++ frontend/src/features/inventory/Inventory.tsx | 126 ++++++++++++++++ .../src/features/inventory/InventoryAdd.tsx | 102 +++++++++++++ .../features/inventory/InventoryReport.tsx | 138 ++++++++++++++++++ .../createInventoryTransaction.graphql | 20 +++ .../features/inventory/inventoryPage.graphql | 25 ++++ .../inventory/inventoryProducts.graphql | 7 + .../inventory/inventoryReport.graphql | 18 +++ .../src/features/register/ProductCard.tsx | 4 +- frontend/src/features/register/Register.tsx | 4 +- frontend/src/features/register/useRegister.ts | 14 +- frontend/src/types.ts | 116 +++++++++++++++ frontend/vite.config.js | 2 +- schema.graphql | 54 +++++++ 26 files changed, 1090 insertions(+), 18 deletions(-) create mode 100644 bbs/api/mutations/inventory.py create mode 100644 bbs/tests/test_spa.py create mode 100644 bbs/views/spa.py create mode 100644 frontend/src/features/inventory/Inventory.module.css create mode 100644 frontend/src/features/inventory/Inventory.tsx create mode 100644 frontend/src/features/inventory/InventoryAdd.tsx create mode 100644 frontend/src/features/inventory/InventoryReport.tsx create mode 100644 frontend/src/features/inventory/createInventoryTransaction.graphql create mode 100644 frontend/src/features/inventory/inventoryPage.graphql create mode 100644 frontend/src/features/inventory/inventoryProducts.graphql create mode 100644 frontend/src/features/inventory/inventoryReport.graphql diff --git a/README.md b/README.md index dee29ed..7941fe4 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,6 @@ To create the admin during data seeding instead, set `BBS_SEED_ADMIN_PASSWORD` f ## TODO -- Migrate inventory and report views to the SPA - Stats: Implement users tab ## TODO-old diff --git a/bbs/api/mutations/inventory.py b/bbs/api/mutations/inventory.py new file mode 100644 index 0000000..ed265c2 --- /dev/null +++ b/bbs/api/mutations/inventory.py @@ -0,0 +1,63 @@ +from decimal import Decimal +from typing import Annotated + +import strawberry +from django.db import transaction +from strawberry import ID, Info + +from bbs import models +from bbs.api.errors import FieldError, FormErrors +from bbs.api.types.inventory import InventoryTransaction + + +@strawberry.input +class InventoryTransactionInput: + product_id: ID + amount: int + unit_price: Decimal + comment: str = "" + + +@strawberry.type +class CreateInventoryTransactionSuccess: + inventory_transaction: InventoryTransaction + + +CreateInventoryTransactionResponse = Annotated[ + CreateInventoryTransactionSuccess | FormErrors, + strawberry.union("CreateInventoryTransactionResponse"), +] + + +@strawberry.mutation +@transaction.atomic +def create_inventory_transaction( + info: Info, + data: InventoryTransactionInput, +) -> CreateInventoryTransactionResponse: + user = info.context.request.user + if not user.is_authenticated: + message = "Logg inn for å registrere varetransaksjoner." + return FormErrors(message=message, fields=[FieldError(field="user", message=message)]) + + try: + product = models.Product.objects.get(pk=data.product_id, active=True) + except (models.Product.DoesNotExist, ValueError): + message = "Velg et aktivt produkt." + return FormErrors(message=message, fields=[FieldError(field="product_id", message=message)]) + + product.inventory_amount += data.amount + product.save() + inventory_transaction = models.InventoryTransaction.objects.create( + user=user, + product=product, + amount=data.amount, + unit_price=data.unit_price, + comment=data.comment, + ) + return CreateInventoryTransactionSuccess(inventory_transaction=inventory_transaction) + + +@strawberry.type +class InventoryMutations: + create_inventory_transaction = create_inventory_transaction diff --git a/bbs/api/queries/inventory.py b/bbs/api/queries/inventory.py index fa1ca22..0059297 100644 --- a/bbs/api/queries/inventory.py +++ b/bbs/api/queries/inventory.py @@ -1,9 +1,113 @@ +from datetime import datetime +from decimal import Decimal + import strawberry import strawberry_django +from django.db.models import F, Sum +from django.utils import timezone -from bbs.api.types.inventory import Transaction, TransactionOrdering +from bbs import models +from bbs.api.types.inventory import ( + InventoryReport, + InventoryReportProduct, + InventoryTransaction, + InventoryTransactionOrdering, + Transaction, + TransactionOrdering, +) @strawberry.type class InventoryQueries: transaction_list: list[Transaction] = strawberry_django.field(pagination=True, ordering=TransactionOrdering) + inventory_transaction_list: list[InventoryTransaction] = strawberry_django.field( + pagination=True, + ordering=InventoryTransactionOrdering, + ) + + @strawberry.field + def inventory_report(self, start_time: datetime, end_time: datetime) -> InventoryReport: + if timezone.is_naive(start_time): + start_time = timezone.make_aware(start_time) + if timezone.is_naive(end_time): + end_time = timezone.make_aware(end_time) + if start_time > end_time: + raise ValueError("The report start date must not be after the end date.") + + purchases = ( + models.InventoryTransaction.objects.filter(created__range=(start_time, end_time)) + .values("product_id", "product__name") + .annotate(units=Sum("amount"), value=Sum(F("amount") * F("unit_price"))) + ) + sales = ( + models.OrderLine.objects.filter(order__created__range=(start_time, end_time)) + .values("product_id", "product__name") + .annotate(units=Sum("amount"), value=Sum(F("amount") * F("unit_price"))) + ) + + product_totals = {} + for product in purchases: + product_totals[product["product_id"]] = { + "product_id": product["product_id"], + "product_name": product["product__name"], + "purchase_units": product["units"] or 0, + "purchase_value": product["value"] or Decimal(0), + "sales_units": 0, + "sales_value": Decimal(0), + } + + for product in sales: + product_data = product_totals.setdefault( + product["product_id"], + { + "product_id": product["product_id"], + "product_name": product["product__name"], + "purchase_units": 0, + "purchase_value": Decimal(0), + "sales_units": 0, + "sales_value": Decimal(0), + }, + ) + product_data["sales_units"] = product["units"] or 0 + product_data["sales_value"] = product["value"] or Decimal(0) + + product_data = sorted(product_totals.values(), key=lambda product: product["product_name"]) + inventory_purchases = sum((product["purchase_value"] for product in product_data), Decimal(0)) + product_sales = sum((product["sales_value"] for product in product_data), Decimal(0)) + + deposits = ( + models.Transaction.objects.filter(created__range=(start_time, end_time)).aggregate(total=Sum("amount"))[ + "total" + ] + or Decimal(0) + ) + order_purchases = ( + models.Order.objects.filter(created__range=(start_time, end_time)).aggregate(total=Sum("order_sum"))[ + "total" + ] + or Decimal(0) + ) + deposits_start = ( + models.Transaction.objects.filter(created__lt=start_time).aggregate(total=Sum("amount"))["total"] + or Decimal(0) + ) + orders_start = ( + models.Order.objects.filter(created__lt=start_time).aggregate(total=Sum("order_sum"))["total"] or Decimal(0) + ) + deposits_end = ( + models.Transaction.objects.filter(created__lte=end_time).aggregate(total=Sum("amount"))["total"] + or Decimal(0) + ) + orders_end = ( + models.Order.objects.filter(created__lte=end_time).aggregate(total=Sum("order_sum"))["total"] or Decimal(0) + ) + + return InventoryReport( + products=[InventoryReportProduct(**product) for product in product_data], + inventory_purchases=inventory_purchases, + product_sales=product_sales, + deposits=deposits, + order_purchases=order_purchases, + balance_at_start=deposits_start - orders_start, + balance_at_end=deposits_end - orders_end, + ) diff --git a/bbs/api/schema.py b/bbs/api/schema.py index 4f94e1f..cc96d1d 100644 --- a/bbs/api/schema.py +++ b/bbs/api/schema.py @@ -2,6 +2,7 @@ import strawberry from django.core.exceptions import ObjectDoesNotExist from strawberry.extensions import SchemaExtension +from bbs.api.mutations.inventory import InventoryMutations from bbs.api.mutations.shop import ShopMutations from bbs.api.mutations.users import UsersMutations from bbs.api.queries.inventory import InventoryQueries @@ -15,7 +16,7 @@ class Query(InventoryQueries, ShopQueries, UsersQueries): @strawberry.type -class Mutation(ShopMutations, UsersMutations): +class Mutation(InventoryMutations, ShopMutations, UsersMutations): pass diff --git a/bbs/api/types/inventory.py b/bbs/api/types/inventory.py index 662482b..2a519ce 100644 --- a/bbs/api/types/inventory.py +++ b/bbs/api/types/inventory.py @@ -1,12 +1,14 @@ +from decimal import Decimal from typing import TYPE_CHECKING, Annotated import strawberry import strawberry_django -from strawberry import auto +from strawberry import ID, auto from bbs import models if TYPE_CHECKING: + from .shop import Product from .users import User @@ -21,3 +23,40 @@ class Transaction: user: Annotated["User", strawberry.lazy(".users")] amount: auto created: auto + + +@strawberry_django.type(models.InventoryTransaction) +class InventoryTransaction: + id: auto + user: Annotated["User", strawberry.lazy(".users")] + product: Annotated["Product", strawberry.lazy(".shop")] + amount: auto + unit_price: auto + comment: auto + created: auto + + +@strawberry_django.order_type(models.InventoryTransaction) +class InventoryTransactionOrdering: + created: auto + + +@strawberry.type +class InventoryReportProduct: + product_id: ID + product_name: str + purchase_units: int + purchase_value: Decimal + sales_units: int + sales_value: Decimal + + +@strawberry.type +class InventoryReport: + products: list[InventoryReportProduct] + inventory_purchases: Decimal + product_sales: Decimal + deposits: Decimal + order_purchases: Decimal + balance_at_start: Decimal + balance_at_end: Decimal diff --git a/bbs/api/types/shop.py b/bbs/api/types/shop.py index 13c7915..2e9a8f3 100644 --- a/bbs/api/types/shop.py +++ b/bbs/api/types/shop.py @@ -1,3 +1,4 @@ +from decimal import Decimal from typing import TYPE_CHECKING, Annotated import strawberry @@ -24,6 +25,11 @@ class Product: image_url: str sale_price_int: auto sale_price_ext: auto + active: auto + inventory_amount: auto + updated: auto + wholesale_unit_price: Decimal + wholesale_value: Decimal # FIXME: stricter types user_counts: JSONScalar diff --git a/bbs/tests/test_mutations.py b/bbs/tests/test_mutations.py index 3321c75..da3e57d 100644 --- a/bbs/tests/test_mutations.py +++ b/bbs/tests/test_mutations.py @@ -1,8 +1,10 @@ import pytest # noqa: INP001 +from django.contrib.auth import get_user_model from django.test import Client +from django.utils import timezone from strawberry.django.test import GraphQLTestClient -from bbs.models import Order, Product +from bbs.models import InventoryTransaction, Order, OrderLine, Product, Transaction @pytest.fixture(name="gql_client") @@ -50,3 +52,137 @@ def test_create_order(product, gql_client): assert Order.objects.count() == 1 o = Order.objects.first() assert o.orderlines.first().product.id == product.id + + +def test_create_inventory_transaction_requires_login(product, gql_client): + result = gql_client.query( + """ + mutation CreateInventoryTransaction($data: InventoryTransactionInput!) { + createInventoryTransaction(data: $data) { + ... on FormErrors { message } + } + } + """, + variables={"data": {"productId": str(product.id), "amount": 5, "unitPrice": "12.5"}}, + ) + + assert not result.errors + assert result.data["createInventoryTransaction"]["message"] == "Logg inn for å registrere varetransaksjoner." + product.refresh_from_db() + assert product.inventory_amount == 0 + assert InventoryTransaction.objects.count() == 0 + + +def test_create_inventory_transaction_updates_stock(product, gql_client, client): + user = get_user_model().objects.create_user(username="stock-manager") + client.force_login(user) + + result = gql_client.query( + """ + mutation CreateInventoryTransaction($data: InventoryTransactionInput!) { + createInventoryTransaction(data: $data) { + ... on CreateInventoryTransactionSuccess { + inventoryTransaction { + amount + unitPrice + product { id inventoryAmount } + user { id } + } + } + } + } + """, + variables={ + "data": { + "productId": str(product.id), + "amount": 5, + "unitPrice": "12.5", + "comment": "Restock", + } + }, + ) + + assert not result.errors + transaction = result.data["createInventoryTransaction"]["inventoryTransaction"] + assert transaction["amount"] == 5 + assert transaction["unitPrice"] == "12.5" + assert transaction["product"]["inventoryAmount"] == 5 + assert transaction["user"]["id"] == str(user.id) + product.refresh_from_db() + assert product.inventory_amount == 5 + + +def test_inventory_report_aggregates_period_data(product, gql_client): + user = get_user_model().objects.create_user(username="report-user") + InventoryTransaction.objects.create(user=user, product=product, amount=5, unit_price=10) + transaction = Transaction.objects.create(user=user, amount=100) + order = Order.objects.create(order_sum=30) + OrderLine.objects.create(order=order, product=product, amount=2, unit_price=15) + start_time = timezone.make_naive(timezone.now() - timezone.timedelta(days=1)) + end_time = timezone.make_naive(timezone.now() + timezone.timedelta(days=1)) + + result = gql_client.query( + """ + query InventoryReport($startTime: DateTime!, $endTime: DateTime!) { + inventoryReport(startTime: $startTime, endTime: $endTime) { + products { + productId + purchaseUnits + purchaseValue + salesUnits + salesValue + } + inventoryPurchases + productSales + deposits + orderPurchases + balanceAtStart + balanceAtEnd + } + } + """, + variables={"startTime": start_time.isoformat(), "endTime": end_time.isoformat()}, + ) + + assert not result.errors + report = result.data["inventoryReport"] + assert report["products"] == [ + { + "productId": str(product.id), + "purchaseUnits": 5, + "purchaseValue": "50", + "salesUnits": 2, + "salesValue": "30", + } + ] + assert report["inventoryPurchases"] == "50" + assert report["productSales"] == "30" + assert report["deposits"] == "100" + assert report["orderPurchases"] == "30" + assert report["balanceAtStart"] == "0" + assert report["balanceAtEnd"] == "70" + assert transaction.amount == 100 + + products_result = gql_client.query( + """ + query InventoryProducts { + allProducts { + id + active + inventoryAmount + wholesaleUnitPrice + wholesaleValue + } + inventoryTransactionList(ordering: [{created: DESC}], pagination: {limit: 20, offset: 0}) { + amount + unitPrice + product { name } + user { firstName lastName } + } + } + """ + ) + + assert not products_result.errors + assert products_result.data["allProducts"][0]["active"] + assert products_result.data["inventoryTransactionList"][0]["amount"] == 5 diff --git a/bbs/tests/test_spa.py b/bbs/tests/test_spa.py new file mode 100644 index 0000000..3d841ff --- /dev/null +++ b/bbs/tests/test_spa.py @@ -0,0 +1,10 @@ +import pytest # noqa: INP001 +from django.test import Client + + +@pytest.mark.parametrize("path", ["/inventory", "/inventory/add", "/report"]) +def test_inventory_pages_serve_the_spa_entry(client: Client, path: str): + response = client.get(path) + + assert response.status_code == 200 + assert b'
' in b"".join(response.streaming_content) diff --git a/bbs/urls.py b/bbs/urls.py index 9d5d0f1..01a728e 100644 --- a/bbs/urls.py +++ b/bbs/urls.py @@ -7,15 +7,17 @@ from django.views.decorators.csrf import csrf_exempt from strawberry.django.views import GraphQLView from bbs.api.schema import schema -from bbs.views.reports import inventory, inventory_add, report +from bbs.views.spa import spa_index admin.autodiscover() urlpatterns = [ - # TODO: migrate reports to SPA - path("inventory/", inventory, name="inventory"), - path("inventory/add/", inventory_add, name="inventory-add"), - path("report", report, name="report"), + path("inventory", spa_index, name="inventory"), + path("inventory/", spa_index), + path("inventory/add", spa_index, name="inventory-add"), + path("inventory/add/", spa_index), + path("report", spa_index, name="report"), + path("report/", spa_index), path("accounts/login/", LoginView.as_view(), name="django.contrib.auth.views.login"), path( "accounts/logout/", diff --git a/bbs/views/spa.py b/bbs/views/spa.py new file mode 100644 index 0000000..9de83b3 --- /dev/null +++ b/bbs/views/spa.py @@ -0,0 +1,11 @@ +from pathlib import Path + +from django.contrib.staticfiles import finders +from django.http import FileResponse, Http404 + + +def spa_index(_request): + index_path = finders.find("index.html") + if not index_path: + raise Http404("The frontend has not been built.") + return FileResponse(Path(index_path).open("rb"), content_type="text/html") diff --git a/frontend/src/Routes.tsx b/frontend/src/Routes.tsx index fdcc504..e7c5200 100644 --- a/frontend/src/Routes.tsx +++ b/frontend/src/Routes.tsx @@ -4,6 +4,9 @@ import { Route, Routes as ReactRouterRoutes } from "react-router-dom"; const Splash = React.lazy(() => import("./components/Splash")); const UserCreate = React.lazy(() => import("./features/auth/UserCreate")); const Deposit = React.lazy(() => import("./features/deposit/Deposit")); +const Inventory = React.lazy(() => import("./features/inventory/Inventory")); +const InventoryAdd = React.lazy(() => import("./features/inventory/InventoryAdd")); +const InventoryReport = React.lazy(() => import("./features/inventory/InventoryReport")); const Log = React.lazy(() => import("./features/log/Log")); const Register = React.lazy(() => import("./features/register/Register")); const OrderStats = React.lazy(() => import("./features/stats/OrderStats")); @@ -16,6 +19,9 @@ export const Routes = (): JSX.Element => ( Loading...}> } /> + } /> + } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index fcc6507..3e7f396 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -32,6 +32,16 @@ export const Header = (): JSX.Element => { Logg +
  • + + Varebeholdning + +
  • +
  • + + Rapport + +
  • Statistikk diff --git a/frontend/src/features/inventory/Inventory.module.css b/frontend/src/features/inventory/Inventory.module.css new file mode 100644 index 0000000..9a5ee0d --- /dev/null +++ b/frontend/src/features/inventory/Inventory.module.css @@ -0,0 +1,71 @@ +.section { + margin-top: 2rem; +} + +.tableWrap { + max-width: 100%; + overflow-x: auto; +} + +.tableWrap table { + width: 100%; +} + +.inactive { + opacity: 0.65; +} + +.transactions { + margin-top: 2rem; +} + +.pagination { + display: flex; + gap: 0.75rem; + margin-top: 1rem; +} + +.form { + display: grid; + max-width: 36rem; + gap: 1rem; +} + +.form label { + display: grid; + gap: 0.35rem; +} + +.form textarea, +.form select { + padding: 0.5rem; + border: 1px solid var(--border); + border-radius: 4px; + background: rgb(35, 15, 41); + color: var(--text); + font: inherit; +} + +.form textarea { + min-height: 6rem; +} + +.summary { + display: flex; + flex-wrap: wrap; + gap: 1rem 2rem; +} + +.report { + margin-top: 2rem; +} + +.error { + color: var(--sun); +} + +@media (max-width: 640px) { + .tableWrap table { + min-width: 42rem; + } +} diff --git a/frontend/src/features/inventory/Inventory.tsx b/frontend/src/features/inventory/Inventory.tsx new file mode 100644 index 0000000..7a467e7 --- /dev/null +++ b/frontend/src/features/inventory/Inventory.tsx @@ -0,0 +1,126 @@ +import { useQuery } from "@apollo/client"; +import React, { useState } from "react"; +import { Link } from "react-router-dom"; + +import { GQLError } from "../../components/GQLError"; +import { InventoryPageDocument } from "../../types"; +import { formatCurrency } from "../../utils/currency"; +import { formatTime } from "../../utils/time"; +import styles from "./Inventory.module.css"; + +const PAGE_SIZE = 20; + +const Inventory = () => { + const [offset, setOffset] = useState(0); + const { data, loading, error } = useQuery(InventoryPageDocument, { + variables: { limit: PAGE_SIZE, offset }, + }); + + if (loading) return
    Loading...
    ; + if (error) return ; + + const products = [...(data?.allProducts || [])].sort( + (a, b) => + Number(b.active) - Number(a.active) || a.inventoryAmount - b.inventoryAmount || a.name.localeCompare(b.name), + ); + const activeProducts = products.filter((product) => product.active); + const transactions = data?.inventoryTransactionList || []; + + return ( +
    +

    Varebeholdning

    +
    +
    Antall aktive varer: {activeProducts.reduce((total, product) => total + product.inventoryAmount, 0)}
    +
    + Innkjøpsverdi:{" "} + {formatCurrency(activeProducts.reduce((total, product) => total + Number(product.wholesaleValue), 0))} kr +
    +
    +
    +
    +

    Varer

    + + Registrer varetransaksjon + + Åpne rapport +
    +
    + + + + + + + + + + + + + {products.map((product) => ( + + + + + + + + + ))} + +
    VareBeholdningInnkjøpsprisVerdiI salgSist oppdatert
    {product.name}{product.inventoryAmount}{formatCurrency(Number(product.wholesaleUnitPrice))} kr{formatCurrency(Number(product.wholesaleValue))} kr{product.active ? "Ja" : "Nei"}{formatTime(product.updated)}
    +
    +
    +
    +

    Varetransaksjoner

    +
    + + + + + + + + + + + + + {transactions.map((transaction) => ( + + + + + + + + + ))} + {!transactions.length && ( + + + + )} + +
    NårVareAntallEnhetsprisHvemKommentar
    {formatTime(transaction.created)}{transaction.product.name}{transaction.amount}{formatCurrency(Number(transaction.unitPrice))} kr + {transaction.user.firstName} {transaction.user.lastName} + {transaction.comment}
    Ingen varetransaksjoner ennå.
    +
    +
    + + +
    +
    +
    + ); +}; + +export default Inventory; diff --git a/frontend/src/features/inventory/InventoryAdd.tsx b/frontend/src/features/inventory/InventoryAdd.tsx new file mode 100644 index 0000000..5bf4d42 --- /dev/null +++ b/frontend/src/features/inventory/InventoryAdd.tsx @@ -0,0 +1,102 @@ +import { useMutation, useQuery } from "@apollo/client"; +import React, { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; + +import { GQLError } from "../../components/GQLError"; +import { useToast } from "../../components/ToastProvider"; +import { CreateInventoryTransactionDocument, InventoryProductsDocument } from "../../types"; +import styles from "./Inventory.module.css"; + +const InventoryAdd = () => { + const navigate = useNavigate(); + const { showToast } = useToast(); + const { data, loading, error } = useQuery(InventoryProductsDocument); + const [createInventoryTransaction, { loading: saving }] = useMutation(CreateInventoryTransactionDocument); + const [productId, setProductId] = useState(""); + const [amount, setAmount] = useState(""); + const [unitPrice, setUnitPrice] = useState(""); + const [comment, setComment] = useState(""); + const products = data?.allProducts || []; + + if (loading) return
    Loading...
    ; + if (error) return ; + + const onSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + try { + const result = await createInventoryTransaction({ + variables: { + data: { + productId, + amount: Number(amount), + unitPrice, + comment, + }, + }, + }); + const response = result.data?.createInventoryTransaction; + if (response?.__typename === "CreateInventoryTransactionSuccess") { + showToast(`Registrerte ${amount} stk ${response.inventoryTransaction.product.name}.`); + navigate("/inventory"); + } else if (response?.__typename === "FormErrors") { + showToast(response.message, "danger"); + } else { + showToast("Kunne ikke registrere varetransaksjonen.", "danger"); + } + } catch (mutationError) { + showToast( + mutationError instanceof Error ? mutationError.message : "Kunne ikke registrere varetransaksjonen.", + "danger", + ); + } + }; + + return ( +
    +

    Registrer varetransaksjon

    +

    + Du må være innlogget for å registrere varer. Logg inn. +

    +
    + + + +