feat(inventory): add and edit products without the admin
The inventory page gets a "Ny vare" link and a "Rediger" link per product, leading to a form for name, prices, volume, alcohol, description and whether the product is for sale. Stock still changes only through inventory transactions and sales, and the image through the admin. Saving needs the same access as the Django admin: staff, plus the add or change product permission (superusers have both). The server checks this on every save and validates with the model's rules. Logged out visitors are sent to log in and back to the form. New in the API: createProduct and updateProduct mutations, a product query, and a me query so the frontend knows who is logged in. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
31eb71b354
commit
7ab2a0f1f2
18 files changed
+760
-4
No files matched your search
@@ -0,0 +1,101 @@
|
||||
from decimal import Decimal
|
||||
from typing import Annotated
|
||||
|
||||
import strawberry
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from strawberry import ID, Info
|
||||
|
||||
from bbs import models
|
||||
from bbs.api.errors import FieldError, FormErrors
|
||||
from bbs.api.permissions import can_add_products, can_change_products
|
||||
from bbs.api.types.shop import Product
|
||||
|
||||
|
||||
@strawberry.input
|
||||
class ProductInput:
|
||||
name: str
|
||||
sale_price_int: Decimal
|
||||
sale_price_ext: Decimal
|
||||
volume_liter: Decimal | None = None
|
||||
alcohol_percent: Decimal | None = None
|
||||
description: str = ""
|
||||
active: bool = True
|
||||
|
||||
|
||||
# The stock changes through inventory transactions and sales, and the image through the admin
|
||||
EDITABLE_FIELDS = [
|
||||
"name",
|
||||
"sale_price_int",
|
||||
"sale_price_ext",
|
||||
"volume_liter",
|
||||
"alcohol_percent",
|
||||
"description",
|
||||
"active",
|
||||
]
|
||||
NON_NEGATIVE_FIELDS = ["sale_price_int", "sale_price_ext", "volume_liter", "alcohol_percent"]
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class SaveProductSuccess:
|
||||
product: Product
|
||||
|
||||
|
||||
SaveProductResponse = Annotated[SaveProductSuccess | FormErrors, strawberry.union("SaveProductResponse")]
|
||||
|
||||
|
||||
def _access_denied(user) -> FormErrors:
|
||||
message = "Du har ikke tilgang til å endre varer." if user.is_authenticated else "Logg inn for å endre varer."
|
||||
return FormErrors(message=message, fields=[FieldError(field="user", message=message)])
|
||||
|
||||
|
||||
def _save(product: models.Product, data: ProductInput) -> SaveProductResponse:
|
||||
for field in EDITABLE_FIELDS:
|
||||
setattr(product, field, getattr(data, field))
|
||||
|
||||
errors: dict[str, list[str]] = {}
|
||||
try:
|
||||
product.full_clean()
|
||||
except ValidationError as e:
|
||||
errors = e.message_dict
|
||||
for field in NON_NEGATIVE_FIELDS:
|
||||
value = getattr(product, field)
|
||||
if value is not None and value < 0:
|
||||
errors.setdefault(field, []).append("Kan ikke være negativ.")
|
||||
|
||||
if errors:
|
||||
return FormErrors(
|
||||
message="Varen ble ikke lagret. Rett opp feilene i skjemaet.",
|
||||
fields=[FieldError(field=field, message=" ".join(messages)) for field, messages in errors.items()],
|
||||
)
|
||||
product.save()
|
||||
return SaveProductSuccess(product=product)
|
||||
|
||||
|
||||
@strawberry.mutation
|
||||
@transaction.atomic
|
||||
def create_product(info: Info, data: ProductInput) -> SaveProductResponse:
|
||||
user = info.context.request.user
|
||||
if not can_add_products(user):
|
||||
return _access_denied(user)
|
||||
return _save(models.Product(), data)
|
||||
|
||||
|
||||
@strawberry.mutation
|
||||
@transaction.atomic
|
||||
def update_product(info: Info, product_id: ID, data: ProductInput) -> SaveProductResponse:
|
||||
user = info.context.request.user
|
||||
if not can_change_products(user):
|
||||
return _access_denied(user)
|
||||
try:
|
||||
product = models.Product.objects.select_for_update().get(pk=product_id)
|
||||
except models.Product.DoesNotExist, ValueError:
|
||||
message = "Fant ikke varen."
|
||||
return FormErrors(message=message, fields=[FieldError(field="product_id", message=message)])
|
||||
return _save(product, data)
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class ProductMutations:
|
||||
create_product = create_product
|
||||
update_product = update_product
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Who may do what through the API, using the same rules as the Django admin."""
|
||||
|
||||
|
||||
def _admin_can(user, permission):
|
||||
# The admin only lets in staff, and then checks the model permission (superusers have all of them)
|
||||
return user.is_authenticated and user.is_staff and user.has_perm(permission)
|
||||
|
||||
|
||||
def can_add_products(user):
|
||||
return _admin_can(user, "bbs.add_product")
|
||||
|
||||
|
||||
def can_change_products(user):
|
||||
return _admin_can(user, "bbs.change_product")
|
||||
@@ -19,6 +19,10 @@ class ShopQueries:
|
||||
all_orders: list[Order] = strawberry_django.field()
|
||||
order_list: list[Order] = strawberry_django.field(pagination=True, ordering=OrderOrdering)
|
||||
|
||||
@strawberry.field
|
||||
def product(self, product_id: strawberry.ID) -> Product:
|
||||
return models.Product.objects.get(pk=product_id)
|
||||
|
||||
@strawberry.field
|
||||
def order_stats(self) -> OrderStats:
|
||||
return OrderStats(
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import strawberry
|
||||
from django.contrib.auth.models import User as DjangoUser
|
||||
from strawberry import ID
|
||||
from strawberry import ID, Info
|
||||
|
||||
from bbs.api.types.users import User
|
||||
from bbs.api.permissions import can_add_products, can_change_products
|
||||
from bbs.api.types.users import User, Viewer
|
||||
|
||||
|
||||
@strawberry.type
|
||||
@@ -14,3 +15,15 @@ class UsersQueries:
|
||||
@strawberry.field
|
||||
def user(self, user_id: ID) -> User:
|
||||
return DjangoUser.objects.get(pk=user_id)
|
||||
|
||||
@strawberry.field(description="The logged in user, or null when nobody is logged in")
|
||||
def me(self, info: Info) -> Viewer | None:
|
||||
user = info.context.request.user
|
||||
if not user.is_authenticated:
|
||||
return None
|
||||
return Viewer(
|
||||
id=user.pk,
|
||||
first_name=user.first_name,
|
||||
can_add_products=can_add_products(user),
|
||||
can_change_products=can_change_products(user),
|
||||
)
|
||||
+2
-1
@@ -3,6 +3,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||
from strawberry.extensions import SchemaExtension
|
||||
|
||||
from bbs.api.mutations.inventory import InventoryMutations
|
||||
from bbs.api.mutations.products import ProductMutations
|
||||
from bbs.api.mutations.shop import ShopMutations
|
||||
from bbs.api.mutations.users import UsersMutations
|
||||
from bbs.api.queries.inventory import InventoryQueries
|
||||
@@ -16,7 +17,7 @@ class Query(InventoryQueries, ShopQueries, UsersQueries):
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class Mutation(InventoryMutations, ShopMutations, UsersMutations):
|
||||
class Mutation(InventoryMutations, ProductMutations, ShopMutations, UsersMutations):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@ class ProductFilter:
|
||||
class Product:
|
||||
id: auto
|
||||
name: auto
|
||||
description: auto
|
||||
volume_liter: auto
|
||||
alcohol_percent: auto
|
||||
image_url: str
|
||||
sale_price_int: auto
|
||||
sale_price_ext: auto
|
||||
|
||||
@@ -43,6 +43,14 @@ class UserProfile:
|
||||
]
|
||||
|
||||
|
||||
@strawberry.type
|
||||
class Viewer:
|
||||
id: strawberry.ID
|
||||
first_name: str
|
||||
can_add_products: bool
|
||||
can_change_products: bool
|
||||
|
||||
|
||||
@strawberry_django.type(DjangoUser)
|
||||
class User:
|
||||
id: auto
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.auth.models import User as DjangoUser
|
||||
|
||||
from bbs.models import Product
|
||||
|
||||
ME = "query { me { firstName canAddProducts canChangeProducts } }"
|
||||
|
||||
SAVE_RESULT = """
|
||||
... on SaveProductSuccess {
|
||||
product { id name salePriceInt salePriceExt volumeLiter alcoholPercent description active inventoryAmount }
|
||||
}
|
||||
... on FormErrors { message fields { field message } }
|
||||
"""
|
||||
CREATE_PRODUCT = f"mutation CreateProduct($data: ProductInput!) {{ createProduct(data: $data) {{ {SAVE_RESULT} }} }}"
|
||||
UPDATE_PRODUCT = f"""
|
||||
mutation UpdateProduct($productId: ID!, $data: ProductInput!) {{
|
||||
updateProduct(productId: $productId, data: $data) {{ {SAVE_RESULT} }}
|
||||
}}
|
||||
"""
|
||||
|
||||
PRODUCT_DATA = {
|
||||
"name": "Ny pils",
|
||||
"salePriceInt": "25.0",
|
||||
"salePriceExt": "30.0",
|
||||
"volumeLiter": "0.5",
|
||||
"alcoholPercent": "4.7",
|
||||
"description": "Frisk",
|
||||
"active": True,
|
||||
}
|
||||
|
||||
|
||||
def make_user(username, *, staff=False, superuser=False, permissions=()):
|
||||
user = DjangoUser.objects.create_user(username=username, first_name=username.title())
|
||||
user.is_staff = staff
|
||||
user.is_superuser = superuser
|
||||
user.save()
|
||||
user.user_permissions.set(Permission.objects.filter(content_type__app_label="bbs", codename__in=permissions))
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def staff_with_product_access(db): # noqa: ARG001
|
||||
return make_user("lager", staff=True, permissions=["add_product", "change_product"])
|
||||
|
||||
|
||||
# me
|
||||
# ==
|
||||
|
||||
|
||||
def test_me_is_null_when_logged_out(db, gql_client): # noqa: ARG001
|
||||
res = gql_client.query(ME)
|
||||
assert not res.errors
|
||||
assert res.data["me"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("staff", "superuser", "permissions", "can_add", "can_change"),
|
||||
[
|
||||
(False, False, ["add_product", "change_product"], False, False), # permissions without admin access
|
||||
(True, False, [], False, False),
|
||||
(True, False, ["change_product"], False, True),
|
||||
(True, False, ["add_product", "change_product"], True, True),
|
||||
(True, True, [], True, True),
|
||||
],
|
||||
)
|
||||
def test_me_reports_product_access(db, gql_client, client, staff, superuser, permissions, can_add, can_change): # noqa: ARG001, PLR0913, PLR0917
|
||||
client.force_login(make_user("someone", staff=staff, superuser=superuser, permissions=permissions))
|
||||
res = gql_client.query(ME)
|
||||
assert not res.errors
|
||||
assert res.data["me"] == {"firstName": "Someone", "canAddProducts": can_add, "canChangeProducts": can_change}
|
||||
|
||||
|
||||
# createProduct
|
||||
# =============
|
||||
|
||||
|
||||
def test_create_product(gql_client, client, staff_with_product_access):
|
||||
client.force_login(staff_with_product_access)
|
||||
res = gql_client.query(CREATE_PRODUCT, {"data": PRODUCT_DATA})
|
||||
assert not res.errors
|
||||
product = res.data["createProduct"]["product"]
|
||||
assert product["name"] == "Ny pils"
|
||||
assert product["inventoryAmount"] == 0
|
||||
|
||||
saved = Product.objects.get(pk=product["id"])
|
||||
assert saved.sale_price_int == Decimal("25.0")
|
||||
assert saved.volume_liter == Decimal("0.5")
|
||||
assert saved.active
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("user_kwargs", "message"),
|
||||
[
|
||||
(None, "Logg inn for å endre varer."),
|
||||
({}, "Du har ikke tilgang til å endre varer."),
|
||||
({"permissions": ["add_product"]}, "Du har ikke tilgang til å endre varer."), # not staff
|
||||
({"staff": True, "permissions": ["change_product"]}, "Du har ikke tilgang til å endre varer."),
|
||||
],
|
||||
)
|
||||
def test_create_product_requires_admin_access(db, gql_client, client, user_kwargs, message): # noqa: ARG001
|
||||
if user_kwargs is not None:
|
||||
client.force_login(make_user("someone", **user_kwargs))
|
||||
res = gql_client.query(CREATE_PRODUCT, {"data": PRODUCT_DATA})
|
||||
assert not res.errors
|
||||
assert res.data["createProduct"]["message"] == message
|
||||
assert not Product.objects.exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("changes", "field"),
|
||||
[
|
||||
({"name": ""}, "name"),
|
||||
({"salePriceInt": "-1"}, "sale_price_int"),
|
||||
({"salePriceExt": "12.55"}, "sale_price_ext"), # one decimal at most
|
||||
({"volumeLiter": None}, "volume_liter"),
|
||||
({"alcoholPercent": "-0.5"}, "alcohol_percent"),
|
||||
],
|
||||
)
|
||||
def test_create_product_validates(gql_client, client, staff_with_product_access, changes, field):
|
||||
client.force_login(staff_with_product_access)
|
||||
res = gql_client.query(CREATE_PRODUCT, {"data": {**PRODUCT_DATA, **changes}})
|
||||
assert not res.errors
|
||||
errors = res.data["createProduct"]
|
||||
assert errors["message"] == "Varen ble ikke lagret. Rett opp feilene i skjemaet."
|
||||
assert [error["field"] for error in errors["fields"]] == [field]
|
||||
assert not Product.objects.exists()
|
||||
|
||||
|
||||
# updateProduct
|
||||
# =============
|
||||
|
||||
|
||||
def test_update_product(product, gql_client, client):
|
||||
product.inventory_amount = 7
|
||||
product.save()
|
||||
client.force_login(make_user("admin", staff=True, superuser=True))
|
||||
|
||||
res = gql_client.query(UPDATE_PRODUCT, {"productId": str(product.id), "data": {**PRODUCT_DATA, "active": False}})
|
||||
assert not res.errors
|
||||
assert res.data["updateProduct"]["product"]["name"] == "Ny pils"
|
||||
|
||||
product.refresh_from_db()
|
||||
assert product.name == "Ny pils"
|
||||
assert product.sale_price_ext == Decimal("30.0")
|
||||
assert not product.active
|
||||
assert product.inventory_amount == 7 # stock only changes through transactions and sales
|
||||
|
||||
|
||||
def test_update_product_requires_change_permission(product, gql_client, client):
|
||||
client.force_login(make_user("someone", staff=True, permissions=["add_product"]))
|
||||
res = gql_client.query(UPDATE_PRODUCT, {"productId": str(product.id), "data": PRODUCT_DATA})
|
||||
assert not res.errors
|
||||
assert res.data["updateProduct"]["message"] == "Du har ikke tilgang til å endre varer."
|
||||
product.refresh_from_db()
|
||||
assert product.name == "borg"
|
||||
|
||||
|
||||
def test_update_missing_product(gql_client, client, staff_with_product_access):
|
||||
client.force_login(staff_with_product_access)
|
||||
res = gql_client.query(UPDATE_PRODUCT, {"productId": "999", "data": PRODUCT_DATA})
|
||||
assert not res.errors
|
||||
assert res.data["updateProduct"]["message"] == "Fant ikke varen."
|
||||
|
||||
|
||||
# product
|
||||
# =======
|
||||
|
||||
|
||||
def test_product_query(product, gql_client):
|
||||
res = gql_client.query("query P($id: ID!) { product(productId: $id) { name } }", {"id": str(product.id)})
|
||||
assert not res.errors
|
||||
assert res.data["product"]["name"] == "borg"
|
||||
@@ -7,6 +7,7 @@ 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 ProductEdit = React.lazy(() => import("./features/inventory/ProductEdit"));
|
||||
const Log = React.lazy(() => import("./features/log/Log"));
|
||||
const Register = React.lazy(() => import("./features/register/Register"));
|
||||
const OrderStats = React.lazy(() => import("./features/stats/OrderStats"));
|
||||
@@ -22,6 +23,8 @@ export const Routes = (): React.JSX.Element => (
|
||||
<Route path="/deposit" element={<Deposit />} />
|
||||
<Route path="/inventory" element={<Inventory />} />
|
||||
<Route path="/inventory/add" element={<InventoryAdd />} />
|
||||
<Route path="/inventory/products/new" element={<ProductEdit />} />
|
||||
<Route path="/inventory/products/:productId" element={<ProductEdit />} />
|
||||
<Route path="/report" element={<InventoryReport />} />
|
||||
<Route path="/log" element={<Log />} />
|
||||
<Route path="/stats" element={<Stats />} />
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
query viewer {
|
||||
me {
|
||||
id
|
||||
firstName
|
||||
canAddProducts
|
||||
canChangeProducts
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,12 @@
|
||||
min-height: 6rem;
|
||||
}
|
||||
|
||||
.form label.checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -70,3 +76,13 @@
|
||||
min-width: 42rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Read by screen readers, not shown */
|
||||
.visuallyHidden {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { Link } from "react-router-dom";
|
||||
|
||||
import styles from "./Inventory.module.css";
|
||||
import { GQLError } from "../../components/GQLError";
|
||||
import { InventoryPageDocument } from "../../types";
|
||||
import { InventoryPageDocument, ViewerDocument } from "../../types";
|
||||
import { formatCurrency } from "../../utils/currency";
|
||||
import { formatTime } from "../../utils/time";
|
||||
|
||||
@@ -15,6 +15,8 @@ const Inventory = () => {
|
||||
const { data, loading, error } = useQuery(InventoryPageDocument, {
|
||||
variables: { limit: PAGE_SIZE, offset },
|
||||
});
|
||||
const { data: viewerData } = useQuery(ViewerDocument);
|
||||
const canChangeProducts = !!viewerData?.me?.canChangeProducts;
|
||||
|
||||
if (loading) return <div>Loading...</div>;
|
||||
if (error) return <GQLError error={error} />;
|
||||
@@ -42,6 +44,8 @@ const Inventory = () => {
|
||||
<Link className="btn-primary" to="/inventory/add">
|
||||
Registrer varetransaksjon
|
||||
</Link>
|
||||
{/* Shown to everyone; the page asks for a login, and for admin access, when needed */}
|
||||
<Link to="/inventory/products/new">Ny vare</Link>
|
||||
<Link to="/report">Åpne rapport</Link>
|
||||
</div>
|
||||
<div className={styles.tableWrap}>
|
||||
@@ -54,6 +58,11 @@ const Inventory = () => {
|
||||
<th scope="col">Verdi</th>
|
||||
<th scope="col">I salg</th>
|
||||
<th scope="col">Sist oppdatert</th>
|
||||
{canChangeProducts && (
|
||||
<th scope="col">
|
||||
<span className={styles.visuallyHidden}>Endre</span>
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -65,6 +74,11 @@ const Inventory = () => {
|
||||
<td>{formatCurrency(Number(product.wholesaleValue))} kr</td>
|
||||
<td>{product.active ? "Ja" : "Nei"}</td>
|
||||
<td>{formatTime(product.updated)}</td>
|
||||
{canChangeProducts && (
|
||||
<td>
|
||||
<Link to={`/inventory/products/${product.id}`}>Rediger</Link>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
import { useMutation, useQuery } from "@apollo/client/react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import styles from "./Inventory.module.css";
|
||||
import { GQLError } from "../../components/GQLError";
|
||||
import { useToast } from "../../hooks/useToast";
|
||||
import {
|
||||
CreateProductDocument,
|
||||
ProductFormDocument,
|
||||
ProductFormQuery,
|
||||
ProductInput,
|
||||
UpdateProductDocument,
|
||||
ViewerDocument,
|
||||
} from "../../types";
|
||||
import { loginUrl } from "../../utils/api";
|
||||
|
||||
type FormValues = {
|
||||
name: string;
|
||||
salePriceInt: string;
|
||||
salePriceExt: string;
|
||||
volumeLiter: string;
|
||||
alcoholPercent: string;
|
||||
description: string;
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
const emptyValues: FormValues = {
|
||||
name: "",
|
||||
salePriceInt: "",
|
||||
salePriceExt: "",
|
||||
volumeLiter: "",
|
||||
alcoholPercent: "",
|
||||
description: "",
|
||||
active: true,
|
||||
};
|
||||
|
||||
const toFormValues = (product: ProductFormQuery["product"]): FormValues => ({
|
||||
name: product.name,
|
||||
salePriceInt: product.salePriceInt,
|
||||
salePriceExt: product.salePriceExt,
|
||||
volumeLiter: product.volumeLiter ?? "",
|
||||
alcoholPercent: product.alcoholPercent ?? "",
|
||||
description: product.description,
|
||||
active: product.active,
|
||||
});
|
||||
|
||||
const toInput = (values: FormValues): ProductInput => ({
|
||||
...values,
|
||||
volumeLiter: values.volumeLiter || null,
|
||||
alcoholPercent: values.alcoholPercent || null,
|
||||
});
|
||||
|
||||
// Keyed by the API's field names, e.g. sale_price_int
|
||||
type FieldErrors = Record<string, string>;
|
||||
|
||||
const FieldError = ({ errors, field }: { errors: FieldErrors; field: string }) =>
|
||||
errors[field] ? <span className={styles.error}>{errors[field]}</span> : null;
|
||||
|
||||
const ProductForm = ({ productId, initialValues }: { productId?: string; initialValues: FormValues }) => {
|
||||
const navigate = useNavigate();
|
||||
const { showToast } = useToast();
|
||||
const [values, setValues] = useState(initialValues);
|
||||
const [fieldErrors, setFieldErrors] = useState<FieldErrors>({});
|
||||
const [createProduct, { loading: creating }] = useMutation(CreateProductDocument, {
|
||||
// The product lists don't know about the new product; have them load again
|
||||
update: (cache) => {
|
||||
cache.evict({ id: "ROOT_QUERY", fieldName: "allProducts" });
|
||||
cache.gc();
|
||||
},
|
||||
});
|
||||
const [updateProduct, { loading: updating }] = useMutation(UpdateProductDocument);
|
||||
const saving = creating || updating;
|
||||
|
||||
const set =
|
||||
(field: Exclude<keyof FormValues, "active">) =>
|
||||
(event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) =>
|
||||
setValues({ ...values, [field]: event.target.value });
|
||||
|
||||
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const data = toInput(values);
|
||||
try {
|
||||
const response = productId
|
||||
? (await updateProduct({ variables: { productId, data } })).data?.updateProduct
|
||||
: (await createProduct({ variables: { data } })).data?.createProduct;
|
||||
if (response?.__typename === "SaveProductSuccess") {
|
||||
showToast(`Lagret ${response.product.name}.`);
|
||||
navigate("/inventory");
|
||||
} else if (response?.__typename === "FormErrors") {
|
||||
setFieldErrors(Object.fromEntries(response.fields.map(({ field, message }) => [field, message])));
|
||||
showToast(response.message, "danger");
|
||||
} else {
|
||||
showToast("Kunne ikke lagre varen.", "danger");
|
||||
}
|
||||
} catch (mutationError) {
|
||||
showToast(mutationError instanceof Error ? mutationError.message : "Kunne ikke lagre varen.", "danger");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form className={styles.form} onSubmit={onSubmit}>
|
||||
<label>
|
||||
Navn
|
||||
<input required maxLength={64} value={values.name} onChange={set("name")} />
|
||||
<FieldError errors={fieldErrors} field="name" />
|
||||
</label>
|
||||
<label>
|
||||
Pris internt (kr)
|
||||
<input required type="number" min="0" step="0.1" value={values.salePriceInt} onChange={set("salePriceInt")} />
|
||||
<FieldError errors={fieldErrors} field="sale_price_int" />
|
||||
</label>
|
||||
<label>
|
||||
Pris eksternt (kr)
|
||||
<input required type="number" min="0" step="0.1" value={values.salePriceExt} onChange={set("salePriceExt")} />
|
||||
<FieldError errors={fieldErrors} field="sale_price_ext" />
|
||||
</label>
|
||||
<label>
|
||||
Volum (liter)
|
||||
<input required type="number" min="0" step="0.1" value={values.volumeLiter} onChange={set("volumeLiter")} />
|
||||
<FieldError errors={fieldErrors} field="volume_liter" />
|
||||
</label>
|
||||
<label>
|
||||
Alkohol (%)
|
||||
<input
|
||||
required
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.1"
|
||||
value={values.alcoholPercent}
|
||||
onChange={set("alcoholPercent")}
|
||||
/>
|
||||
<FieldError errors={fieldErrors} field="alcohol_percent" />
|
||||
</label>
|
||||
<label>
|
||||
Beskrivelse
|
||||
<textarea maxLength={256} value={values.description} onChange={set("description")} />
|
||||
<FieldError errors={fieldErrors} field="description" />
|
||||
</label>
|
||||
<label className={styles.checkbox}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={values.active}
|
||||
onChange={(event) => setValues({ ...values, active: event.target.checked })}
|
||||
/>
|
||||
I salg
|
||||
</label>
|
||||
<div className={styles.summary}>
|
||||
<button type="submit" className="primary" disabled={saving}>
|
||||
{saving ? "Lagrer..." : "Lagre"}
|
||||
</button>
|
||||
<Link to="/inventory">Avbryt</Link>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
// Adds a product, or edits one at /inventory/products/:productId. Needs the same access as the Django admin.
|
||||
const ProductEdit = () => {
|
||||
const { productId } = useParams();
|
||||
const location = useLocation();
|
||||
const { data: viewerData, loading: viewerLoading, error: viewerError } = useQuery(ViewerDocument);
|
||||
const { data, loading, error } = useQuery(ProductFormDocument, {
|
||||
variables: { productId: productId ?? "" },
|
||||
skip: !productId,
|
||||
});
|
||||
// Logged out: log in, and Django sends the browser back here afterwards
|
||||
const loggedOut = !!viewerData && !viewerData.me;
|
||||
const login = loginUrl(location.pathname);
|
||||
useEffect(() => {
|
||||
if (loggedOut) window.location.replace(login);
|
||||
}, [loggedOut, login]);
|
||||
|
||||
if (viewerLoading || loading) return <div>Loading...</div>;
|
||||
if (viewerError || error) return <GQLError error={(viewerError || error)!} />;
|
||||
|
||||
const viewer = viewerData?.me;
|
||||
const title = productId ? `Rediger ${data?.product.name ?? "vare"}` : "Ny vare";
|
||||
const hasAccess = productId ? viewer?.canChangeProducts : viewer?.canAddProducts;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>{title}</h2>
|
||||
{!viewer && (
|
||||
<p>
|
||||
Sender deg til innlogging… Skjer det ingenting, <a href={login}>logg inn her</a>.
|
||||
</p>
|
||||
)}
|
||||
{viewer && !hasAccess && (
|
||||
<p>Du har ikke tilgang til å endre varer. Be en administrator om tilgang til varer i adminpanelet.</p>
|
||||
)}
|
||||
{hasAccess && (
|
||||
<>
|
||||
<p>Beholdningen endres med varetransaksjoner, og bildet i adminpanelet.</p>
|
||||
<ProductForm
|
||||
key={productId ?? "new"}
|
||||
productId={productId}
|
||||
initialValues={data ? toFormValues(data.product) : emptyValues}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductEdit;
|
||||
@@ -0,0 +1,25 @@
|
||||
mutation createProduct($data: ProductInput!) {
|
||||
createProduct(data: $data) {
|
||||
... on SaveProductSuccess {
|
||||
product {
|
||||
id
|
||||
name
|
||||
salePriceInt
|
||||
salePriceExt
|
||||
volumeLiter
|
||||
alcoholPercent
|
||||
description
|
||||
active
|
||||
inventoryAmount
|
||||
updated
|
||||
}
|
||||
}
|
||||
... on FormErrors {
|
||||
message
|
||||
fields {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
query productForm($productId: ID!) {
|
||||
product(productId: $productId) {
|
||||
id
|
||||
name
|
||||
salePriceInt
|
||||
salePriceExt
|
||||
volumeLiter
|
||||
alcoholPercent
|
||||
description
|
||||
active
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
mutation updateProduct($productId: ID!, $data: ProductInput!) {
|
||||
updateProduct(productId: $productId, data: $data) {
|
||||
... on SaveProductSuccess {
|
||||
product {
|
||||
id
|
||||
name
|
||||
salePriceInt
|
||||
salePriceExt
|
||||
volumeLiter
|
||||
alcoholPercent
|
||||
description
|
||||
active
|
||||
inventoryAmount
|
||||
updated
|
||||
}
|
||||
}
|
||||
... on FormErrors {
|
||||
message
|
||||
fields {
|
||||
field
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,8 @@ export type SchemaInventoryTransactionOrdering =
|
||||
export type SchemaMutation = {
|
||||
__typename?: 'Mutation';
|
||||
createInventoryTransaction: SchemaCreateInventoryTransactionResponse;
|
||||
createProduct: SchemaSaveProductResponse;
|
||||
updateProduct: SchemaSaveProductResponse;
|
||||
createOrder: SchemaCreateOrderResponse;
|
||||
createDeposit: SchemaCreateDepositResponse;
|
||||
createUser: SchemaUser;
|
||||
@@ -131,6 +133,17 @@ export type SchemaMutationCreateInventoryTransactionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type SchemaMutationCreateProductArgs = {
|
||||
data: SchemaProductInput;
|
||||
};
|
||||
|
||||
|
||||
export type SchemaMutationUpdateProductArgs = {
|
||||
productId: Scalars['ID']['input'];
|
||||
data: SchemaProductInput;
|
||||
};
|
||||
|
||||
|
||||
export type SchemaMutationCreateOrderArgs = {
|
||||
customerId: Scalars['ID']['input'];
|
||||
orderLines: Array<SchemaOrderLineInput>;
|
||||
@@ -213,6 +226,9 @@ export type SchemaProduct = {
|
||||
__typename?: 'Product';
|
||||
id: Scalars['ID']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
description: Scalars['String']['output'];
|
||||
volumeLiter?: Maybe<Scalars['Decimal']['output']>;
|
||||
alcoholPercent?: Maybe<Scalars['Decimal']['output']>;
|
||||
imageUrl: Scalars['String']['output'];
|
||||
salePriceInt: Scalars['Decimal']['output'];
|
||||
salePriceExt: Scalars['Decimal']['output'];
|
||||
@@ -233,6 +249,16 @@ export type SchemaProductFilter = {
|
||||
DISTINCT?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
};
|
||||
|
||||
export type SchemaProductInput = {
|
||||
name: Scalars['String']['input'];
|
||||
salePriceInt: Scalars['Decimal']['input'];
|
||||
salePriceExt: Scalars['Decimal']['input'];
|
||||
volumeLiter?: InputMaybe<Scalars['Decimal']['input']>;
|
||||
alcoholPercent?: InputMaybe<Scalars['Decimal']['input']>;
|
||||
description?: Scalars['String']['input'];
|
||||
active?: Scalars['Boolean']['input'];
|
||||
};
|
||||
|
||||
export type SchemaProductOrderStat = {
|
||||
__typename?: 'ProductOrderStat';
|
||||
productName: Scalars['String']['output'];
|
||||
@@ -247,10 +273,13 @@ export type SchemaQuery = {
|
||||
allProducts: Array<SchemaProduct>;
|
||||
allOrders: Array<SchemaOrder>;
|
||||
orderList: Array<SchemaOrder>;
|
||||
product: SchemaProduct;
|
||||
orderStats: SchemaOrderStats;
|
||||
productStats: Array<SchemaProductOrderStat>;
|
||||
allUsers: Array<SchemaUser>;
|
||||
user: SchemaUser;
|
||||
/** The logged in user, or null when nobody is logged in */
|
||||
me?: Maybe<SchemaViewer>;
|
||||
};
|
||||
|
||||
|
||||
@@ -283,10 +312,22 @@ export type SchemaQueryOrderListArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type SchemaQueryProductArgs = {
|
||||
productId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type SchemaQueryUserArgs = {
|
||||
userId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type SchemaSaveProductResponse = SchemaSaveProductSuccess | SchemaFormErrors;
|
||||
|
||||
export type SchemaSaveProductSuccess = {
|
||||
__typename?: 'SaveProductSuccess';
|
||||
product: SchemaProduct;
|
||||
};
|
||||
|
||||
export type SchemaTopMonth = {
|
||||
__typename?: 'TopMonth';
|
||||
period: Scalars['String']['output'];
|
||||
@@ -330,6 +371,14 @@ export type SchemaUserProfile = {
|
||||
productOrderStats: Array<SchemaProductOrderStat>;
|
||||
};
|
||||
|
||||
export type SchemaViewer = {
|
||||
__typename?: 'Viewer';
|
||||
id: Scalars['ID']['output'];
|
||||
firstName: Scalars['String']['output'];
|
||||
canAddProducts: Scalars['Boolean']['output'];
|
||||
canChangeProducts: Scalars['Boolean']['output'];
|
||||
};
|
||||
|
||||
export type InventoryTransactionInput = {
|
||||
productId: string | number;
|
||||
amount: number;
|
||||
@@ -342,6 +391,16 @@ export type OrderLineInput = {
|
||||
amount: number;
|
||||
};
|
||||
|
||||
export type ProductInput = {
|
||||
name: string;
|
||||
salePriceInt: string;
|
||||
salePriceExt: string;
|
||||
volumeLiter?: string | null | undefined;
|
||||
alcoholPercent?: string | null | undefined;
|
||||
description?: string;
|
||||
active?: boolean;
|
||||
};
|
||||
|
||||
export type CreateUserMutationVariables = Exact<{
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
@@ -351,6 +410,11 @@ export type CreateUserMutationVariables = Exact<{
|
||||
|
||||
export type CreateUserMutation = { __typename: 'Mutation', createUser: { __typename: 'User', id: string, firstName: string, lastName: string, profile: { __typename: 'UserProfile', id: string } | null } };
|
||||
|
||||
export type ViewerQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ViewerQuery = { __typename: 'Query', me: { __typename: 'Viewer', id: string, firstName: string, canAddProducts: boolean, canChangeProducts: boolean } | null };
|
||||
|
||||
export type CreateDepositMutationVariables = Exact<{
|
||||
userId: string | number;
|
||||
amount: number;
|
||||
@@ -373,6 +437,16 @@ export type CreateInventoryTransactionMutation = { __typename: 'Mutation', creat
|
||||
| { __typename: 'FormErrors', message: string, fields: Array<{ __typename: 'FieldError', field: string, message: string }> }
|
||||
};
|
||||
|
||||
export type CreateProductMutationVariables = Exact<{
|
||||
data: ProductInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateProductMutation = { __typename: 'Mutation', createProduct:
|
||||
| { __typename: 'SaveProductSuccess', product: { __typename: 'Product', id: string, name: string, salePriceInt: string, salePriceExt: string, volumeLiter: string | null, alcoholPercent: string | null, description: string, active: boolean, inventoryAmount: number, updated: string } }
|
||||
| { __typename: 'FormErrors', message: string, fields: Array<{ __typename: 'FieldError', field: string, message: string }> }
|
||||
};
|
||||
|
||||
export type InventoryPageQueryVariables = Exact<{
|
||||
limit?: number | null | undefined;
|
||||
offset?: number | null | undefined;
|
||||
@@ -394,6 +468,24 @@ export type InventoryReportQueryVariables = Exact<{
|
||||
|
||||
export type InventoryReportQuery = { __typename: 'Query', inventoryReport: { __typename: 'InventoryReport', inventoryPurchases: string, productSales: string, deposits: string, orderPurchases: string, balanceAtStart: string, balanceAtEnd: string, products: Array<{ __typename: 'InventoryReportProduct', productId: string, productName: string, purchaseUnits: number, purchaseValue: string, salesUnits: number, salesValue: string }> } };
|
||||
|
||||
export type ProductFormQueryVariables = Exact<{
|
||||
productId: string | number;
|
||||
}>;
|
||||
|
||||
|
||||
export type ProductFormQuery = { __typename: 'Query', product: { __typename: 'Product', id: string, name: string, salePriceInt: string, salePriceExt: string, volumeLiter: string | null, alcoholPercent: string | null, description: string, active: boolean } };
|
||||
|
||||
export type UpdateProductMutationVariables = Exact<{
|
||||
productId: string | number;
|
||||
data: ProductInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateProductMutation = { __typename: 'Mutation', updateProduct:
|
||||
| { __typename: 'SaveProductSuccess', product: { __typename: 'Product', id: string, name: string, salePriceInt: string, salePriceExt: string, volumeLiter: string | null, alcoholPercent: string | null, description: string, active: boolean, inventoryAmount: number, updated: string } }
|
||||
| { __typename: 'FormErrors', message: string, fields: Array<{ __typename: 'FieldError', field: string, message: string }> }
|
||||
};
|
||||
|
||||
export type OrderListQueryVariables = Exact<{
|
||||
limit?: number | null | undefined;
|
||||
offset?: number | null | undefined;
|
||||
@@ -464,11 +556,15 @@ export type UserDetailQuery = { __typename: 'Query', user: { __typename: 'User',
|
||||
|
||||
|
||||
export const CreateUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateUser"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"firstName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"lastName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"email"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createUser"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"firstName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"firstName"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"lastName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"lastName"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"email"},"value":{"kind":"Variable","name":{"kind":"Name","value":"email"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"profile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode<CreateUserMutation, CreateUserMutationVariables>;
|
||||
export const ViewerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"viewer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"me"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"canAddProducts"}},{"kind":"Field","name":{"kind":"Name","value":"canChangeProducts"}}]}}]}}]} as unknown as DocumentNode<ViewerQuery, ViewerQueryVariables>;
|
||||
export const CreateDepositDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateDeposit"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"amount"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createDeposit"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"userId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}},{"kind":"Argument","name":{"kind":"Name","value":"amount"},"value":{"kind":"Variable","name":{"kind":"Name","value":"amount"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CreateDepositSuccess"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"transaction"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"profile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"balance"}}]}}]}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AboveMaxSaldo"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"maxSaldo"}},{"kind":"Field","name":{"kind":"Name","value":"above"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FormErrors"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"field"}},{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<CreateDepositMutation, CreateDepositMutationVariables>;
|
||||
export const CreateInventoryTransactionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"createInventoryTransaction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"InventoryTransactionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createInventoryTransaction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CreateInventoryTransactionSuccess"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inventoryTransaction"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"product"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"inventoryAmount"}}]}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FormErrors"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"field"}},{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<CreateInventoryTransactionMutation, CreateInventoryTransactionMutationVariables>;
|
||||
export const CreateProductDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"createProduct"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ProductInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createProduct"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SaveProductSuccess"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"product"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"salePriceInt"}},{"kind":"Field","name":{"kind":"Name","value":"salePriceExt"}},{"kind":"Field","name":{"kind":"Name","value":"volumeLiter"}},{"kind":"Field","name":{"kind":"Name","value":"alcoholPercent"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"inventoryAmount"}},{"kind":"Field","name":{"kind":"Name","value":"updated"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FormErrors"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"field"}},{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<CreateProductMutation, CreateProductMutationVariables>;
|
||||
export const InventoryPageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"inventoryPage"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"20"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"offset"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"0"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"allProducts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"inventoryAmount"}},{"kind":"Field","name":{"kind":"Name","value":"updated"}},{"kind":"Field","name":{"kind":"Name","value":"wholesaleUnitPrice"}},{"kind":"Field","name":{"kind":"Name","value":"wholesaleValue"}}]}},{"kind":"Field","name":{"kind":"Name","value":"inventoryTransactionList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"ordering"},"value":{"kind":"ListValue","values":[{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"created"},"value":{"kind":"EnumValue","value":"DESC"}}]}]}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"offset"},"value":{"kind":"Variable","name":{"kind":"Name","value":"offset"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"created"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"unitPrice"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"product"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}}]}}]}}]}}]} as unknown as DocumentNode<InventoryPageQuery, InventoryPageQueryVariables>;
|
||||
export const InventoryProductsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"inventoryProducts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"allProducts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"inventoryAmount"}}]}}]}}]} as unknown as DocumentNode<InventoryProductsQuery, InventoryProductsQueryVariables>;
|
||||
export const InventoryReportDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"inventoryReport"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"startTime"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DateTime"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"endTime"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DateTime"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"inventoryReport"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"startTime"},"value":{"kind":"Variable","name":{"kind":"Name","value":"startTime"}}},{"kind":"Argument","name":{"kind":"Name","value":"endTime"},"value":{"kind":"Variable","name":{"kind":"Name","value":"endTime"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"products"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"productId"}},{"kind":"Field","name":{"kind":"Name","value":"productName"}},{"kind":"Field","name":{"kind":"Name","value":"purchaseUnits"}},{"kind":"Field","name":{"kind":"Name","value":"purchaseValue"}},{"kind":"Field","name":{"kind":"Name","value":"salesUnits"}},{"kind":"Field","name":{"kind":"Name","value":"salesValue"}}]}},{"kind":"Field","name":{"kind":"Name","value":"inventoryPurchases"}},{"kind":"Field","name":{"kind":"Name","value":"productSales"}},{"kind":"Field","name":{"kind":"Name","value":"deposits"}},{"kind":"Field","name":{"kind":"Name","value":"orderPurchases"}},{"kind":"Field","name":{"kind":"Name","value":"balanceAtStart"}},{"kind":"Field","name":{"kind":"Name","value":"balanceAtEnd"}}]}}]}}]} as unknown as DocumentNode<InventoryReportQuery, InventoryReportQueryVariables>;
|
||||
export const ProductFormDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"productForm"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"productId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"product"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"productId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"productId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"salePriceInt"}},{"kind":"Field","name":{"kind":"Name","value":"salePriceExt"}},{"kind":"Field","name":{"kind":"Name","value":"volumeLiter"}},{"kind":"Field","name":{"kind":"Name","value":"alcoholPercent"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"active"}}]}}]}}]} as unknown as DocumentNode<ProductFormQuery, ProductFormQueryVariables>;
|
||||
export const UpdateProductDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"updateProduct"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"productId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ProductInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateProduct"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"productId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"productId"}}},{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SaveProductSuccess"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"product"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"salePriceInt"}},{"kind":"Field","name":{"kind":"Name","value":"salePriceExt"}},{"kind":"Field","name":{"kind":"Name","value":"volumeLiter"}},{"kind":"Field","name":{"kind":"Name","value":"alcoholPercent"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"inventoryAmount"}},{"kind":"Field","name":{"kind":"Name","value":"updated"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"FormErrors"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"field"}},{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}}]}}]} as unknown as DocumentNode<UpdateProductMutation, UpdateProductMutationVariables>;
|
||||
export const OrderListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"orderList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"10"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"offset"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"0"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"orderList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"ordering"},"value":{"kind":"ListValue","values":[{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"created"},"value":{"kind":"EnumValue","value":"DESC"}}]}]}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"offset"},"value":{"kind":"Variable","name":{"kind":"Name","value":"offset"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isExternal"}},{"kind":"Field","name":{"kind":"Name","value":"created"}},{"kind":"Field","name":{"kind":"Name","value":"orderSum"}},{"kind":"Field","name":{"kind":"Name","value":"customer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"profile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"balance"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"orderlines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"unitPrice"}},{"kind":"Field","name":{"kind":"Name","value":"product"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"salePriceExt"}},{"kind":"Field","name":{"kind":"Name","value":"salePriceInt"}}]}}]}}]}}]}}]} as unknown as DocumentNode<OrderListQuery, OrderListQueryVariables>;
|
||||
export const TransactionListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"transactionList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"10"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"offset"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"0"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"transactionList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"ordering"},"value":{"kind":"ListValue","values":[{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"created"},"value":{"kind":"EnumValue","value":"DESC"}}]}]}},{"kind":"Argument","name":{"kind":"Name","value":"pagination"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"offset"},"value":{"kind":"Variable","name":{"kind":"Name","value":"offset"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"created"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}},{"kind":"Field","name":{"kind":"Name","value":"profile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"balance"}}]}}]}}]}}]}}]} as unknown as DocumentNode<TransactionListQuery, TransactionListQueryVariables>;
|
||||
export const AllProductsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"allProducts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"allProducts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"imageUrl"}},{"kind":"Field","name":{"kind":"Name","value":"salePriceExt"}},{"kind":"Field","name":{"kind":"Name","value":"salePriceInt"}},{"kind":"Field","name":{"kind":"Name","value":"active"}}]}}]}}]} as unknown as DocumentNode<AllProductsQuery, AllProductsQueryVariables>;
|
||||
|
||||
@@ -101,6 +101,8 @@ scalar JSON @specifiedBy(url: "https://ecma-international.org/wp-content/uploads
|
||||
|
||||
type Mutation {
|
||||
createInventoryTransaction(data: InventoryTransactionInput!): CreateInventoryTransactionResponse!
|
||||
createProduct(data: ProductInput!): SaveProductResponse!
|
||||
updateProduct(productId: ID!, data: ProductInput!): SaveProductResponse!
|
||||
createOrder(customerId: ID!, orderLines: [OrderLineInput!]!, isExternal: Boolean! = false): CreateOrderResponse!
|
||||
createDeposit(userId: ID!, amount: Int!): CreateDepositResponse!
|
||||
createUser(input: UserInput!): User!
|
||||
@@ -166,6 +168,9 @@ type Point {
|
||||
type Product {
|
||||
id: ID!
|
||||
name: String!
|
||||
description: String!
|
||||
volumeLiter: Decimal
|
||||
alcoholPercent: Decimal
|
||||
imageUrl: String!
|
||||
salePriceInt: Decimal!
|
||||
salePriceExt: Decimal!
|
||||
@@ -186,6 +191,16 @@ input ProductFilter {
|
||||
DISTINCT: Boolean
|
||||
}
|
||||
|
||||
input ProductInput {
|
||||
name: String!
|
||||
salePriceInt: Decimal!
|
||||
salePriceExt: Decimal!
|
||||
volumeLiter: Decimal = null
|
||||
alcoholPercent: Decimal = null
|
||||
description: String! = ""
|
||||
active: Boolean! = true
|
||||
}
|
||||
|
||||
type ProductOrderStat {
|
||||
productName: String!
|
||||
data: [Point!]!
|
||||
@@ -198,10 +213,20 @@ type Query {
|
||||
allProducts(filters: ProductFilter): [Product!]!
|
||||
allOrders: [Order!]!
|
||||
orderList(ordering: [OrderOrdering!]! = [], pagination: OffsetPaginationInput): [Order!]!
|
||||
product(productId: ID!): Product!
|
||||
orderStats: OrderStats!
|
||||
productStats: [ProductOrderStat!]!
|
||||
allUsers: [User!]!
|
||||
user(userId: ID!): User!
|
||||
|
||||
"""The logged in user, or null when nobody is logged in"""
|
||||
me: Viewer
|
||||
}
|
||||
|
||||
union SaveProductResponse = SaveProductSuccess | FormErrors
|
||||
|
||||
type SaveProductSuccess {
|
||||
product: Product!
|
||||
}
|
||||
|
||||
type TopMonth {
|
||||
@@ -242,4 +267,11 @@ type UserProfile {
|
||||
orderSumTotal: Decimal!
|
||||
topMonths: [TopMonth!]!
|
||||
productOrderStats: [ProductOrderStat!]!
|
||||
}
|
||||
|
||||
type Viewer {
|
||||
id: ID!
|
||||
firstName: String!
|
||||
canAddProducts: Boolean!
|
||||
canChangeProducts: Boolean!
|
||||
}
|
||||
Reference in new issue
Block a user