Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
362de62e2f | ||
|
|
ff09985944 | ||
|
|
d76c911d4b | ||
|
|
e60473e7ed | ||
|
|
d596c31353 | ||
|
|
90558a3a28 | ||
|
|
57a6c412d5 | ||
|
|
19f58211e7 | ||
|
|
0726754678 | ||
|
|
480fba870a | ||
|
|
ed08c60380 | ||
|
|
79fe5cac0f | ||
|
|
95d7d13f38 | ||
|
|
ce2cdfd409 | ||
|
|
b56d103ae7 |
No files matched your search
+2
-1
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
+15
-1
@@ -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])
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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">
|
||||||
|
|||||||
Generated
+6
-6
@@ -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": {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
+12
-10
@@ -1,18 +1,19 @@
|
|||||||
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 => (
|
||||||
|
<React.Suspense fallback={<div>Loading...</div>}>
|
||||||
<ReactRouterRoutes>
|
<ReactRouterRoutes>
|
||||||
<Route path="/deposit" element={<Deposit />} />
|
<Route path="/deposit" element={<Deposit />} />
|
||||||
<Route path="/log" element={<Log />} />
|
<Route path="/log" element={<Log />} />
|
||||||
@@ -25,4 +26,5 @@ export const Routes = (): JSX.Element => (
|
|||||||
<Route path="/" element={<Register />} />
|
<Route path="/" element={<Register />} />
|
||||||
<Route path="*" element={<Splash brandText="404" noOverlay />} />
|
<Route path="*" element={<Splash brandText="404" noOverlay />} />
|
||||||
</ReactRouterRoutes>
|
</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;
|
||||||
|
}
|
||||||
@@ -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>;
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ import { NavLink } from "react-router-dom";
|
|||||||
import styles from "./Header.module.css";
|
import styles from "./Header.module.css";
|
||||||
|
|
||||||
const ADMIN_URL = `${import.meta.env.VITE_API_URL}/admin/`;
|
const ADMIN_URL = `${import.meta.env.VITE_API_URL}/admin/`;
|
||||||
|
const LOGOUT_URL = `${import.meta.env.VITE_API_URL}/`.replace("://", "://none:none@");
|
||||||
|
|
||||||
const navLinkStyles = ({ isActive }: { isActive: boolean }) => clsx(styles.navLink, { [styles.active]: isActive });
|
const navLinkStyles = ({ isActive }: { isActive: boolean }) => clsx(styles.navLink, { [styles.active]: isActive });
|
||||||
|
|
||||||
@@ -48,6 +49,11 @@ export const Header = (): JSX.Element => {
|
|||||||
Admin
|
Admin
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href={LOGOUT_URL} className={styles.navLink}>
|
||||||
|
Logg ut
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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,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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export function formatCurrency(value: number) {
|
||||||
|
if (isNaN(value)) {
|
||||||
|
return "";
|
||||||
|
} else {
|
||||||
|
return value.toFixed(2).replace(/\.00$/, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user