Author SHA1 Message Date
Emma Eilefsen Glenna d5de4597c3 Fix: "Insufficient Funds" when user has exact amount 2024-02-15 19:36:04 +00:00
alve ff09985944 🥷 Hide users navigation tab 2024-02-15 19:29:13 +00:00
Emma Eilefsen Glenna d76c911d4b fix rendering of register total 2024-01-25 21:45:17 +01:00
Emma Eilefsen Glenna e60473e7ed fix rendering of user balance 2024-01-25 21:44:03 +01:00
Emma Eilefsen Glenna d596c31353 extract function 2024-01-25 21:43:32 +01:00
Emma Eilefsen Glenna 90558a3a28 add comment 2024-01-25 19:01:45 +00:00
alve 57a6c412d5 💄 Hide trailing decimal zeros 2024-01-25 19:01:45 +00:00
alve 19f58211e7 🗃 Make order sum a decimal field 2024-01-25 19:01:45 +00:00
nikolark 0726754678 💡 add some FIXMEs 2024-01-25 19:01:45 +00:00
nikolark 480fba870a 🗃 Replace float fields with int or decimals 2024-01-25 19:01:45 +00:00
alve ed08c60380 💄Fix favicon path 2023-07-19 19:46:01 +02:00
nikolark 79fe5cac0f 🔒 Audit 2022-04-17 21:29:02 +02:00
nikolark 95d7d13f38 ✨ Handle user not found 2022-04-17 21:13:22 +02:00
nikolark ce2cdfd409 ⚡ Code split routes 2022-04-17 16:59:00 +02:00
nikolark b56d103ae7 Merge branch 'feature/synthwave' into 'main'
✨ BBS 2222 synthwave theme

See merge request edb/bbs!3
2022-04-17 14:39:44 +00:00
29 changed files with 228 additions and 64 deletions

No files matched your search

+2 -1
View File
@@ -11,6 +11,7 @@ CACHE/
media/ media/
tmp/ tmp/
frontend/dist frontend/dist
static/modern static/assets
static/index.html
.python-version .python-version
.env .env
+1
View File
@@ -26,6 +26,7 @@ npm start
- improve product listing (light purple background) - improve product listing (light purple background)
- Stats: products over time - Stats: products over time
- inventory views - inventory views
- Stats: Implement users tab
## TODO-old ## TODO-old
+1 -1
View File
@@ -66,7 +66,7 @@ def create_order(customer_id: ID, order_lines: List[OrderLineInput], is_external
product.inventory_amount -= ol.amount product.inventory_amount -= ol.amount
product.save() product.save()
if not is_external and user.profile.balance <= order_sum: if not is_external and user.profile.balance < order_sum:
return InsufficientFunds( return InsufficientFunds(
message="Du har ikke nok penger på bok.", amount_lacking=abs(order_sum - user.profile.balance) message="Du har ikke nok penger på bok.", amount_lacking=abs(order_sum - user.profile.balance)
) )
+15 -1
View File
@@ -1,4 +1,6 @@
import strawberry import strawberry
from django.core.exceptions import ObjectDoesNotExist
from strawberry.extensions import Extension
from bbs.api.mutations.shop import ShopMutations from bbs.api.mutations.shop import ShopMutations
from bbs.api.mutations.users import UsersMutations from bbs.api.mutations.users import UsersMutations
@@ -17,4 +19,16 @@ class Mutation(ShopMutations, UsersMutations):
pass pass
schema = strawberry.Schema(query=Query, mutation=Mutation) class HandleNotFound(Extension):
def on_request_end(self):
result = self.execution_context.result
if result.errors:
for error in result.errors:
if isinstance(error.original_error, ObjectDoesNotExist):
error.extensions = {
"code": "NOT_FOUND",
"model": type(error.original_error).__qualname__.replace(".DoesNotExist", ""),
}
schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[HandleNotFound])
+2 -1
View File
@@ -1,5 +1,6 @@
from datetime import datetime from datetime import datetime
from typing import List from typing import List
from decimal import Decimal
import strawberry import strawberry
import strawberry_django import strawberry_django
@@ -25,7 +26,7 @@ class UserProfile:
balance: auto balance: auto
image: auto image: auto
last_purchase_date: datetime last_purchase_date: datetime
order_sum_total: float order_sum_total: Decimal
@strawberry.field() @strawberry.field()
def top_months(self) -> List[TopMonth]: def top_months(self) -> List[TopMonth]:
+3 -1
View File
@@ -2,9 +2,11 @@ from django import forms
from bbs.models import InventoryTransaction, Product from bbs.models import InventoryTransaction, Product
MAX_DIGITS = 9
DECIMAL_PLACES = 1
class InventoryTransactionForm(forms.ModelForm): class InventoryTransactionForm(forms.ModelForm):
unit_price = forms.FloatField(required=True, localize=True) unit_price = forms.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, required=True, localize=True)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Only show active products # Only show active products
@@ -0,0 +1,63 @@
# Generated by Django 4.0.2 on 2022-03-05 19:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bbs", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="inventorytransaction",
name="amount",
field=models.IntegerField(verbose_name="Antall"),
),
migrations.AlterField(
model_name="inventorytransaction",
name="unit_price",
field=models.DecimalField(decimal_places=1, max_digits=9, verbose_name="Enhetspris"),
),
migrations.AlterField(
model_name="orderline",
name="unit_price",
field=models.DecimalField(decimal_places=1, max_digits=9, verbose_name="Enhetspris"),
),
migrations.AlterField(
model_name="product",
name="alcohol_percent",
field=models.DecimalField(decimal_places=1, max_digits=9, null=True),
),
migrations.AlterField(
model_name="product",
name="inventory_amount",
field=models.IntegerField(default=0),
),
migrations.AlterField(
model_name="product",
name="sale_price_ext",
field=models.DecimalField(decimal_places=1, max_digits=9),
),
migrations.AlterField(
model_name="product",
name="sale_price_int",
field=models.DecimalField(decimal_places=1, max_digits=9),
),
migrations.AlterField(
model_name="product",
name="volume_liter",
field=models.DecimalField(decimal_places=1, max_digits=9, null=True),
),
migrations.AlterField(
model_name="transaction",
name="amount",
field=models.DecimalField(decimal_places=1, max_digits=9),
),
migrations.AlterField(
model_name="userprofile",
name="balance",
field=models.DecimalField(decimal_places=1, default=0, max_digits=9),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 4.0.2 on 2023-06-28 19:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bbs', '0002_alter_inventorytransaction_amount_and_more'),
]
operations = [
migrations.AlterField(
model_name='order',
name='order_sum',
field=models.DecimalField(db_column='sum', decimal_places=1, max_digits=9),
),
]
+20 -12
View File
@@ -2,6 +2,7 @@ import hashlib
from datetime import datetime from datetime import datetime
from itertools import groupby from itertools import groupby
from typing import Type from typing import Type
from decimal import Decimal
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
@@ -17,6 +18,8 @@ from django.db.models.functions import (
) )
EXTERNAL_USER = "Ekstern" EXTERNAL_USER = "Ekstern"
MAX_DIGITS = 9
DECIMAL_PLACES = 1
def _format_counts(per_user): def _format_counts(per_user):
@@ -36,14 +39,15 @@ def _format_counts(per_user):
class Product(models.Model): class Product(models.Model):
name = models.CharField(max_length=64) name = models.CharField(max_length=64)
sale_price_int = models.FloatField() sale_price_int = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES)
sale_price_ext = models.FloatField() sale_price_ext = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES)
description = models.CharField(max_length=256, blank=True) description = models.CharField(max_length=256, blank=True)
volume_liter = models.FloatField(null=True) volume_liter = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, null=True)
alcohol_percent = models.FloatField(null=True) alcohol_percent = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, null=True)
image = models.ImageField(upload_to="products", blank=True) image = models.ImageField(upload_to="products", blank=True)
customer_support = models.CharField(max_length=32, blank=True) customer_support = models.CharField(max_length=32, blank=True)
inventory_amount = models.FloatField(default=0) # FIXME: rename to inventory_quantity
inventory_amount = models.IntegerField(default=0)
active = models.BooleanField(default=True) active = models.BooleanField(default=True)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True) updated = models.DateTimeField(auto_now=True)
@@ -109,7 +113,7 @@ class Order(models.Model):
blank=True, blank=True,
related_name="orders", related_name="orders",
) )
order_sum = models.FloatField(db_column="sum") order_sum = models.DecimalField(db_column="sum", max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
objects = OrderManager() objects = OrderManager()
@@ -148,8 +152,10 @@ class OrderLineManager(models.QuerySet):
class OrderLine(models.Model): class OrderLine(models.Model):
order = models.ForeignKey("bbs.Order", on_delete=models.CASCADE, related_name="orderlines") order = models.ForeignKey("bbs.Order", on_delete=models.CASCADE, related_name="orderlines")
product = models.ForeignKey("bbs.Product", on_delete=models.CASCADE, related_name="orderlines") product = models.ForeignKey("bbs.Product", on_delete=models.CASCADE, related_name="orderlines")
# FIXME: rename to quantity
amount = models.IntegerField() amount = models.IntegerField()
unit_price = models.FloatField(verbose_name="Enhetspris") # FIXME: rename to price
unit_price = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, verbose_name="Enhetspris")
objects = OrderLineManager.as_manager() objects = OrderLineManager.as_manager()
@@ -168,7 +174,7 @@ class Transaction(models.Model):
"""Money""" """Money"""
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="transactions") user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="transactions")
amount = models.FloatField() amount = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
@@ -189,8 +195,10 @@ class InventoryTransaction(models.Model):
related_name="transactions", related_name="transactions",
verbose_name="Produkt", verbose_name="Produkt",
) )
amount = models.FloatField(verbose_name="Antall") # FIXME: rename to quantity
unit_price = models.FloatField(verbose_name="Enhetspris") amount = models.IntegerField(verbose_name="Antall")
# FIXME: rename to price
unit_price = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, verbose_name="Enhetspris")
comment = models.TextField(verbose_name="Kommentar", blank=True) comment = models.TextField(verbose_name="Kommentar", blank=True)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
@@ -204,7 +212,7 @@ class InventoryTransaction(models.Model):
class UserProfile(models.Model): class UserProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="profile") user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="profile")
balance = models.FloatField(default=0.0) balance = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, default=0)
image = models.URLField(blank=True) image = models.URLField(blank=True)
def last_purchase(self): def last_purchase(self):
@@ -227,7 +235,7 @@ class UserProfile(models.Model):
def product_order_stats(self): def product_order_stats(self):
return OrderLine.objects.filter(order__customer__pk=self.pk).product_order_stats() return OrderLine.objects.filter(order__customer__pk=self.pk).product_order_stats()
def order_sum_total(self) -> float: def order_sum_total(self) -> Decimal:
return Order.objects.filter(customer_id=self.user_id).aggregate(sum=Sum("order_sum"))["sum"] or 0 return Order.objects.filter(customer_id=self.user_id).aggregate(sum=Sum("order_sum"))["sum"] or 0
def image_url(self) -> str: def image_url(self) -> str:
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/src/assets/brick.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap" rel="stylesheet">
+6 -6
View File
@@ -8776,9 +8776,9 @@
} }
}, },
"node_modules/log-update/node_modules/ansi-regex": { "node_modules/log-update/node_modules/ansi-regex": {
"version": "3.0.0", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=4" "node": ">=4"
@@ -19172,9 +19172,9 @@
"dev": true "dev": true
}, },
"ansi-regex": { "ansi-regex": {
"version": "3.0.0", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==",
"dev": true "dev": true
}, },
"cli-cursor": { "cli-cursor": {
-1
View File
@@ -7,7 +7,6 @@ import { Routes } from "./Routes";
import client from "./client"; import client from "./client";
import { Footer } from "./components/Footer"; import { Footer } from "./components/Footer";
import { Header } from "./components/Header"; import { Header } from "./components/Header";
import { Splash as Splash } from "./components/Splash";
import { Toast } from "./components/Toast"; import { Toast } from "./components/Toast";
import { ToastProvider } from "./components/ToastProvider"; import { ToastProvider } from "./components/ToastProvider";
import { Wireframe } from "./components/Wireframe"; import { Wireframe } from "./components/Wireframe";
+24 -22
View File
@@ -1,28 +1,30 @@
import React from "react"; import React from "react";
import { Route, Routes as ReactRouterRoutes } from "react-router-dom"; import { Route, Routes as ReactRouterRoutes } from "react-router-dom";
import { Splash } from "./components/Splash"; const Splash = React.lazy(() => import("./components/Splash"));
import { UserCreate } from "./features/auth/UserCreate"; const UserCreate = React.lazy(() => import("./features/auth/UserCreate"));
import { Deposit } from "./features/deposit/Deposit"; const Deposit = React.lazy(() => import("./features/deposit/Deposit"));
import { Log } from "./features/log/Log"; const Log = React.lazy(() => import("./features/log/Log"));
import { Register } from "./features/register/Register"; const Register = React.lazy(() => import("./features/register/Register"));
import { OrderStats } from "./features/stats/OrderStats"; const OrderStats = React.lazy(() => import("./features/stats/OrderStats"));
import { ProductStats } from "./features/stats/ProductStats"; const ProductStats = React.lazy(() => import("./features/stats/ProductStats"));
import { Stats } from "./features/stats/Stats"; const Stats = React.lazy(() => import("./features/stats/Stats"));
import { UserDetail } from "./features/users/UserDetail"; const UserDetail = React.lazy(() => import("./features/users/UserDetail"));
import { UserList } from "./features/users/UserList"; const UserList = React.lazy(() => import("./features/users/UserList"));
export const Routes = (): JSX.Element => ( export const Routes = (): JSX.Element => (
<ReactRouterRoutes> <React.Suspense fallback={<div>Loading...</div>}>
<Route path="/deposit" element={<Deposit />} /> <ReactRouterRoutes>
<Route path="/log" element={<Log />} /> <Route path="/deposit" element={<Deposit />} />
<Route path="/stats" element={<Stats />} /> <Route path="/log" element={<Log />} />
<Route path="/stats/orders" element={<OrderStats />} /> <Route path="/stats" element={<Stats />} />
<Route path="/stats/products" element={<ProductStats />} /> <Route path="/stats/orders" element={<OrderStats />} />
<Route path="/users" element={<UserList />} /> <Route path="/stats/products" element={<ProductStats />} />
<Route path="/users/create" element={<UserCreate />} /> <Route path="/users" element={<UserList />} />
<Route path="/user/:userId" element={<UserDetail />} /> <Route path="/users/create" element={<UserCreate />} />
<Route path="/" element={<Register />} /> <Route path="/user/:userId" element={<UserDetail />} />
<Route path="*" element={<Splash brandText="404" noOverlay />} /> <Route path="/" element={<Register />} />
</ReactRouterRoutes> <Route path="*" element={<Splash brandText="404" noOverlay />} />
</ReactRouterRoutes>
</React.Suspense>
); );
@@ -0,0 +1,9 @@
.errorMessage {
font-family: Rocket Rinder, monospace;
font-size: 3rem;
font-style: italic;
color: var(--brand);
text-align: center;
width: 100%;
margin-top: 30vh;
}
+23
View File
@@ -0,0 +1,23 @@
import { ApolloError } from "@apollo/client";
import React from "react";
import styles from "./GQLError.module.css";
const formatModel = (text: string) => {
const withSpaces = text
.replace(/([A-Z])/g, " $1")
.toLocaleLowerCase()
.trim();
return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1);
};
export const GQLError = ({ error }: { error: ApolloError }) => {
if (error?.graphQLErrors?.length == 1) {
const single = error?.graphQLErrors[0];
if (single?.extensions?.code === "NOT_FOUND") {
const modelPretty = formatModel(single?.extensions?.model as string);
return <div className={styles.errorMessage}>{`${modelPretty} not found`}</div>;
}
}
return <div className={styles.errorMessage}>Error...</div>;
};
+2 -1
View File
@@ -8,7 +8,7 @@ interface SplashProps {
noOverlay?: boolean; noOverlay?: boolean;
} }
export const Splash = ({ brandText = "BBS", noOverlay = false }: SplashProps): JSX.Element => ( const Splash = ({ brandText = "BBS", noOverlay = false }: SplashProps): JSX.Element => (
<div className={clsx(styles.splash, { [styles.overlay]: !noOverlay })}> <div className={clsx(styles.splash, { [styles.overlay]: !noOverlay })}>
<div className={styles.wrap}> <div className={styles.wrap}>
<div className={styles.sun}></div> <div className={styles.sun}></div>
@@ -17,3 +17,4 @@ export const Splash = ({ brandText = "BBS", noOverlay = false }: SplashProps): J
</div> </div>
</div> </div>
); );
export default Splash;
+2 -1
View File
@@ -11,7 +11,7 @@ type FormValues = {
email: string; email: string;
}; };
export const UserCreate = () => { const UserCreate = () => {
const { const {
register, register,
handleSubmit, handleSubmit,
@@ -48,3 +48,4 @@ export const UserCreate = () => {
</div> </div>
); );
}; };
export default UserCreate;
+2 -1
View File
@@ -4,7 +4,7 @@ import { UserCard } from "../users/UserCard";
import styles from "./Deposit.module.css"; import styles from "./Deposit.module.css";
import { useDeposit } from "./useDeposit"; import { useDeposit } from "./useDeposit";
export const Deposit = (): JSX.Element => { const Deposit = (): JSX.Element => {
const { const {
loading, loading,
selectedUser, selectedUser,
@@ -65,3 +65,4 @@ export const Deposit = (): JSX.Element => {
</div> </div>
); );
}; };
export default Deposit;
+5 -4
View File
@@ -9,7 +9,7 @@ import styles from "./Log.module.css";
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
export const Log = () => { const Log = () => {
const types = { const types = {
orderOffset: Number, orderOffset: Number,
transactionOffset: Number, transactionOffset: Number,
@@ -51,11 +51,11 @@ export const Log = () => {
<td> <td>
{(order.orderlines ?? []).map((line) => ( {(order.orderlines ?? []).map((line) => (
<div key={line.id}> <div key={line.id}>
{line.amount}x {line.product.name} {Number(line.amount)}x {line.product.name}
</div> </div>
))} ))}
</td> </td>
<td>{order.orderSum}</td> <td>{Number(order.orderSum)}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@@ -104,7 +104,7 @@ export const Log = () => {
<td> <td>
{trans.user.firstName} {trans.user.lastName} {trans.user.firstName} {trans.user.lastName}
</td> </td>
<td>{trans.amount}</td> <td>{Number(trans.amount)}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@@ -139,3 +139,4 @@ export const Log = () => {
</div> </div>
); );
}; };
export default Log;
+2 -1
View File
@@ -8,7 +8,7 @@ import { useRegister } from "./useRegister";
const USER_EXTERNAL = "external"; const USER_EXTERNAL = "external";
export const Register = (): JSX.Element => { const Register = (): JSX.Element => {
const { const {
users, users,
products, products,
@@ -80,3 +80,4 @@ export const Register = (): JSX.Element => {
</div> </div>
); );
}; };
export default Register;
@@ -4,6 +4,7 @@ import { useState } from "react";
import { useToast } from "../../components/ToastProvider"; import { useToast } from "../../components/ToastProvider";
import { AllProductsDocument, AllUsersDocument, CreateOrderDocument, OrderLineInput, Product } from "../../types"; import { AllProductsDocument, AllUsersDocument, CreateOrderDocument, OrderLineInput, Product } from "../../types";
import { activeUserFilter } from "../../utils/users"; import { activeUserFilter } from "../../utils/users";
import {formatCurrency} from "../../utils/currency";
const USER_EXTERNAL = "external"; const USER_EXTERNAL = "external";
@@ -53,7 +54,7 @@ export const useRegister = () => {
setOrder(newOrder); setOrder(newOrder);
}; };
const total = sumTotal(products, order, selectedUser === USER_EXTERNAL); const total = formatCurrency(sumTotal(products, order, selectedUser === USER_EXTERNAL));
const onSubmit = async () => { const onSubmit = async () => {
let res; let res;
+2 -1
View File
@@ -35,7 +35,7 @@ const OrderBar = ({ data }: { data: BarDatum[] }) => (
/> />
); );
export const OrderStats = () => { const OrderStats = () => {
const { data, loading } = useQuery(OrderStatsDocument); const { data, loading } = useQuery(OrderStatsDocument);
return ( return (
@@ -77,3 +77,4 @@ export const OrderStats = () => {
</StatsView> </StatsView>
); );
}; };
export default OrderStats;
+2 -1
View File
@@ -11,7 +11,7 @@ type UserCount = {
count: number; count: number;
}; };
export const ProductStats = () => { const ProductStats = () => {
const { data, loading } = useQuery(AllProductStatsDocument, { const { data, loading } = useQuery(AllProductStatsDocument, {
variables: { active: true }, variables: { active: true },
}); });
@@ -38,3 +38,4 @@ export const ProductStats = () => {
</StatsView> </StatsView>
); );
}; };
export default ProductStats;
+2 -1
View File
@@ -5,7 +5,7 @@ import React from "react";
import { ProductStatsDocument } from "../../types"; import { ProductStatsDocument } from "../../types";
import { StatsView } from "./StatsView"; import { StatsView } from "./StatsView";
export const Stats = () => { const Stats = () => {
const { data, loading } = useQuery(ProductStatsDocument); const { data, loading } = useQuery(ProductStatsDocument);
const series: any[] = []; const series: any[] = [];
@@ -68,3 +68,4 @@ export const Stats = () => {
</StatsView> </StatsView>
); );
}; };
export default Stats;
+1 -1
View File
@@ -25,7 +25,7 @@ export const StatsNav = () => (
</NavLink> </NavLink>
</li> </li>
<li> <li>
<NavLink className="disabled" to="/stats/users" title="TODO"> <NavLink className="disabled" to="/stats/users" title="TODO" hidden={true}>
Folk Folk
</NavLink> </NavLink>
</li> </li>
+3 -1
View File
@@ -3,6 +3,7 @@ import React, { MouseEventHandler } from "react";
import { AllUsersQuery } from "../../types"; import { AllUsersQuery } from "../../types";
import styles from "./UserCard.module.css"; import styles from "./UserCard.module.css";
import {formatCurrency} from "../../utils/currency";
type UserCardProps = { type UserCardProps = {
user: AllUsersQuery["allUsers"][0] | null; user: AllUsersQuery["allUsers"][0] | null;
@@ -13,6 +14,7 @@ type UserCardProps = {
/** If user is null, then it's the special external user */ /** If user is null, then it's the special external user */
export const UserCard = ({ user, onUser, active = false }: UserCardProps): JSX.Element => { export const UserCard = ({ user, onUser, active = false }: UserCardProps): JSX.Element => {
const classes = clsx(styles.user, { [styles.active]: active }); const classes = clsx(styles.user, { [styles.active]: active });
const userBalanceString = formatCurrency(Number(user?.profile?.balance));
return ( return (
<button className={classes} onClick={onUser} type="button" title={user?.firstName || "Ekstern"}> <button className={classes} onClick={onUser} type="button" title={user?.firstName || "Ekstern"}>
<div className={styles.avatarWrap}> <div className={styles.avatarWrap}>
@@ -27,7 +29,7 @@ export const UserCard = ({ user, onUser, active = false }: UserCardProps): JSX.E
</div> </div>
<div className={styles.meta}> <div className={styles.meta}>
{user?.firstName || "Ekstern"} {user?.firstName || "Ekstern"}
<span className={styles.balance}>{user?.profile?.balance || ""}</span> <span className={styles.balance}>{userBalanceString}</span>
</div> </div>
</button> </button>
); );
+6 -2
View File
@@ -3,16 +3,19 @@ import { Line } from "@nivo/line";
import React from "react"; import React from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { GQLError } from "../../components/GQLError";
import { User, UserDetailDocument, UserProfile } from "../../types"; import { User, UserDetailDocument, UserProfile } from "../../types";
import { formatMonth, formatTime } from "../../utils/time"; import { formatMonth, formatTime } from "../../utils/time";
import styles from "./UserDetail.module.css"; import styles from "./UserDetail.module.css";
export const UserDetail = () => { const UserDetail = () => {
const { userId } = useParams(); const { userId } = useParams();
const { data, loading } = useQuery(UserDetailDocument, { variables: { userId: userId || "" }, skip: !userId }); const { data, loading, error } = useQuery(UserDetailDocument, { variables: { userId: userId || "" }, skip: !userId });
if (loading) return <div>Loading...</div>; if (loading) return <div>Loading...</div>;
if (error) return <GQLError error={error} />;
const user = data?.user as User; const user = data?.user as User;
const profile = user?.profile as UserProfile; const profile = user?.profile as UserProfile;
// FIXME: types // FIXME: types
@@ -108,3 +111,4 @@ export const UserDetail = () => {
</div> </div>
); );
}; };
export default UserDetail;
+2 -1
View File
@@ -5,7 +5,7 @@ import { Link } from "react-router-dom";
import { AllUsersDocument } from "../../types"; import { AllUsersDocument } from "../../types";
import { activeUserFilter } from "../../utils/users"; import { activeUserFilter } from "../../utils/users";
export const UserList = () => { const UserList = () => {
const { data, loading } = useQuery(AllUsersDocument); const { data, loading } = useQuery(AllUsersDocument);
const [showAll, setShowAll] = useState(false); const [showAll, setShowAll] = useState(false);
@@ -35,3 +35,4 @@ export const UserList = () => {
</div> </div>
); );
}; };
export default UserList;
+7
View File
@@ -0,0 +1,7 @@
export function formatCurrency(value: number) {
if (isNaN(value)) {
return "";
} else {
return value.toFixed(2).replace(/\.00$/, '');
}
}