Compare commits
198
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f6fbc648c | ||
|
|
7474885f14 | ||
|
|
f3a6e563bb | ||
|
|
af45b29b1c | ||
|
|
64eb6c717c | ||
|
|
2e5fbb6fa6 | ||
|
|
158f5c5652 | ||
|
|
cfd0d94537 | ||
|
|
b5c9094b64 | ||
|
|
a47de48243 | ||
|
|
41638e605f | ||
|
|
3274cb96c0 | ||
|
|
5f332cd365 | ||
|
|
aa66694fd5 | ||
|
|
8b0aaa287e | ||
|
|
2e8b732798 | ||
|
|
87396874c9 | ||
|
|
e2a84c25a6 | ||
|
|
ec0549154d | ||
|
|
ad96873493 | ||
|
|
9fa6cfe0ee | ||
|
|
a9f81737d5 | ||
|
|
5f7a4e6ff2 | ||
|
|
d5ed841c9c | ||
|
|
9c7fe11bfc | ||
|
|
df3aff7fb2 | ||
|
|
264e189698 | ||
|
|
7ba3ead688 | ||
|
|
d7513f389e | ||
|
|
bd8dbd750a | ||
|
|
7aea1ead45 | ||
|
|
771e0c2a3b | ||
|
|
6fa6bf1334 | ||
|
|
9e80669a1f | ||
|
|
aead00a893 | ||
|
|
e240eda1cb | ||
|
|
1bd8fddf73 | ||
|
|
ed99dadcb0 | ||
|
|
87e2f2292c | ||
|
|
a2cfd0ceaf | ||
|
|
85698deba5 | ||
|
|
1bd06b7e81 | ||
|
|
6791000dea | ||
|
|
d3016a3925 | ||
|
|
717752ddae | ||
|
|
dcd8edaaa6 | ||
|
|
9d3afadbff | ||
|
|
549b0cfdd6 | ||
|
|
35d4334627 | ||
|
|
5969e70e89 | ||
|
|
db7bdd7181 | ||
|
|
dbda43077e | ||
|
|
bc4414473c | ||
|
|
2bcc993aaa | ||
|
|
3d93a645ad | ||
|
|
40b3cb119f | ||
|
|
9576becb62 | ||
|
|
cf9c23bf7b | ||
|
|
576392ba5f | ||
|
|
0a435d3224 | ||
|
|
ed82ca3891 | ||
|
|
c44e582706 | ||
|
|
d5bff90ee3 | ||
|
|
d86afd9392 | ||
|
|
350d31d8d8 | ||
|
|
46c52b7618 | ||
|
|
2ed0c57b97 | ||
|
|
f0656b34e9 | ||
|
|
fa89353525 | ||
|
|
e2db9052bc | ||
|
|
aef0509056 | ||
|
|
0e4ef1b75f | ||
|
|
49870595b0 | ||
|
|
799548a90b | ||
|
|
2a3188667f | ||
|
|
a052397990 | ||
|
|
9c4a5b3d84 | ||
|
|
1a27e3a521 | ||
|
|
d311e65058 | ||
|
|
a1437ab5d2 | ||
|
|
079af129d7 | ||
|
|
90eae21bc7 | ||
|
|
cd11bd580b | ||
|
|
72562c931d | ||
|
|
a84c94e23b | ||
|
|
16bc430369 | ||
|
|
8fb2389d2c | ||
|
|
c1ef6f6678 | ||
|
|
f0e6331630 | ||
|
|
505b9d73d5 | ||
|
|
2100408b72 | ||
|
|
8a4a9fcb26 | ||
|
|
21c4f11ebc | ||
|
|
35835fc64f | ||
|
|
6e11410972 | ||
|
|
63f0056898 | ||
|
|
e260b3d5bd | ||
|
|
17545b5261 | ||
|
|
6560d3e89b | ||
|
|
31cffdb24b | ||
|
|
2b5c3529d6 | ||
|
|
195c49b0fd | ||
|
|
ff72b8dd0b | ||
|
|
3b7980ffe9 | ||
|
|
de85988039 | ||
|
|
ec638d51f9 | ||
|
|
b69f917d57 | ||
|
|
42e25f57a4 | ||
|
|
3bb9040797 | ||
|
|
b47ee8f832 | ||
|
|
8c004276a7 | ||
|
|
0296617746 | ||
|
|
a858158316 | ||
|
|
52d327ec83 | ||
|
|
49dfec0c70 | ||
|
|
44d76dae0b | ||
|
|
cf560a8d3d | ||
|
|
8d2e0ea396 | ||
|
|
4488acdfcb | ||
|
|
14c57bf9eb | ||
|
|
292235858e | ||
|
|
c7fb6102b0 | ||
|
|
f8a94b7bee | ||
|
|
fc65e15d98 | ||
|
|
acae7f51fe | ||
|
|
8d30585cdb | ||
|
|
3727f1c14a | ||
|
|
a2e7c595c1 | ||
|
|
d8a750151b | ||
|
|
9641b66346 | ||
|
|
ea7174815b | ||
|
|
1a68e4b95b | ||
|
|
4b3ffeda3c | ||
|
|
85218c6120 | ||
|
|
18012d081a | ||
|
|
1bc3e5983d | ||
|
|
32e41c05ae | ||
|
|
09d41ba682 | ||
|
|
36c7192d73 | ||
|
|
a6faa4ff96 | ||
|
|
efc1e39692 | ||
|
|
dac8319252 | ||
|
|
532cab2c6a | ||
|
|
cb53489f86 | ||
|
|
921dc92b07 | ||
|
|
0e7cb37503 | ||
|
|
e071c9f360 | ||
|
|
8aed5ace21 | ||
|
|
2ef30085d6 | ||
|
|
d02d22f52d | ||
|
|
52629901e7 | ||
|
|
9864acb26b | ||
|
|
7b042ee9fb | ||
|
|
4652736f72 | ||
|
|
75001f0723 | ||
|
|
e079ad685f | ||
|
|
2295455375 | ||
|
|
00959c73b1 | ||
|
|
b7b0238871 | ||
|
|
7f84c936ab | ||
|
|
3bb521db39 | ||
|
|
fa4f8b1f05 | ||
|
|
ca1b25afd9 | ||
|
|
3ae6a0675a | ||
|
|
3e11ea9b75 | ||
|
|
368228e5fa | ||
|
|
32bb4d94ab | ||
|
|
305e9888a0 | ||
|
|
e0936bb64e | ||
|
|
9854a17f4e | ||
|
|
3229d54365 | ||
|
|
598e2a8e57 | ||
|
|
d112e1b195 | ||
|
|
da554ff9da | ||
|
|
3b2389094d | ||
|
|
f1d7516341 | ||
|
|
edf1426ed2 | ||
|
|
cfb1074277 | ||
|
|
a93e9178dc | ||
|
|
07f37e57df | ||
|
|
14f4a9ec3d | ||
|
|
0cbda8e0c5 | ||
|
|
b9db82e31f | ||
|
|
99a8a3aeb4 | ||
|
|
c82a76d4ce | ||
|
|
11a18b6aba | ||
|
|
9f1911289a | ||
|
|
ce5b36ec37 | ||
|
|
8cdddefacc | ||
|
|
85071ba6af | ||
|
|
c05e8f9aa4 | ||
|
|
e7d499aeca | ||
|
|
54ec001fb1 | ||
|
|
fc400dffd4 | ||
|
|
ce8190c870 | ||
|
|
8dd1fd6573 | ||
|
|
2dd7bb6224 | ||
|
|
c634d16ee1 |
No files matched your search
+12
-4
@@ -1,6 +1,14 @@
|
|||||||
*.swp
|
*.swp
|
||||||
*.pyc
|
*.pyc
|
||||||
settings.py
|
local_settings.py
|
||||||
dev.db
|
db.sqlite3
|
||||||
static/gaute/.sass-cache
|
static/.sass-cache
|
||||||
uploads
|
uploads/[^.]*
|
||||||
|
venv/
|
||||||
|
node_modules/
|
||||||
|
.idea/
|
||||||
|
CACHE/
|
||||||
|
media/
|
||||||
|
tmp/
|
||||||
|
frontend/dist
|
||||||
|
static/modern
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
variables:
|
||||||
|
PIP_CACHE_DIR: "${CI_PROJECT_DIR}/.cache/pip"
|
||||||
|
|
||||||
|
default:
|
||||||
|
image: nikolaik/python-nodejs:python3.9-nodejs16
|
||||||
|
cache:
|
||||||
|
paths:
|
||||||
|
- .cache/pip
|
||||||
|
- .venv
|
||||||
|
before_script:
|
||||||
|
- poetry config virtualenvs.in-project true
|
||||||
|
- poetry install
|
||||||
|
|
||||||
|
django-checks:
|
||||||
|
stage: build
|
||||||
|
script:
|
||||||
|
- poetry run python manage.py migrate
|
||||||
|
- poetry run python manage.py check
|
||||||
|
|
||||||
|
tests:
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- poetry run bin/test
|
||||||
|
|
||||||
|
lint:
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- poetry run bin/lint
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
stage: deploy
|
||||||
|
script:
|
||||||
|
- ssh gitdeploy@dreamcast.neuf.no "$(< bin/deploy)"
|
||||||
|
only:
|
||||||
|
- main
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# See https://pre-commit.com for more information
|
||||||
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
|
repos:
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
name: black
|
||||||
|
entry: black
|
||||||
|
args: [--check]
|
||||||
|
language: system
|
||||||
|
require_serial: true
|
||||||
|
types: [python]
|
||||||
|
- id: isort
|
||||||
|
name: isort
|
||||||
|
entry: isort
|
||||||
|
args: [--check]
|
||||||
|
language: system
|
||||||
|
types: [python]
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
TODO:
|
|
||||||
- Prototype register GUI.
|
|
||||||
- Show picture of buyers and username, with balance > 0.
|
|
||||||
- Submit orders
|
|
||||||
- User profile settings
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Getting started
|
||||||
|
|
||||||
|
Before starting, make sure you have a working python and nodejs setup with pyenv and nvm.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apt-get install libmysqlclient-dev
|
||||||
|
poetry install
|
||||||
|
poetry shell
|
||||||
|
pre-commit install
|
||||||
|
python manage.py migrate
|
||||||
|
bin/run
|
||||||
|
|
||||||
|
# in another terminal
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- We use JS code generation to work with queries/mutations in our graphql API: https://github.com/dotansimha/graphql-typed-document-node
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- update emails
|
||||||
|
- Stats: products over time
|
||||||
|
- Light on dark colors
|
||||||
|
- inventory views
|
||||||
|
|
||||||
|
|
||||||
|
## TODO-old
|
||||||
|
|
||||||
|
- Larger pictures
|
||||||
|
- Compact GUI for max 768 vertical pixels
|
||||||
|
- Search right of in title
|
||||||
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
|
|
||||||
# Create your models here.
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
"""
|
|
||||||
This file demonstrates writing tests using the unittest module. These will pass
|
|
||||||
when you run "manage.py test".
|
|
||||||
|
|
||||||
Replace this with more appropriate tests for your application.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleTest(TestCase):
|
|
||||||
def test_basic_addition(self):
|
|
||||||
"""
|
|
||||||
Tests that 1 + 1 always equals 2.
|
|
||||||
"""
|
|
||||||
self.assertEqual(1 + 1, 2)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# Create your views here.
|
|
||||||
Executable → Regular
File renamed without changes.
Executable
+146
@@ -0,0 +1,146 @@
|
|||||||
|
# Django settings for bbs project.
|
||||||
|
import os
|
||||||
|
|
||||||
|
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ADMINS = (
|
||||||
|
# ('Your Name', 'your_email@example.com'),
|
||||||
|
)
|
||||||
|
MANAGERS = ADMINS
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": "db.sqlite3",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
TIME_ZONE = "Europe/Oslo"
|
||||||
|
LANGUAGE_CODE = "nb"
|
||||||
|
USE_I18N = True
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
SITE_ID = 1
|
||||||
|
|
||||||
|
# Media and uploads
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
||||||
|
MEDIA_URL = "/media/"
|
||||||
|
|
||||||
|
# Static files
|
||||||
|
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")
|
||||||
|
STATIC_URL = "/static/"
|
||||||
|
STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),)
|
||||||
|
|
||||||
|
STATICFILES_FINDERS = (
|
||||||
|
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||||
|
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make this unique, and don't share it with anybody.
|
||||||
|
SECRET_KEY = "osv$uy-ea$*%lc)+xiwt5@2b7au%u*c!pa6hmh#vl)*+tp%fss"
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [
|
||||||
|
os.path.join(BASE_DIR, "templates"),
|
||||||
|
],
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.template.context_processors.debug",
|
||||||
|
"django.template.context_processors.i18n",
|
||||||
|
"django.template.context_processors.media",
|
||||||
|
"django.template.context_processors.static",
|
||||||
|
"django.template.context_processors.tz",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"corsheaders.middleware.CorsMiddleware",
|
||||||
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = "bbssite.urls"
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.sites",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django_extensions",
|
||||||
|
"easy_thumbnails",
|
||||||
|
"corsheaders",
|
||||||
|
]
|
||||||
|
LOCAL_APPS = [
|
||||||
|
"main",
|
||||||
|
]
|
||||||
|
INSTALLED_APPS += LOCAL_APPS
|
||||||
|
|
||||||
|
LOGIN_REDIRECT_URL = "/"
|
||||||
|
|
||||||
|
SESSION_COOKIE_AGE = 1209600 # 15 minutes
|
||||||
|
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
||||||
|
|
||||||
|
BBS_SALDO_MAX = 600
|
||||||
|
BBS_LIMIT_DEPOSITS = False
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
THUMBNAIL_ALIASES = {
|
||||||
|
"": {
|
||||||
|
"product": {"size": (64, 64), "crop": True},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGING = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"root": {
|
||||||
|
"level": "INFO",
|
||||||
|
"handlers": ["console"],
|
||||||
|
},
|
||||||
|
"formatters": {
|
||||||
|
"verbose": {"format": "%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s"},
|
||||||
|
},
|
||||||
|
"handlers": {
|
||||||
|
"console": {
|
||||||
|
"level": "DEBUG",
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"formatter": "verbose",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"loggers": {
|
||||||
|
"django.db.backends": {
|
||||||
|
"level": "ERROR",
|
||||||
|
"handlers": ["console"],
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||||
|
|
||||||
|
CORS_ALLOWED_ORIGINS = [
|
||||||
|
"https://bbs.neuf.no",
|
||||||
|
"http://localhost:8000",
|
||||||
|
"http://localhost:3000",
|
||||||
|
]
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .local_settings import *
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
Executable
+22
@@ -0,0 +1,22 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.views import LoginView, LogoutView
|
||||||
|
from django.urls import include, path, re_path
|
||||||
|
from strawberry.django.views import GraphQLView
|
||||||
|
|
||||||
|
from main import urls as main_urls
|
||||||
|
from main.api.schema import schema
|
||||||
|
|
||||||
|
admin.autodiscover()
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
re_path(r"", include(main_urls)),
|
||||||
|
path("accounts/login/", LoginView.as_view(), name="django.contrib.auth.views.login"),
|
||||||
|
path("accounts/logout/", LogoutView.as_view(), name="django.contrib.auth.views.logout"),
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
path("graphql/", GraphQLView.as_view(schema=schema), name="graphql"),
|
||||||
|
]
|
||||||
|
|
||||||
|
urlpatterns += static("/media", document_root=settings.MEDIA_ROOT)
|
||||||
|
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bbssite.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
PROJECT_PATH=/var/www/neuf.no/bbs
|
||||||
|
PROCESS_NAME="$PROJECT.neuf.no"
|
||||||
|
|
||||||
|
cd "$PROJECT_PATH" || exit 1
|
||||||
|
git pull
|
||||||
|
poetry install
|
||||||
|
umask 022; poetry run python manage.py collectstatic --noinput # Collect static
|
||||||
|
poetry run python manage.py migrate # Run DB migrations
|
||||||
|
|
||||||
|
# Reload
|
||||||
|
sudo /usr/bin/supervisorctl pid "$PROCESS_NAME" | xargs kill -HUP
|
||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
SRC_DIR="main bbssite"
|
||||||
|
|
||||||
|
black $SRC_DIR
|
||||||
|
isort $SRC_DIR
|
||||||
Executable
+4
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
python manage.py export_schema
|
||||||
|
cd frontend && npm run generate
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
SRC_DIR="main bbssite"
|
||||||
|
|
||||||
|
black --check $SRC_DIR
|
||||||
|
isort --diff $SRC_DIR
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
cd frontend && npm run start
|
||||||
-15
@@ -1,15 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
from os.path import dirname, join
|
|
||||||
|
|
||||||
def map_path(target_name=''):
|
|
||||||
'''Enables path names to be decided at runtime.'''
|
|
||||||
return join(dirname(__file__), target_name).replace('\\', '/')
|
|
||||||
|
|
||||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
|
|
||||||
|
|
||||||
path = map_path(".")
|
|
||||||
if path not in sys.path:
|
|
||||||
sys.path.append(path)
|
|
||||||
import django.core.handlers.wsgi
|
|
||||||
application = django.core.handlers.wsgi.WSGIHandler()
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
image: mysql
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: dev
|
||||||
|
MYSQL_DATABASE: bbs
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- "3306:3306"
|
||||||
|
volumes:
|
||||||
|
- mydata:/var/lib/mysql
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mydata:
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
src/types.ts
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
},
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:import/errors",
|
||||||
|
"plugin:import/warnings",
|
||||||
|
"plugin:import/typescript",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"no-console": "off",
|
||||||
|
"prefer-destructuring": "off",
|
||||||
|
"import/no-extraneous-dependencies": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
devDependencies: ["**/*.test.js", "**/*.spec.js", "**/__tests__/**"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"import/prefer-default-export": "off",
|
||||||
|
"import/order": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
groups: ["builtin", "external", "internal", ["parent", "sibling", "index"]],
|
||||||
|
"newlines-between": "always",
|
||||||
|
alphabetize: { order: "asc" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"react-hooks/exhaustive-deps": "error",
|
||||||
|
"react-hooks/rules-of-hooks": "error",
|
||||||
|
"react/button-has-type": "error",
|
||||||
|
"react/jsx-filename-extension": ["warn", { extensions: [".js", ".jsx", ".ts", ".tsx"] }],
|
||||||
|
"react/jsx-uses-react": "off",
|
||||||
|
"react/prefer-stateless-function": "warn",
|
||||||
|
"react/prop-types": "off",
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
|
"react/sort-comp": "off",
|
||||||
|
},
|
||||||
|
plugins: ["prettier", "react", "react-hooks"],
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: "detect",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "bbs-schema",
|
||||||
|
"schemaPath": "../schema.graphql",
|
||||||
|
"extensions": {
|
||||||
|
"endpoints": {
|
||||||
|
"default": {
|
||||||
|
"url": "http://localhost:8000/graphql/",
|
||||||
|
"introspect": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
engine-strict=true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
v16
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
schema: ../schema.graphql
|
||||||
|
documents: ./src/**/*.graphql
|
||||||
|
generates:
|
||||||
|
./src/types.ts:
|
||||||
|
plugins:
|
||||||
|
- typescript
|
||||||
|
- typescript-operations
|
||||||
|
- typed-document-node
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>BBS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+22024
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"name": "bbs",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"check-types": "tsc",
|
||||||
|
"dev": "vite",
|
||||||
|
"format:js": "eslint --fix src",
|
||||||
|
"format:styles": "stylelint --fix src",
|
||||||
|
"generate": "graphql-codegen",
|
||||||
|
"lint:js": "eslint src",
|
||||||
|
"lint:styles": "stylelint src",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"start": "vite",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@apollo/client": "^3.5.8",
|
||||||
|
"@nivo/bar": "^0.79.1",
|
||||||
|
"@nivo/line": "^0.79.1",
|
||||||
|
"@nivo/pie": "^0.79.1",
|
||||||
|
"bootstrap": "^5.1.3",
|
||||||
|
"bootstrap-icons": "^1.8.1",
|
||||||
|
"clsx": "^1.1.1",
|
||||||
|
"date-fns": "^2.28.0",
|
||||||
|
"graphql": "^16.3.0",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"react-hook-form": "^7.27.1",
|
||||||
|
"react-router-dom": "^6.2.1",
|
||||||
|
"use-url-search-params": "^2.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@graphql-codegen/cli": "^2.6.1",
|
||||||
|
"@graphql-codegen/typed-document-node": "^2.2.3",
|
||||||
|
"@graphql-codegen/typescript-operations": "^2.3.0",
|
||||||
|
"@graphql-typed-document-node/core": "^3.1.1",
|
||||||
|
"@types/bootstrap": "^5.1.9",
|
||||||
|
"@types/react": "^17.0.39",
|
||||||
|
"@types/react-dom": "^17.0.11",
|
||||||
|
"@types/react-router-dom": "^5.3.3",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.11.0",
|
||||||
|
"@typescript-eslint/parser": "^5.11.0",
|
||||||
|
"@vitejs/plugin-react": "^1.2.0",
|
||||||
|
"@vitejs/plugin-react-refresh": "^1.3.6",
|
||||||
|
"eslint": "^8.9.0",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-import": "^2.25.4",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"eslint-plugin-react": "^7.28.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.3.0",
|
||||||
|
"jest": "^27.5.1",
|
||||||
|
"prettier": "^2.5.1",
|
||||||
|
"stylelint": "^14.5.0",
|
||||||
|
"stylelint-config-idiomatic-order": "^8.1.0",
|
||||||
|
"stylelint-config-prettier": "^9.0.3",
|
||||||
|
"stylelint-config-recommended": "^7.0.0",
|
||||||
|
"stylelint-order": "^5.0.0",
|
||||||
|
"stylelint-prettier": "^2.0.0",
|
||||||
|
"typescript": "^4.5.5",
|
||||||
|
"vite": "^2.8.1",
|
||||||
|
"vite-plugin-svgr": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16",
|
||||||
|
"npm": ">=7"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.app {
|
||||||
|
margin: 16px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { ApolloProvider } from "@apollo/client";
|
||||||
|
import React from "react";
|
||||||
|
import { BrowserRouter as Router } from "react-router-dom";
|
||||||
|
import "bootstrap/dist/css/bootstrap.min.css";
|
||||||
|
|
||||||
|
import styles from "./App.module.css";
|
||||||
|
import { Routes } from "./Routes";
|
||||||
|
import client from "./client";
|
||||||
|
import { Header } from "./components/Header";
|
||||||
|
import { Toast } from "./components/Toast";
|
||||||
|
import { ToastProvider } from "./components/ToastProvider";
|
||||||
|
|
||||||
|
const App: React.FC = () => (
|
||||||
|
<ApolloProvider client={client}>
|
||||||
|
<Router>
|
||||||
|
<ToastProvider>
|
||||||
|
<Header />
|
||||||
|
<div className={styles.app}>
|
||||||
|
<Routes />
|
||||||
|
</div>
|
||||||
|
<Toast />
|
||||||
|
</ToastProvider>
|
||||||
|
</Router>
|
||||||
|
</ApolloProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Route, Routes as ReactRouterRoutes } from "react-router-dom";
|
||||||
|
|
||||||
|
import { UserCreate } from "./features/auth/UserCreate";
|
||||||
|
import { Deposit } from "./features/deposit/Deposit";
|
||||||
|
import { Log } from "./features/log/Log";
|
||||||
|
import { Register } from "./features/register/Register";
|
||||||
|
import { OrderStats } from "./features/stats/OrderStats";
|
||||||
|
import { ProductStats } from "./features/stats/ProductStats";
|
||||||
|
import { Stats } from "./features/stats/Stats";
|
||||||
|
import { UserDetail } from "./features/users/UserDetail";
|
||||||
|
import { UserList } from "./features/users/UserList";
|
||||||
|
|
||||||
|
export const Routes: React.FC = () => (
|
||||||
|
<ReactRouterRoutes>
|
||||||
|
<Route path="/deposit" element={<Deposit />} />
|
||||||
|
<Route path="/log" element={<Log />} />
|
||||||
|
<Route path="/stats" element={<Stats />} />
|
||||||
|
<Route path="/stats/orders" element={<OrderStats />} />
|
||||||
|
<Route path="/stats/products" element={<ProductStats />} />
|
||||||
|
<Route path="/users" element={<UserList />} />
|
||||||
|
<Route path="/users/create" element={<UserCreate />} />
|
||||||
|
<Route path="/user/:userId" element={<UserDetail />} />
|
||||||
|
<Route path="/" element={<Register />} />
|
||||||
|
</ReactRouterRoutes>
|
||||||
|
);
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="g5" transform="matrix(1.301558, 0, 0, 1.301558, -16.927717, 11.280891)" style="">
|
||||||
|
<g id="g7">
|
||||||
|
<g id="g9">
|
||||||
|
<rect x="48.799" y="12.333" width="27.089001" height="27.089001" id="rect11" style="fill:#fed600"/>
|
||||||
|
</g>
|
||||||
|
<defs id="defs13">
|
||||||
|
<filter id="Adobe_OpacityMaskFilter" filterUnits="userSpaceOnUse" x="48.799" y="12.333" width="27.089001" height="27.089001">
|
||||||
|
<feFlood result="back" id="feFlood16"/>
|
||||||
|
<feBlend in="SourceGraphic" in2="back" mode="normal" id="feBlend18"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<mask maskUnits="userSpaceOnUse" x="48.799" y="12.333" width="27.089" height="27.089" id="SVGID_1_">
|
||||||
|
<g id="g21" style="filter:url(#Adobe_OpacityMaskFilter)">
|
||||||
|
<image overflow="visible" width="130" height="130" transform="matrix(0.24,0,0,0.24,46.7988,10.3325)" id="image23" style="overflow:visible" xlink:href="data:image/jpeg;base64,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 FX+uRPy5Prky5c/8H//Z"/>
|
||||||
|
</g>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#SVGID_1_)" id="g25" style="opacity:0.46000001">
|
||||||
|
<rect x="48.799" y="12.333" width="27.089001" height="27.089001" id="rect27" style="fill:#ffffff"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g id="g29">
|
||||||
|
<g id="g31">
|
||||||
|
<rect x="48.799" y="41.158001" width="27.089001" height="27.089001" id="rect33" style="fill:#fed600"/>
|
||||||
|
</g>
|
||||||
|
<defs id="defs35">
|
||||||
|
<filter id="Adobe_OpacityMaskFilter_1_" filterUnits="userSpaceOnUse" x="48.799" y="41.158001" width="27.089001" height="27.089001">
|
||||||
|
<feFlood result="back" id="feFlood38"/>
|
||||||
|
<feBlend in="SourceGraphic" in2="back" mode="normal" id="feBlend40"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<mask maskUnits="userSpaceOnUse" x="48.799" y="41.158" width="27.089" height="27.089" id="SVGID_2_">
|
||||||
|
<g id="g43" style="filter:url(#Adobe_OpacityMaskFilter_1_)">
|
||||||
|
<image overflow="visible" width="130" height="130" transform="matrix(0.24,0,0,0.24,46.7988,39.1582)" id="image45" style="overflow:visible" xlink:href="data:image/jpeg;base64,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 uztZ9R//2Q=="/>
|
||||||
|
</g>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#SVGID_2_)" id="g47" style="opacity:0.46000001">
|
||||||
|
<rect x="48.799" y="41.158001" width="27.089001" height="27.089001" id="rect49" style="fill:#ffffff"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g id="g51">
|
||||||
|
<g id="g53">
|
||||||
|
<rect x="78.223" y="41.158001" width="27.089001" height="27.089001" id="rect55" style="fill:#fed600"/>
|
||||||
|
</g>
|
||||||
|
<defs id="defs57">
|
||||||
|
<filter id="Adobe_OpacityMaskFilter_2_" filterUnits="userSpaceOnUse" x="78.223" y="41.158001" width="27.089001" height="27.089001">
|
||||||
|
<feFlood result="back" id="feFlood60"/>
|
||||||
|
<feBlend in="SourceGraphic" in2="back" mode="normal" id="feBlend62"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<mask maskUnits="userSpaceOnUse" x="78.223" y="41.158" width="27.089" height="27.089" id="SVGID_3_">
|
||||||
|
<g id="g65" style="filter:url(#Adobe_OpacityMaskFilter_2_)">
|
||||||
|
<image overflow="visible" width="130" height="130" transform="matrix(0.24,0,0,0.24,76.2231,39.1582)" id="image67" style="overflow:visible" xlink:href="data:image/jpeg;base64,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"/>
|
||||||
|
</g>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#SVGID_3_)" id="g69" style="opacity:0.46000001">
|
||||||
|
<rect x="78.223" y="41.158001" width="27.089001" height="27.089001" id="rect71" style="fill:#ffffff"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g id="g73">
|
||||||
|
<g id="g75">
|
||||||
|
<rect x="19.657" y="12.333" width="27.091" height="27.089001" id="rect77" style="fill:#fed600"/>
|
||||||
|
</g>
|
||||||
|
<defs id="defs79">
|
||||||
|
<filter id="Adobe_OpacityMaskFilter_3_" filterUnits="userSpaceOnUse" x="19.657" y="12.333" width="27.091" height="27.089001">
|
||||||
|
<feFlood result="back" id="feFlood82"/>
|
||||||
|
<feBlend in="SourceGraphic" in2="back" mode="normal" id="feBlend84"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<mask maskUnits="userSpaceOnUse" x="19.657" y="12.333" width="27.091" height="27.089" id="SVGID_4_">
|
||||||
|
<g id="g87" style="filter:url(#Adobe_OpacityMaskFilter_3_)">
|
||||||
|
<image overflow="visible" width="130" height="130" transform="matrix(0.24,0,0,0.24,17.6567,10.3325)" id="image89" style="overflow:visible" xlink:href="data:image/jpeg;base64,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 Z9L/AErWzlbXa7CGrtNXV1fznz5/wf/Z"/>
|
||||||
|
</g>
|
||||||
|
</mask>
|
||||||
|
<g mask="url(#SVGID_4_)" id="g91" style="opacity:0.46000001">
|
||||||
|
<rect x="19.657" y="12.333" width="27.091" height="27.089001" id="rect93" style="fill:#ffffff"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<rect x="18.633" y="79.66" fill="none" width="615.332" height="68.885" id="rect95"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
import { ApolloClient, InMemoryCache } from "@apollo/client";
|
||||||
|
|
||||||
|
const client = new ApolloClient({
|
||||||
|
uri: import.meta.env.VITE_API_URL || "http://localhost:8000/graphql/",
|
||||||
|
cache: new InMemoryCache(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default client;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
.logo {
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
display: inline-block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin: 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navIcon {
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { ReactComponent as BarChart } from "bootstrap-icons/icons/bar-chart.svg";
|
||||||
|
import { ReactComponent as CardList } from "bootstrap-icons/icons/card-list.svg";
|
||||||
|
import { ReactComponent as House } from "bootstrap-icons/icons/house.svg";
|
||||||
|
import { ReactComponent as PeopleIcon } from "bootstrap-icons/icons/people.svg";
|
||||||
|
import React from "react";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
|
||||||
|
import { ReactComponent as Logo } from "../assets/brick.svg";
|
||||||
|
import styles from "./Header.module.css";
|
||||||
|
|
||||||
|
const ADMIN_URL = import.meta.env.VITE_API_URL
|
||||||
|
? `${import.meta.env.VITE_API_URL}/admin`
|
||||||
|
: "http://localhost:8000/admin/";
|
||||||
|
|
||||||
|
const navLinkStyles = ({ isActive }: { isActive: boolean }) => (isActive ? "nav-link active" : "nav-link");
|
||||||
|
|
||||||
|
export const Header: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<header>
|
||||||
|
<nav className="navbar navbar-dark bg-dark navbar-expand-md">
|
||||||
|
<NavLink to="/" className="navbar-brand">
|
||||||
|
<span className={styles.logo}>
|
||||||
|
<Logo />
|
||||||
|
</span>
|
||||||
|
Biceps Bar System - BBS
|
||||||
|
</NavLink>
|
||||||
|
<button
|
||||||
|
className="navbar-toggler"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#navbarNav"
|
||||||
|
aria-controls="navbarNav"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-label="Toggle navigation"
|
||||||
|
>
|
||||||
|
<span className="navbar-toggler-icon" />
|
||||||
|
</button>
|
||||||
|
<div className="collapse navbar-collapse" id="navbarNav">
|
||||||
|
<ul className="navbar-nav">
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink to="/" className={navLinkStyles} end>
|
||||||
|
<span className={styles.navIcon}>
|
||||||
|
<House />
|
||||||
|
</span>{" "}
|
||||||
|
Kjøp drikke
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink to="/deposit" className={navLinkStyles}>
|
||||||
|
Sett inn penger
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink to="/log" className={navLinkStyles}>
|
||||||
|
<span className={styles.navIcon}>
|
||||||
|
<CardList />
|
||||||
|
</span>{" "}
|
||||||
|
Logg
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink to="/stats" className={navLinkStyles}>
|
||||||
|
<span className={styles.navIcon}>
|
||||||
|
<BarChart />
|
||||||
|
</span>{" "}
|
||||||
|
Statistikk
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink to="/users" className={navLinkStyles}>
|
||||||
|
<span className={styles.navIcon}>
|
||||||
|
<PeopleIcon />
|
||||||
|
</span>{" "}
|
||||||
|
Folk
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<ul className="navbar-nav">
|
||||||
|
<li className="nav-item">
|
||||||
|
<a href={ADMIN_URL} className="nav-link">
|
||||||
|
Admin
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
.product {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 4px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background-color 0.1s ease-out, border 0.1s ease-out;
|
||||||
|
}
|
||||||
|
.product:hover {
|
||||||
|
border-color: rgba(25, 135, 84, 0.6);
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.active {
|
||||||
|
border-color: rgba(25, 135, 84, 0.6);
|
||||||
|
background: rgba(25, 135, 84, 0.2);
|
||||||
|
}
|
||||||
|
.active:hover {
|
||||||
|
background: rgba(25, 135, 84, 0.2);
|
||||||
|
}
|
||||||
|
.productText {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
padding: 4px;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.productImage {
|
||||||
|
width: 140px;
|
||||||
|
height: 140px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
.noProductImage {
|
||||||
|
width: 140px;
|
||||||
|
height: 140px;
|
||||||
|
}
|
||||||
|
.amount {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
padding: 4px;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
|
import React, { MouseEventHandler } from "react";
|
||||||
|
|
||||||
|
import { Product } from "../types";
|
||||||
|
import styles from "./ProductCard.module.css";
|
||||||
|
|
||||||
|
type ProductCardProps = {
|
||||||
|
product: Omit<Product, "userCounts">;
|
||||||
|
onProduct: MouseEventHandler;
|
||||||
|
active: boolean;
|
||||||
|
amount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProductCard: React.FC<ProductCardProps> = ({ product, onProduct, amount, active = false }) => {
|
||||||
|
const classes = clsx(styles.product, { [styles.active]: active });
|
||||||
|
return (
|
||||||
|
<button className={classes} type="button" onClick={onProduct}>
|
||||||
|
<div className={styles.wrap}>
|
||||||
|
<span className={styles.amount}>{amount}</span>
|
||||||
|
{product.imageUrl && <img src={product.imageUrl} className={styles.productImage} alt={product.name} />}
|
||||||
|
{!product.imageUrl && <div className={styles.noProductImage} />}
|
||||||
|
<span className={styles.productText}>{product.name}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.toast {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 32px;
|
||||||
|
left: 32px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Toast as BSToast } from "bootstrap";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import styles from "./Toast.module.css";
|
||||||
|
import { useToast } from "./ToastProvider";
|
||||||
|
|
||||||
|
export const Toast: React.FC = () => {
|
||||||
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
|
const { visible, showToast, message, background } = useToast();
|
||||||
|
|
||||||
|
const backgroundColor = `bg-${background}`;
|
||||||
|
const classes = clsx("toast align-items-center text-white bg-primary border-0", backgroundColor, styles.toast);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
const bsToast = BSToast.getInstance(ref.current) || new BSToast(ref.current);
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
bsToast?.show();
|
||||||
|
} else if (!visible) {
|
||||||
|
bsToast?.hide();
|
||||||
|
}
|
||||||
|
return () => bsToast?.hide();
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className={classes} role="alert" aria-live="assertive" aria-atomic="true">
|
||||||
|
<div className="d-flex">
|
||||||
|
<div className="toast-body">{message}</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-close btn-close-white me-2 m-auto"
|
||||||
|
data-bs-dismiss="toast"
|
||||||
|
aria-label="Close"
|
||||||
|
onClick={() => showToast(message)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import React, { ReactNode, useContext, useState } from "react";
|
||||||
|
|
||||||
|
interface ToastProps {
|
||||||
|
visible: boolean;
|
||||||
|
message: string;
|
||||||
|
showToast: (message: string, background?: string) => void;
|
||||||
|
hide: () => void;
|
||||||
|
background: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultState = {
|
||||||
|
visible: false,
|
||||||
|
message: "",
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
showToast: () => {},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
hide: () => {},
|
||||||
|
background: "primary",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToastContext = React.createContext<ToastProps>(defaultState);
|
||||||
|
|
||||||
|
type ToastProviderProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
|
||||||
|
const [visible, setVisible] = useState(defaultState.visible);
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
const [background, setBackground] = useState("");
|
||||||
|
|
||||||
|
const showToast = (newMessage: string, background = "primary") => {
|
||||||
|
setMessage(newMessage);
|
||||||
|
setBackground(background);
|
||||||
|
if (!visible) {
|
||||||
|
setVisible(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setVisible(false);
|
||||||
|
setMessage("");
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const hide = () => {
|
||||||
|
setVisible(true);
|
||||||
|
setMessage("");
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<ToastContext.Provider value={{ visible, showToast, message, hide, background }}>{children}</ToastContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useToast = (): ToastProps => useContext(ToastContext);
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
.user {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px;
|
||||||
|
border: 3px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background-color 0.1s ease-out, border 0.1s ease-out;
|
||||||
|
}
|
||||||
|
.user:hover {
|
||||||
|
border-color: rgba(25, 135, 84, 0.6);
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.active {
|
||||||
|
border-color: rgba(25, 135, 84, 0.6);
|
||||||
|
background: rgba(25, 135, 84, 0.2);
|
||||||
|
}
|
||||||
|
.active:hover {
|
||||||
|
background: rgba(25, 135, 84, 0.2);
|
||||||
|
}
|
||||||
|
.avatar {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50px;
|
||||||
|
}
|
||||||
|
.avatar svg {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { ReactComponent as PersonFill } from "bootstrap-icons/icons/person-fill.svg";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import React, { MouseEventHandler } from "react";
|
||||||
|
|
||||||
|
import { AllUsersQuery } from "../types";
|
||||||
|
import styles from "./UserCard.module.css";
|
||||||
|
|
||||||
|
type UserCardProps = {
|
||||||
|
user: AllUsersQuery["allUsers"][0] | null;
|
||||||
|
onUser: MouseEventHandler;
|
||||||
|
active: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** If user is null, then it's the special external user */
|
||||||
|
export const UserCard: React.FC<UserCardProps> = ({ user, onUser, active = false }) => {
|
||||||
|
const classes = clsx(styles.user, { [styles.active]: active });
|
||||||
|
return (
|
||||||
|
<button className={classes} onClick={onUser} type="button">
|
||||||
|
{user?.profile?.image && (
|
||||||
|
<img src={user.profile?.image} alt={`Profile for ${user.firstName}`} className={styles.avatar} />
|
||||||
|
)}
|
||||||
|
{!user?.profile?.image && (
|
||||||
|
<span className={styles.avatar}>
|
||||||
|
<PersonFill />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className={styles.name}>{user?.firstName || "Ekstern"}</span>
|
||||||
|
<span className={styles.balance}>{user?.profile?.balance || ""}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { useMutation } from "@apollo/client";
|
||||||
|
import React from "react";
|
||||||
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import { CreateUserDocument } from "../../types";
|
||||||
|
|
||||||
|
type FormValues = {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserCreate = () => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<FormValues>();
|
||||||
|
const [createUser, { loading: creating }] = useMutation(CreateUserDocument);
|
||||||
|
|
||||||
|
const onSubmit: SubmitHandler<FormValues> = async (data) => {
|
||||||
|
try {
|
||||||
|
await createUser({ variables: data });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("woops", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<label>Fornavn</label>
|
||||||
|
<input {...register("firstName", { required: true })} />
|
||||||
|
{errors.firstName && <span>This field is required</span>}
|
||||||
|
<br />
|
||||||
|
<label>Etternavn</label>
|
||||||
|
<input {...register("lastName", { required: true })} />
|
||||||
|
{errors.lastName && <span>This field is required</span>}
|
||||||
|
<br />
|
||||||
|
<label>E-post</label>
|
||||||
|
<input type="email" {...register("email", { required: true })} />
|
||||||
|
{errors.email && <span>This field is required</span>}
|
||||||
|
<br />
|
||||||
|
<input type="submit" disabled={creating} />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
mutation CreateUser($firstName: String!, $lastName: String!, $email: String!) {
|
||||||
|
createUser(input: {firstName: $firstName, lastName: $lastName, email: $email}) {
|
||||||
|
id
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
profile {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
.userList {
|
||||||
|
display: grid;
|
||||||
|
height: 400px;
|
||||||
|
grid-template-columns: repeat(12, 100px);
|
||||||
|
overflow-y: auto;
|
||||||
|
row-gap: 16px;
|
||||||
|
}
|
||||||
|
.amountSection {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
.checkout {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { UserCard } from "../../components/UserCard";
|
||||||
|
import styles from "./Deposit.module.css";
|
||||||
|
import { useDeposit } from "./useDeposit";
|
||||||
|
|
||||||
|
export const Deposit: React.FC = () => {
|
||||||
|
const {
|
||||||
|
loading,
|
||||||
|
selectedUser,
|
||||||
|
setSelectedUser,
|
||||||
|
amount,
|
||||||
|
setAmount,
|
||||||
|
userQuery,
|
||||||
|
setUserQuery,
|
||||||
|
filteredUsers,
|
||||||
|
reset,
|
||||||
|
onSubmit,
|
||||||
|
} = useDeposit();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<section>
|
||||||
|
<h2>Hvem skal sette inn penger?</h2>
|
||||||
|
<input
|
||||||
|
name="userQuery"
|
||||||
|
onChange={(event: React.FormEvent<HTMLInputElement>) => setUserQuery(event.currentTarget.value)}
|
||||||
|
value={userQuery}
|
||||||
|
placeholder="Filtrér på navn"
|
||||||
|
/>
|
||||||
|
{userQuery !== "" && (
|
||||||
|
<button className="btn btn-link" type="button" onClick={() => setUserQuery("")}>
|
||||||
|
Vis alle
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div className={styles.userList}>
|
||||||
|
{filteredUsers.map((user) => (
|
||||||
|
<UserCard
|
||||||
|
key={user.id}
|
||||||
|
active={selectedUser === user.id}
|
||||||
|
onUser={() => {
|
||||||
|
setSelectedUser(user.id);
|
||||||
|
}}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className={styles.amountSection}>
|
||||||
|
<h2>Hvor mye?</h2>
|
||||||
|
<input name="amount" value={amount} onChange={(event) => setAmount(event.target.value)} placeholder="0" />
|
||||||
|
</section>
|
||||||
|
<section className={styles.checkout}>
|
||||||
|
<button type="button" className="btn btn-primary btn-lg" onClick={onSubmit}>
|
||||||
|
Sett inn
|
||||||
|
</button>{" "}
|
||||||
|
<button type="button" className="btn btn-outline-secondary btn-lg" onClick={reset}>
|
||||||
|
Start på nytt
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
mutation CreateDeposit($userId: ID!, $amount: Int!) {
|
||||||
|
createDeposit(userId: $userId, amount:$amount) {
|
||||||
|
... on CreateDepositSuccess {
|
||||||
|
transaction {
|
||||||
|
id
|
||||||
|
amount
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
profile {
|
||||||
|
id
|
||||||
|
balance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on AboveMaxSaldo {
|
||||||
|
error
|
||||||
|
message
|
||||||
|
maxSaldo
|
||||||
|
above
|
||||||
|
}
|
||||||
|
... on FormErrors {
|
||||||
|
error
|
||||||
|
message
|
||||||
|
fields {
|
||||||
|
field
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useToast } from "../../components/ToastProvider";
|
||||||
|
import { AllUsersDocument, CreateDepositDocument } from "../../types";
|
||||||
|
|
||||||
|
export const useDeposit = () => {
|
||||||
|
const { data, loading } = useQuery(AllUsersDocument);
|
||||||
|
const [createDeposit] = useMutation(CreateDepositDocument);
|
||||||
|
const [selectedUser, setSelectedUser] = useState("");
|
||||||
|
const [amount, setAmount] = useState("");
|
||||||
|
const [userQuery, setUserQuery] = useState("");
|
||||||
|
const filteredUsers = (data?.allUsers || []).filter((user) =>
|
||||||
|
`${user.firstName.toLowerCase()} ${user.lastName.toLowerCase()}`.includes(userQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
const { showToast } = useToast();
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
const amountInt = parseInt(amount, 10) || 0;
|
||||||
|
const variables = { userId: selectedUser, amount: amountInt };
|
||||||
|
let res;
|
||||||
|
try {
|
||||||
|
res = await createDeposit({ variables });
|
||||||
|
} catch (e) {
|
||||||
|
console.log("woops", e);
|
||||||
|
showToast("Kunne ikke sette inn penger", "danger");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.data?.createDeposit?.__typename === "CreateDepositSuccess") {
|
||||||
|
const { transaction } = res.data?.createDeposit;
|
||||||
|
showToast(
|
||||||
|
`${transaction.user.firstName} har satt inn${transaction.amount}. Ny saldo er ${transaction.user.profile?.balance}.`
|
||||||
|
);
|
||||||
|
reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.data?.createDeposit?.__typename === "FormErrors") {
|
||||||
|
showToast(res.data?.createDeposit.message, "danger");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (res.data?.createDeposit?.__typename === "AboveMaxSaldo") {
|
||||||
|
showToast(
|
||||||
|
`${res.data.createDeposit.message}. Ny saldo ville vært ${res.data.createDeposit.above} mer enn maksimalt ${res.data.createDeposit.maxSaldo}.`,
|
||||||
|
"danger"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const reset = () => {
|
||||||
|
setSelectedUser("");
|
||||||
|
setAmount("");
|
||||||
|
setUserQuery("");
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
selectedUser,
|
||||||
|
setSelectedUser,
|
||||||
|
amount,
|
||||||
|
setAmount,
|
||||||
|
userQuery,
|
||||||
|
setUserQuery,
|
||||||
|
filteredUsers,
|
||||||
|
reset,
|
||||||
|
onSubmit,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.log {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import React from "react";
|
||||||
|
import { useUrlSearchParams } from "use-url-search-params";
|
||||||
|
|
||||||
|
import { OrderListDocument, TransactionListDocument } from "../../types";
|
||||||
|
import { formatTime } from "../../utils/time";
|
||||||
|
import styles from "./Log.module.css";
|
||||||
|
|
||||||
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
|
export const Log = () => {
|
||||||
|
const types = {
|
||||||
|
orderOffset: Number,
|
||||||
|
transactionOffset: Number,
|
||||||
|
};
|
||||||
|
const [params, setParams] = useUrlSearchParams({}, types);
|
||||||
|
const orderOffset = Number(params.orderOffset) || 0;
|
||||||
|
const transactionOffset = Number(params.transactionOffset) || 0;
|
||||||
|
const { data: ordersData, loading: ordersLoading } = useQuery(OrderListDocument, {
|
||||||
|
variables: { offset: orderOffset },
|
||||||
|
});
|
||||||
|
const { data: transactionsData, loading: transactionsLoading } = useQuery(TransactionListDocument, {
|
||||||
|
variables: { offset: transactionOffset },
|
||||||
|
});
|
||||||
|
const loading = ordersLoading || transactionsLoading;
|
||||||
|
|
||||||
|
if (loading) return <div>Loading...</div>;
|
||||||
|
|
||||||
|
const orders = ordersData?.orderList || [];
|
||||||
|
const transactions = transactionsData?.transactionList || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.log}>
|
||||||
|
<div>
|
||||||
|
<h2>Ordre</h2>
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Når</th>
|
||||||
|
<th scope="col">Hvem</th>
|
||||||
|
<th scope="col">Hva</th>
|
||||||
|
<th scope="col">Sum</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{orders.map((order) => (
|
||||||
|
<tr key={order.id}>
|
||||||
|
<td>{formatTime(order.created)}</td>
|
||||||
|
<td>{order.isExternal ? "Ekstern" : `${order.customer?.firstName} ${order.customer?.lastName}`}</td>
|
||||||
|
<td>
|
||||||
|
{(order.orderlines ?? []).map((line) => (
|
||||||
|
<div key={line.id}>
|
||||||
|
{line.amount}x {line.product.name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</td>
|
||||||
|
<td>{order.orderSum}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<nav aria-label="Order pagination">
|
||||||
|
<ul className="pagination">
|
||||||
|
<li className={clsx("page-item", { disabled: orderOffset <= 0 })}>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="page-link"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setParams({ orderOffset: orderOffset - PAGE_SIZE });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="page-item">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="page-link"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setParams({ orderOffset: orderOffset + PAGE_SIZE });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>Transaksjoner</h2>
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Når</th>
|
||||||
|
<th scope="col">Hvem</th>
|
||||||
|
<th scope="col">Innskudd</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{transactions.map((trans) => (
|
||||||
|
<tr key={trans.id}>
|
||||||
|
<td>{formatTime(trans.created)}</td>
|
||||||
|
<td>
|
||||||
|
{trans.user.firstName} {trans.user.lastName}
|
||||||
|
</td>
|
||||||
|
<td>{trans.amount}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<nav aria-label="Transaction pagination">
|
||||||
|
<ul className="pagination">
|
||||||
|
<li className={clsx("page-item", { disabled: transactionOffset <= 0 })}>
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="page-link"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setParams({ transactionOffset: transactionOffset - PAGE_SIZE });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="page-item">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="page-link"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setParams({ transactionOffset: transactionOffset + PAGE_SIZE });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
query orderList($limit: Int = 10, $offset: Int = 0) {
|
||||||
|
orderList(order: {created: DESC}, pagination: {limit: $limit, offset: $offset}) {
|
||||||
|
id
|
||||||
|
isExternal
|
||||||
|
created
|
||||||
|
orderSum
|
||||||
|
customer {
|
||||||
|
id
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
profile {
|
||||||
|
id
|
||||||
|
balance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
orderlines {
|
||||||
|
id
|
||||||
|
amount
|
||||||
|
unitPrice
|
||||||
|
product {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
salePriceExt
|
||||||
|
salePriceInt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
query transactionList($limit: Int = 10, $offset: Int = 0) {
|
||||||
|
transactionList(order: {created: DESC}, pagination: {limit: $limit, offset: $offset}) {
|
||||||
|
id
|
||||||
|
created
|
||||||
|
amount
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
profile {
|
||||||
|
id
|
||||||
|
balance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
.userList {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(12, 128px);
|
||||||
|
row-gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.productList {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(8, 150px);
|
||||||
|
row-gap: 16px;
|
||||||
|
}
|
||||||
|
.productListSection {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkout {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total {
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 200px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
background: black;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { ProductCard } from "../../components/ProductCard";
|
||||||
|
import { UserCard } from "../../components/UserCard";
|
||||||
|
import { OrderLineInput, Product } from "../../types";
|
||||||
|
import styles from "./Register.module.css";
|
||||||
|
import { useRegister } from "./useRegister";
|
||||||
|
|
||||||
|
const USER_EXTERNAL = "external";
|
||||||
|
|
||||||
|
export const Register: React.FC = () => {
|
||||||
|
const {
|
||||||
|
users,
|
||||||
|
products,
|
||||||
|
loading,
|
||||||
|
showAll,
|
||||||
|
setShowAll,
|
||||||
|
setSelectedUser,
|
||||||
|
selectedUser,
|
||||||
|
addToOrder,
|
||||||
|
order,
|
||||||
|
total,
|
||||||
|
onSubmit,
|
||||||
|
reset,
|
||||||
|
} = useRegister();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<section>
|
||||||
|
<h2>Hvem kjøper?</h2>
|
||||||
|
<div className={styles.userList}>
|
||||||
|
<UserCard
|
||||||
|
key={USER_EXTERNAL}
|
||||||
|
user={null}
|
||||||
|
onUser={() => setSelectedUser(USER_EXTERNAL)}
|
||||||
|
active={selectedUser == USER_EXTERNAL}
|
||||||
|
/>
|
||||||
|
{users.map((user) => (
|
||||||
|
<UserCard
|
||||||
|
key={user.id}
|
||||||
|
user={user}
|
||||||
|
onUser={() => setSelectedUser(user.id)}
|
||||||
|
active={selectedUser == user.id}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button type="button" className="btn btn-link" onClick={() => setShowAll(!showAll)}>
|
||||||
|
{showAll ? "Skjul gamliser" : "Vis alle"}
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
<section className={styles.productListSection}>
|
||||||
|
<h2>Kjøper hva?</h2>
|
||||||
|
<div className={styles.productList}>
|
||||||
|
{products.map((product: Omit<Product, "userCounts">) => (
|
||||||
|
<ProductCard
|
||||||
|
key={product.id}
|
||||||
|
product={product}
|
||||||
|
onProduct={() => addToOrder(product.id)}
|
||||||
|
amount={order.find((orderLine: OrderLineInput) => orderLine.productId === product.id)?.amount || 0}
|
||||||
|
active={order.some((orderLine: OrderLineInput) => orderLine.productId === product.id)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section className={styles.checkout}>
|
||||||
|
<span className={styles.total}>
|
||||||
|
Totalt: <span>{total}</span>
|
||||||
|
</span>
|
||||||
|
<button type="button" className="btn btn-primary btn-lg" onClick={onSubmit}>
|
||||||
|
Kjøp
|
||||||
|
</button>{" "}
|
||||||
|
<button type="button" className="btn btn-outline-secondary btn-lg" onClick={reset}>
|
||||||
|
Start på nytt
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
query allProducts($active: Boolean!) {
|
||||||
|
allProducts(filters: {active: $active}) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
imageUrl
|
||||||
|
salePriceExt
|
||||||
|
salePriceInt
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
query allUsers {
|
||||||
|
allUsers {
|
||||||
|
id
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
profile {
|
||||||
|
id
|
||||||
|
balance
|
||||||
|
image
|
||||||
|
lastPurchaseDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
mutation CreateOrder($customerId: ID!, $orderLines:[OrderLineInput!]!, $isExternal: Boolean!) {
|
||||||
|
createOrder(customerId: $customerId, orderLines: $orderLines, isExternal: $isExternal) {
|
||||||
|
... on CreateOrderSuccess {
|
||||||
|
order {
|
||||||
|
id
|
||||||
|
isExternal
|
||||||
|
customer {
|
||||||
|
id
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
profile {
|
||||||
|
id
|
||||||
|
balance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
orderlines {
|
||||||
|
id
|
||||||
|
amount
|
||||||
|
unitPrice
|
||||||
|
product {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
salePriceExt
|
||||||
|
salePriceInt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
orderSum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on InsufficientFunds {
|
||||||
|
error
|
||||||
|
message
|
||||||
|
amountLacking
|
||||||
|
}
|
||||||
|
... on FormErrors {
|
||||||
|
error
|
||||||
|
message
|
||||||
|
fields {
|
||||||
|
field
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useToast } from "../../components/ToastProvider";
|
||||||
|
import { AllProductsDocument, AllUsersDocument, CreateOrderDocument, OrderLineInput, Product } from "../../types";
|
||||||
|
import { activeUserFilter } from "../../utils/users";
|
||||||
|
|
||||||
|
const USER_EXTERNAL = "external";
|
||||||
|
|
||||||
|
const sumTotal = (products: Omit<Product, "userCounts">[], order: OrderLineInput[], isExternal: boolean) => {
|
||||||
|
const prices = order.map((orderLine: OrderLineInput) => {
|
||||||
|
const product = products.find((product: Omit<Product, "userCounts">) => product.id === orderLine.productId);
|
||||||
|
if (!product) return 0;
|
||||||
|
return orderLine.amount * (isExternal ? product.salePriceExt : product.salePriceInt);
|
||||||
|
});
|
||||||
|
return prices.reduce((partialSum, a) => partialSum + a, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRegister = () => {
|
||||||
|
const [showAll, setShowAll] = useState(false);
|
||||||
|
const [selectedUser, setSelectedUser] = useState("");
|
||||||
|
const [order, setOrder] = useState<OrderLineInput[]>([]);
|
||||||
|
const { showToast } = useToast();
|
||||||
|
|
||||||
|
const { data, loading } = useQuery(AllProductsDocument, { variables: { active: true } });
|
||||||
|
const products = data?.allProducts || [];
|
||||||
|
|
||||||
|
const { data: usersData, loading: usersLoading } = useQuery(AllUsersDocument);
|
||||||
|
const allUsers = usersData?.allUsers || [];
|
||||||
|
const users = showAll ? allUsers : allUsers.filter(activeUserFilter);
|
||||||
|
|
||||||
|
const [createOrderMutation] = useMutation(CreateOrderDocument, {
|
||||||
|
variables: { customerId: selectedUser, isExternal: selectedUser === USER_EXTERNAL, orderLines: order },
|
||||||
|
});
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
setOrder([]);
|
||||||
|
setSelectedUser("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const addToOrder = (productId: string) => {
|
||||||
|
let added = false;
|
||||||
|
const newOrder = order.map((orderLine: OrderLineInput) => {
|
||||||
|
if (orderLine.productId === productId) {
|
||||||
|
added = true;
|
||||||
|
return { ...orderLine, amount: orderLine.amount + 1 };
|
||||||
|
}
|
||||||
|
return orderLine;
|
||||||
|
});
|
||||||
|
if (!added) {
|
||||||
|
newOrder.push({ productId, amount: 1 });
|
||||||
|
}
|
||||||
|
setOrder(newOrder);
|
||||||
|
};
|
||||||
|
|
||||||
|
const total = sumTotal(products, order, selectedUser === USER_EXTERNAL);
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
let res;
|
||||||
|
try {
|
||||||
|
res = await createOrderMutation();
|
||||||
|
} catch (e) {
|
||||||
|
showToast("Whoops");
|
||||||
|
console.log("woops");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { data } = res;
|
||||||
|
const createOrderResponse = data?.createOrder;
|
||||||
|
if (createOrderResponse?.__typename === "CreateOrderSuccess") {
|
||||||
|
const { order } = createOrderResponse;
|
||||||
|
const orderLines = (order?.orderlines || [])
|
||||||
|
.map((orderLine) => `${orderLine.amount} ${orderLine.product.name}`)
|
||||||
|
.join(", ");
|
||||||
|
const who = !order.customer ? "Ekstern" : `${order.customer?.firstName} ${order.customer?.lastName}`;
|
||||||
|
showToast(`${who} kjøpte: ${orderLines}`);
|
||||||
|
reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createOrderResponse?.__typename === "FormErrors") {
|
||||||
|
showToast(createOrderResponse.message, "danger");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createOrderResponse?.__typename === "InsufficientFunds") {
|
||||||
|
showToast(`${createOrderResponse.message} Du mangler ${createOrderResponse.amountLacking},-`, "danger");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
loading: loading || usersLoading,
|
||||||
|
users,
|
||||||
|
products,
|
||||||
|
showAll,
|
||||||
|
setShowAll,
|
||||||
|
setSelectedUser,
|
||||||
|
selectedUser,
|
||||||
|
addToOrder,
|
||||||
|
order,
|
||||||
|
total,
|
||||||
|
onSubmit,
|
||||||
|
reset,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { Bar, BarDatum } from "@nivo/bar";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { OrderStatsDocument, OrderStatsByTime } from "../../types";
|
||||||
|
import { StatsView } from "./StatsView";
|
||||||
|
|
||||||
|
const days = { 1: "Mandag", 2: "Tirsdag", 3: "Onsdag", 4: "Torsdag", 5: "Fredag", 6: "Lørdag", 7: "Søndag" };
|
||||||
|
|
||||||
|
const months = {
|
||||||
|
1: "Januar",
|
||||||
|
2: "Februar",
|
||||||
|
3: "Mars",
|
||||||
|
4: "April",
|
||||||
|
5: "Mai",
|
||||||
|
6: "Juni",
|
||||||
|
7: "Juli",
|
||||||
|
8: "August",
|
||||||
|
9: "September",
|
||||||
|
10: "Oktober",
|
||||||
|
11: "November",
|
||||||
|
12: "Desember",
|
||||||
|
};
|
||||||
|
|
||||||
|
const OrderBar = ({ data }: { data: BarDatum[] }) => (
|
||||||
|
<Bar
|
||||||
|
data={data}
|
||||||
|
margin={{ left: 60, bottom: 40 }}
|
||||||
|
width={1200}
|
||||||
|
height={400}
|
||||||
|
colorBy="indexValue"
|
||||||
|
enableLabel={false}
|
||||||
|
tooltipLabel={(d) => d.id.toString()}
|
||||||
|
axisLeft={{ legend: "Ordre", legendPosition: "middle", legendOffset: -50 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const OrderStats = () => {
|
||||||
|
const { data, loading } = useQuery(OrderStatsDocument);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatsView loading={loading}>
|
||||||
|
<h3>Ordre per time</h3>
|
||||||
|
<OrderBar
|
||||||
|
data={(data?.orderStats?.hourly || []).map((stat: OrderStatsByTime) => ({
|
||||||
|
id: `${stat.period.toString().padStart(2, "0")}:00`,
|
||||||
|
label: stat.period,
|
||||||
|
value: stat.count,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<h3>Ordre per ukedag</h3>
|
||||||
|
<OrderBar
|
||||||
|
data={(data?.orderStats?.daily || []).map((stat: OrderStatsByTime) => ({
|
||||||
|
/* @ts-expect-error Here, stat.period can only be a key in days range. */
|
||||||
|
id: days[stat.period],
|
||||||
|
label: stat.period,
|
||||||
|
value: stat.count,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<h3>Ordre per måned</h3>
|
||||||
|
<OrderBar
|
||||||
|
data={(data?.orderStats?.monthly || []).map((stat: OrderStatsByTime) => ({
|
||||||
|
/* @ts-expect-error Here, stat.period can only be a key in months range. */
|
||||||
|
id: months[stat.period],
|
||||||
|
label: stat.period,
|
||||||
|
value: stat.count,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<h3>Ordre per år</h3>
|
||||||
|
<OrderBar
|
||||||
|
data={(data?.orderStats?.yearly || []).map((stat: OrderStatsByTime) => ({
|
||||||
|
id: stat.period,
|
||||||
|
label: stat.period,
|
||||||
|
value: stat.count,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</StatsView>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { Pie } from "@nivo/pie";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { AllProductStatsDocument } from "../../types";
|
||||||
|
import styles from "./Stats.module.css";
|
||||||
|
import { StatsView } from "./StatsView";
|
||||||
|
|
||||||
|
type UserCount = {
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProductStats = () => {
|
||||||
|
const { data, loading } = useQuery(AllProductStatsDocument, {
|
||||||
|
variables: { active: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatsView loading={loading}>
|
||||||
|
<div className={styles.products}>
|
||||||
|
{(data?.allProducts || []).map((product) => (
|
||||||
|
<div key={product.id}>
|
||||||
|
{product.name}
|
||||||
|
<Pie
|
||||||
|
margin={{ top: 40, right: 120, bottom: 40, left: 120 }}
|
||||||
|
data={product.userCounts.map((uc: UserCount) => ({
|
||||||
|
id: uc.name,
|
||||||
|
label: uc.name,
|
||||||
|
value: uc.count,
|
||||||
|
}))}
|
||||||
|
width={400}
|
||||||
|
height={300}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</StatsView>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.products {
|
||||||
|
display: grid;
|
||||||
|
max-width: 1200px;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
.statsNav {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { Line } from "@nivo/line";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { ProductStatsDocument } from "../../types";
|
||||||
|
import { StatsView } from "./StatsView";
|
||||||
|
|
||||||
|
export const Stats = () => {
|
||||||
|
const { data, loading } = useQuery(ProductStatsDocument);
|
||||||
|
|
||||||
|
const series: any[] = [];
|
||||||
|
(data?.productStats || [])?.forEach(({ productName: id, data }) => {
|
||||||
|
const lineData: any[] = [];
|
||||||
|
data.forEach((point, index) => {
|
||||||
|
lineData.push({ x: point.x, y: point.y + (lineData?.[index - 1]?.y || 0) });
|
||||||
|
});
|
||||||
|
series.push({ id, data: lineData });
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatsView loading={loading}>
|
||||||
|
<h2>Kjøp over tid</h2>
|
||||||
|
<Line
|
||||||
|
data={series}
|
||||||
|
width={1000}
|
||||||
|
height={400}
|
||||||
|
margin={{ top: 50, right: 110, bottom: 50, left: 60 }}
|
||||||
|
xScale={{
|
||||||
|
type: "time",
|
||||||
|
format: "%Y-%m-%d",
|
||||||
|
useUTC: false,
|
||||||
|
precision: "day",
|
||||||
|
}}
|
||||||
|
xFormat="time:%Y-%m-%d"
|
||||||
|
yScale={{
|
||||||
|
type: "linear",
|
||||||
|
stacked: false,
|
||||||
|
}}
|
||||||
|
axisLeft={{
|
||||||
|
legend: "Purchases",
|
||||||
|
legendOffset: 12,
|
||||||
|
}}
|
||||||
|
axisBottom={{
|
||||||
|
format: "%b %Y",
|
||||||
|
legend: "Date",
|
||||||
|
legendOffset: -40,
|
||||||
|
}}
|
||||||
|
useMesh
|
||||||
|
pointLabelYOffset={0}
|
||||||
|
tooltip={({ point }) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "white",
|
||||||
|
padding: "9px 12px",
|
||||||
|
border: "1px solid #ccc",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<strong>{point.data.xFormatted}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span style={{ background: point.serieColor, width: 13, height: 13, display: "inline-block" }} />{" "}
|
||||||
|
<strong>{point.data.yFormatted}</strong> {point.serieId}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</StatsView>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { NavLink } from "react-router-dom";
|
||||||
|
|
||||||
|
import styles from "./Stats.module.css";
|
||||||
|
const navLinkStyles = ({ isActive }: { isActive: boolean }) => (isActive ? "nav-link active" : "nav-link");
|
||||||
|
|
||||||
|
export const StatsNav = () => (
|
||||||
|
<div className={styles.statsNav}>
|
||||||
|
<ul className="nav nav-tabs">
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink className={navLinkStyles} to="/stats" end>
|
||||||
|
Oversikt
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink className={navLinkStyles} to="/stats/products">
|
||||||
|
Produkter
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink className={navLinkStyles} to="/stats/orders">
|
||||||
|
Ordre
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
<li className="nav-item">
|
||||||
|
<NavLink className="nav-link disabled" to="/stats/users" title="TODO">
|
||||||
|
Folk
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const StatsView = ({ children, loading }: { children: React.ReactNode; loading: boolean }) => (
|
||||||
|
<div className={styles.stats}>
|
||||||
|
<h2>Statistikk</h2>
|
||||||
|
<StatsNav />
|
||||||
|
{loading && <div>Loading...</div>}
|
||||||
|
{!loading && children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
query allProductStats($active: Boolean) {
|
||||||
|
allProducts(filters: {active: $active}) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
userCounts
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
query orderStats {
|
||||||
|
orderStats {
|
||||||
|
yearly {
|
||||||
|
period
|
||||||
|
count
|
||||||
|
}
|
||||||
|
monthly {
|
||||||
|
period
|
||||||
|
count
|
||||||
|
}
|
||||||
|
daily {
|
||||||
|
period
|
||||||
|
count
|
||||||
|
}
|
||||||
|
hourly {
|
||||||
|
period
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
query productStats {
|
||||||
|
productStats {
|
||||||
|
productName
|
||||||
|
data {
|
||||||
|
x
|
||||||
|
y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.userDetail {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-template-columns: 400px 1fr;
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { Line } from "@nivo/line";
|
||||||
|
import React from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
|
import { User, UserDetailDocument, UserProfile } from "../../types";
|
||||||
|
import { formatMonth, formatTime } from "../../utils/time";
|
||||||
|
import styles from "./UserDetail.module.css";
|
||||||
|
|
||||||
|
export const UserDetail = () => {
|
||||||
|
const { userId } = useParams();
|
||||||
|
const { data, loading } = useQuery(UserDetailDocument, { variables: { userId: userId || "" }, skip: !userId });
|
||||||
|
|
||||||
|
if (loading) return <div>Loading...</div>;
|
||||||
|
|
||||||
|
const user = data?.user as User;
|
||||||
|
const profile = user?.profile as UserProfile;
|
||||||
|
// FIXME: types
|
||||||
|
const series: any[] = [];
|
||||||
|
(profile?.productOrderStats || [])?.forEach(({ productName: id, data }) => {
|
||||||
|
const lineData: any[] = [];
|
||||||
|
data.forEach((point, index) => {
|
||||||
|
lineData.push({ x: point.x, y: point.y + (lineData?.[index - 1]?.y || 0) });
|
||||||
|
});
|
||||||
|
series.push({ id, data: lineData });
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div className={styles.userDetail}>
|
||||||
|
<div>
|
||||||
|
<h1>
|
||||||
|
{profile.image && <img src={profile.image} alt={`${user.firstName}s profile picture`} />} {user.firstName}
|
||||||
|
</h1>
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">Stats</div>
|
||||||
|
<ul className="list-group list-group-flush">
|
||||||
|
<li className="list-group-item">
|
||||||
|
Saldo: <strong>{profile.balance.toLocaleString("no")} kr</strong>
|
||||||
|
</li>
|
||||||
|
<li className="list-group-item">
|
||||||
|
Siste kjøp: <strong>{formatTime(profile.lastPurchaseDate)}</strong>
|
||||||
|
</li>
|
||||||
|
<li className="list-group-item">
|
||||||
|
Kjøp totalt: <strong>{profile.orderSumTotal.toLocaleString("no")} kr</strong>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">Top 5 måneder</div>
|
||||||
|
<ul className="list-group list-group-flush">
|
||||||
|
{profile.topMonths.map((topMonth) => (
|
||||||
|
<li key={topMonth.period} className="list-group-item">
|
||||||
|
{formatMonth(topMonth.period)}: <strong>{topMonth.count}</strong> kjøp på totalt{" "}
|
||||||
|
<strong>{topMonth.sum} kr</strong>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>Kjøp over tid</h2>
|
||||||
|
<Line
|
||||||
|
data={series}
|
||||||
|
width={1000}
|
||||||
|
height={400}
|
||||||
|
margin={{ top: 50, right: 110, bottom: 50, left: 60 }}
|
||||||
|
xScale={{
|
||||||
|
type: "time",
|
||||||
|
format: "%Y-%m-%d",
|
||||||
|
useUTC: false,
|
||||||
|
precision: "day",
|
||||||
|
}}
|
||||||
|
xFormat="time:%Y-%m-%d"
|
||||||
|
yScale={{
|
||||||
|
type: "linear",
|
||||||
|
stacked: false,
|
||||||
|
}}
|
||||||
|
axisLeft={{
|
||||||
|
legend: "Purchases",
|
||||||
|
legendOffset: 12,
|
||||||
|
}}
|
||||||
|
axisBottom={{
|
||||||
|
format: "%b %Y",
|
||||||
|
legend: "Date",
|
||||||
|
legendOffset: -12,
|
||||||
|
}}
|
||||||
|
useMesh
|
||||||
|
pointLabelYOffset={0}
|
||||||
|
tooltip={({ point }) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
background: "white",
|
||||||
|
padding: "9px 12px",
|
||||||
|
border: "1px solid #ccc",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<strong>{point.data.xFormatted}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span style={{ background: point.serieColor, width: 13, height: 13, display: "inline-block" }} />{" "}
|
||||||
|
<strong>{point.data.yFormatted}</strong> {point.serieId}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
|
import { AllUsersDocument } from "../../types";
|
||||||
|
import { activeUserFilter } from "../../utils/users";
|
||||||
|
|
||||||
|
export const UserList = () => {
|
||||||
|
const { data, loading } = useQuery(AllUsersDocument);
|
||||||
|
const [showAll, setShowAll] = useState(false);
|
||||||
|
|
||||||
|
if (loading) return <div>Loading...</div>;
|
||||||
|
|
||||||
|
const allUsers = data?.allUsers || [];
|
||||||
|
const users = showAll ? allUsers : allUsers.filter((u) => activeUserFilter(u));
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button type="button" className="btn btn-link" onClick={() => setShowAll(!showAll)}>
|
||||||
|
{showAll ? "Skjul gamliser" : "Vis alle"}
|
||||||
|
</button>
|
||||||
|
<ul>
|
||||||
|
{(users || []).map((user) => (
|
||||||
|
<li key={user.id}>
|
||||||
|
<Link to={`/user/${user.id}`}>
|
||||||
|
{user.firstName} {user.lastName}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<br />
|
||||||
|
<Link to="/users/create" className="btn btn-primary">
|
||||||
|
Opprett ny
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
query userDetail($userId: ID!) {
|
||||||
|
user(userId: $userId) {
|
||||||
|
id
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
profile {
|
||||||
|
id
|
||||||
|
balance
|
||||||
|
image
|
||||||
|
lastPurchaseDate
|
||||||
|
topMonths {
|
||||||
|
period
|
||||||
|
sum
|
||||||
|
count
|
||||||
|
}
|
||||||
|
productOrderStats {
|
||||||
|
productName
|
||||||
|
data {
|
||||||
|
x
|
||||||
|
y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
orderSumTotal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
export function usePrevious(value: any) {
|
||||||
|
// The ref object is a generic container whose current property is mutable ...
|
||||||
|
// ... and can hold any value, similar to an instance property on a class
|
||||||
|
const ref = useRef();
|
||||||
|
|
||||||
|
// Store current value in ref
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value;
|
||||||
|
}, [value]); // Only re-run if value changes
|
||||||
|
// Return previous value (happens before update in useEffect above)
|
||||||
|
return ref.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useToast() {
|
||||||
|
const show = useCallback(() => {
|
||||||
|
console.log("showing toast with element from root");
|
||||||
|
}, []);
|
||||||
|
return { show };
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
document.getElementById("root")
|
||||||
|
);
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
||||||
|
export type Maybe<T> = T | null;
|
||||||
|
export type InputMaybe<T> = Maybe<T>;
|
||||||
|
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||||
|
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
|
||||||
|
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
|
||||||
|
/** All built-in and custom scalars, mapped to their actual values */
|
||||||
|
export type Scalars = {
|
||||||
|
ID: string;
|
||||||
|
String: string;
|
||||||
|
Boolean: boolean;
|
||||||
|
Int: number;
|
||||||
|
Float: number;
|
||||||
|
/** Date with time (isoformat) */
|
||||||
|
DateTime: any;
|
||||||
|
/** The GenericScalar scalar type represents a generic GraphQL scalar value that could be: List or Object. */
|
||||||
|
JSONScalar: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AboveMaxSaldo = Error & {
|
||||||
|
__typename?: 'AboveMaxSaldo';
|
||||||
|
above: Scalars['Float'];
|
||||||
|
error: Scalars['Boolean'];
|
||||||
|
maxSaldo: Scalars['Float'];
|
||||||
|
message: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateDepositResponse = AboveMaxSaldo | CreateDepositSuccess | FormErrors;
|
||||||
|
|
||||||
|
export type CreateDepositSuccess = {
|
||||||
|
__typename?: 'CreateDepositSuccess';
|
||||||
|
transaction: Transaction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateOrderResponse = CreateOrderSuccess | FormErrors | InsufficientFunds;
|
||||||
|
|
||||||
|
export type CreateOrderSuccess = {
|
||||||
|
__typename?: 'CreateOrderSuccess';
|
||||||
|
order: Order;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Error = {
|
||||||
|
error: Scalars['Boolean'];
|
||||||
|
message: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FieldError = Error & {
|
||||||
|
__typename?: 'FieldError';
|
||||||
|
error: Scalars['Boolean'];
|
||||||
|
field: Scalars['String'];
|
||||||
|
message: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FormErrors = {
|
||||||
|
__typename?: 'FormErrors';
|
||||||
|
error: Scalars['Boolean'];
|
||||||
|
fields: Array<FieldError>;
|
||||||
|
message: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InsufficientFunds = Error & {
|
||||||
|
__typename?: 'InsufficientFunds';
|
||||||
|
amountLacking: Scalars['Float'];
|
||||||
|
error: Scalars['Boolean'];
|
||||||
|
message: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Mutation = {
|
||||||
|
__typename?: 'Mutation';
|
||||||
|
createDeposit: CreateDepositResponse;
|
||||||
|
createOrder: CreateOrderResponse;
|
||||||
|
createUser: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateDepositArgs = {
|
||||||
|
amount: Scalars['Int'];
|
||||||
|
userId: Scalars['ID'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateOrderArgs = {
|
||||||
|
customerId: Scalars['ID'];
|
||||||
|
isExternal?: Scalars['Boolean'];
|
||||||
|
orderLines: Array<OrderLineInput>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationCreateUserArgs = {
|
||||||
|
input: UserInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OffsetPaginationInput = {
|
||||||
|
limit?: Scalars['Int'];
|
||||||
|
offset?: Scalars['Int'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Order = {
|
||||||
|
__typename?: 'Order';
|
||||||
|
created: Scalars['DateTime'];
|
||||||
|
customer?: Maybe<User>;
|
||||||
|
id: Scalars['ID'];
|
||||||
|
isExternal: Scalars['Boolean'];
|
||||||
|
orderSum: Scalars['Float'];
|
||||||
|
orderlines?: Maybe<Array<OrderLine>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OrderLine = {
|
||||||
|
__typename?: 'OrderLine';
|
||||||
|
amount: Scalars['Int'];
|
||||||
|
id: Scalars['ID'];
|
||||||
|
order: Order;
|
||||||
|
product: Product;
|
||||||
|
unitPrice: Scalars['Float'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OrderLineInput = {
|
||||||
|
amount: Scalars['Int'];
|
||||||
|
productId: Scalars['ID'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OrderOrdering = {
|
||||||
|
created?: InputMaybe<Ordering>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OrderStats = {
|
||||||
|
__typename?: 'OrderStats';
|
||||||
|
daily: Array<OrderStatsByTime>;
|
||||||
|
hourly: Array<OrderStatsByTime>;
|
||||||
|
monthly: Array<OrderStatsByTime>;
|
||||||
|
yearly: Array<OrderStatsByTime>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OrderStatsByTime = {
|
||||||
|
__typename?: 'OrderStatsByTime';
|
||||||
|
count: Scalars['String'];
|
||||||
|
period: Scalars['Int'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum Ordering {
|
||||||
|
Asc = 'ASC',
|
||||||
|
Desc = 'DESC'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Point = {
|
||||||
|
__typename?: 'Point';
|
||||||
|
x: Scalars['String'];
|
||||||
|
y: Scalars['Int'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Product = {
|
||||||
|
__typename?: 'Product';
|
||||||
|
id: Scalars['ID'];
|
||||||
|
imageUrl: Scalars['String'];
|
||||||
|
name: Scalars['String'];
|
||||||
|
salePriceExt: Scalars['Float'];
|
||||||
|
salePriceInt: Scalars['Float'];
|
||||||
|
userCounts: Scalars['JSONScalar'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProductFilter = {
|
||||||
|
active?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
id?: InputMaybe<Scalars['ID']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProductOrderStat = {
|
||||||
|
__typename?: 'ProductOrderStat';
|
||||||
|
data: Array<Point>;
|
||||||
|
productName: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Query = {
|
||||||
|
__typename?: 'Query';
|
||||||
|
allOrders: Array<Order>;
|
||||||
|
allProducts: Array<Product>;
|
||||||
|
allUsers: Array<User>;
|
||||||
|
orderList: Array<Order>;
|
||||||
|
orderStats: OrderStats;
|
||||||
|
productStats: Array<ProductOrderStat>;
|
||||||
|
transactionList: Array<Transaction>;
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryAllProductsArgs = {
|
||||||
|
filters?: InputMaybe<ProductFilter>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryOrderListArgs = {
|
||||||
|
order?: InputMaybe<OrderOrdering>;
|
||||||
|
pagination?: InputMaybe<OffsetPaginationInput>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryTransactionListArgs = {
|
||||||
|
order?: InputMaybe<TransactionOrdering>;
|
||||||
|
pagination?: InputMaybe<OffsetPaginationInput>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryUserArgs = {
|
||||||
|
userId: Scalars['ID'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TopMonth = {
|
||||||
|
__typename?: 'TopMonth';
|
||||||
|
count: Scalars['Int'];
|
||||||
|
period: Scalars['String'];
|
||||||
|
sum: Scalars['Int'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Transaction = {
|
||||||
|
__typename?: 'Transaction';
|
||||||
|
amount: Scalars['Float'];
|
||||||
|
created: Scalars['DateTime'];
|
||||||
|
id: Scalars['ID'];
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TransactionOrdering = {
|
||||||
|
created?: InputMaybe<Ordering>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
__typename?: 'User';
|
||||||
|
firstName: Scalars['String'];
|
||||||
|
id: Scalars['ID'];
|
||||||
|
lastName: Scalars['String'];
|
||||||
|
profile?: Maybe<UserProfile>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserInput = {
|
||||||
|
email?: InputMaybe<Scalars['String']>;
|
||||||
|
firstName?: InputMaybe<Scalars['String']>;
|
||||||
|
lastName?: InputMaybe<Scalars['String']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserProfile = {
|
||||||
|
__typename?: 'UserProfile';
|
||||||
|
balance: Scalars['Float'];
|
||||||
|
id: Scalars['ID'];
|
||||||
|
image: Scalars['String'];
|
||||||
|
lastPurchaseDate: Scalars['DateTime'];
|
||||||
|
orderSumTotal: Scalars['Float'];
|
||||||
|
productOrderStats: Array<ProductOrderStat>;
|
||||||
|
topMonths: Array<TopMonth>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateUserMutationVariables = Exact<{
|
||||||
|
firstName: Scalars['String'];
|
||||||
|
lastName: Scalars['String'];
|
||||||
|
email: Scalars['String'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type CreateUserMutation = { __typename?: 'Mutation', createUser: { __typename?: 'User', id: string, firstName: string, lastName: string, profile?: { __typename?: 'UserProfile', id: string } | null } };
|
||||||
|
|
||||||
|
export type CreateDepositMutationVariables = Exact<{
|
||||||
|
userId: Scalars['ID'];
|
||||||
|
amount: Scalars['Int'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type CreateDepositMutation = { __typename?: 'Mutation', createDeposit: { __typename?: 'AboveMaxSaldo', error: boolean, message: string, maxSaldo: number, above: number } | { __typename?: 'CreateDepositSuccess', transaction: { __typename?: 'Transaction', id: string, amount: number, user: { __typename?: 'User', id: string, firstName: string, lastName: string, profile?: { __typename?: 'UserProfile', id: string, balance: number } | null } } } | { __typename?: 'FormErrors', error: boolean, message: string, fields: Array<{ __typename?: 'FieldError', field: string, message: string }> } };
|
||||||
|
|
||||||
|
export type OrderListQueryVariables = Exact<{
|
||||||
|
limit?: InputMaybe<Scalars['Int']>;
|
||||||
|
offset?: InputMaybe<Scalars['Int']>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type OrderListQuery = { __typename?: 'Query', orderList: Array<{ __typename?: 'Order', id: string, isExternal: boolean, created: any, orderSum: number, customer?: { __typename?: 'User', id: string, firstName: string, lastName: string, profile?: { __typename?: 'UserProfile', id: string, balance: number } | null } | null, orderlines?: Array<{ __typename?: 'OrderLine', id: string, amount: number, unitPrice: number, product: { __typename?: 'Product', id: string, name: string, salePriceExt: number, salePriceInt: number } }> | null }> };
|
||||||
|
|
||||||
|
export type TransactionListQueryVariables = Exact<{
|
||||||
|
limit?: InputMaybe<Scalars['Int']>;
|
||||||
|
offset?: InputMaybe<Scalars['Int']>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type TransactionListQuery = { __typename?: 'Query', transactionList: Array<{ __typename?: 'Transaction', id: string, created: any, amount: number, user: { __typename?: 'User', id: string, firstName: string, lastName: string, profile?: { __typename?: 'UserProfile', id: string, balance: number } | null } }> };
|
||||||
|
|
||||||
|
export type AllProductsQueryVariables = Exact<{
|
||||||
|
active: Scalars['Boolean'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type AllProductsQuery = { __typename?: 'Query', allProducts: Array<{ __typename?: 'Product', id: string, name: string, imageUrl: string, salePriceExt: number, salePriceInt: number }> };
|
||||||
|
|
||||||
|
export type AllUsersQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
|
export type AllUsersQuery = { __typename?: 'Query', allUsers: Array<{ __typename?: 'User', id: string, firstName: string, lastName: string, profile?: { __typename?: 'UserProfile', id: string, balance: number, image: string, lastPurchaseDate: any } | null }> };
|
||||||
|
|
||||||
|
export type CreateOrderMutationVariables = Exact<{
|
||||||
|
customerId: Scalars['ID'];
|
||||||
|
orderLines: Array<OrderLineInput> | OrderLineInput;
|
||||||
|
isExternal: Scalars['Boolean'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type CreateOrderMutation = { __typename?: 'Mutation', createOrder: { __typename?: 'CreateOrderSuccess', order: { __typename?: 'Order', id: string, isExternal: boolean, orderSum: number, customer?: { __typename?: 'User', id: string, firstName: string, lastName: string, profile?: { __typename?: 'UserProfile', id: string, balance: number } | null } | null, orderlines?: Array<{ __typename?: 'OrderLine', id: string, amount: number, unitPrice: number, product: { __typename?: 'Product', id: string, name: string, salePriceExt: number, salePriceInt: number } }> | null } } | { __typename?: 'FormErrors', error: boolean, message: string, fields: Array<{ __typename?: 'FieldError', field: string, message: string }> } | { __typename?: 'InsufficientFunds', error: boolean, message: string, amountLacking: number } };
|
||||||
|
|
||||||
|
export type AllProductStatsQueryVariables = Exact<{
|
||||||
|
active?: InputMaybe<Scalars['Boolean']>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type AllProductStatsQuery = { __typename?: 'Query', allProducts: Array<{ __typename?: 'Product', id: string, name: string, userCounts: any }> };
|
||||||
|
|
||||||
|
export type OrderStatsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
|
export type OrderStatsQuery = { __typename?: 'Query', orderStats: { __typename?: 'OrderStats', yearly: Array<{ __typename?: 'OrderStatsByTime', period: number, count: string }>, monthly: Array<{ __typename?: 'OrderStatsByTime', period: number, count: string }>, daily: Array<{ __typename?: 'OrderStatsByTime', period: number, count: string }>, hourly: Array<{ __typename?: 'OrderStatsByTime', period: number, count: string }> } };
|
||||||
|
|
||||||
|
export type ProductStatsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
|
export type ProductStatsQuery = { __typename?: 'Query', productStats: Array<{ __typename?: 'ProductOrderStat', productName: string, data: Array<{ __typename?: 'Point', x: string, y: number }> }> };
|
||||||
|
|
||||||
|
export type UserDetailQueryVariables = Exact<{
|
||||||
|
userId: Scalars['ID'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type UserDetailQuery = { __typename?: 'Query', user: { __typename?: 'User', id: string, firstName: string, lastName: string, profile?: { __typename?: 'UserProfile', id: string, balance: number, image: string, lastPurchaseDate: any, orderSumTotal: number, topMonths: Array<{ __typename?: 'TopMonth', period: string, sum: number, count: number }>, productOrderStats: Array<{ __typename?: 'ProductOrderStat', productName: string, data: Array<{ __typename?: 'Point', x: string, y: number }> }> } | null } };
|
||||||
|
|
||||||
|
|
||||||
|
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 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 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":"order"},"value":{"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":"order"},"value":{"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"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"active"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"allProducts"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"active"},"value":{"kind":"Variable","name":{"kind":"Name","value":"active"}}}]}}],"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"}}]}}]}}]} as unknown as DocumentNode<AllProductsQuery, AllProductsQueryVariables>;
|
||||||
|
export const AllUsersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"allUsers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"allUsers"},"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":"image"}},{"kind":"Field","name":{"kind":"Name","value":"lastPurchaseDate"}}]}}]}}]}}]} as unknown as DocumentNode<AllUsersQuery, AllUsersQueryVariables>;
|
||||||
|
export const CreateOrderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOrder"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"customerId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderLines"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"OrderLineInput"}}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"isExternal"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOrder"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"customerId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"customerId"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderLines"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderLines"}}},{"kind":"Argument","name":{"kind":"Name","value":"isExternal"},"value":{"kind":"Variable","name":{"kind":"Name","value":"isExternal"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"CreateOrderSuccess"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isExternal"}},{"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"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"orderSum"}}]}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"InsufficientFunds"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"amountLacking"}}]}},{"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<CreateOrderMutation, CreateOrderMutationVariables>;
|
||||||
|
export const AllProductStatsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"allProductStats"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"active"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"allProducts"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"active"},"value":{"kind":"Variable","name":{"kind":"Name","value":"active"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"userCounts"}}]}}]}}]} as unknown as DocumentNode<AllProductStatsQuery, AllProductStatsQueryVariables>;
|
||||||
|
export const OrderStatsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"orderStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"orderStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"yearly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"period"}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}},{"kind":"Field","name":{"kind":"Name","value":"monthly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"period"}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}},{"kind":"Field","name":{"kind":"Name","value":"daily"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"period"}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}},{"kind":"Field","name":{"kind":"Name","value":"hourly"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"period"}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}}]}}]}}]} as unknown as DocumentNode<OrderStatsQuery, OrderStatsQueryVariables>;
|
||||||
|
export const ProductStatsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"productStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"productStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"productName"}},{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"x"}},{"kind":"Field","name":{"kind":"Name","value":"y"}}]}}]}}]}}]} as unknown as DocumentNode<ProductStatsQuery, ProductStatsQueryVariables>;
|
||||||
|
export const UserDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"userDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"userId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"userId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"userId"}}}],"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":"image"}},{"kind":"Field","name":{"kind":"Name","value":"lastPurchaseDate"}},{"kind":"Field","name":{"kind":"Name","value":"topMonths"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"period"}},{"kind":"Field","name":{"kind":"Name","value":"sum"}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}},{"kind":"Field","name":{"kind":"Name","value":"productOrderStats"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"productName"}},{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"x"}},{"kind":"Field","name":{"kind":"Name","value":"y"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"orderSumTotal"}}]}}]}}]}}]} as unknown as DocumentNode<UserDetailQuery, UserDetailQueryVariables>;
|
||||||
Vendored
+15
@@ -0,0 +1,15 @@
|
|||||||
|
/* svgs */
|
||||||
|
declare module "*.svg" {
|
||||||
|
import { ReactElement, SVGProps } from "react";
|
||||||
|
export const ReactComponent: (props: SVGProps<SVGElement>) => ReactElement;
|
||||||
|
}
|
||||||
|
/* CSS modules */
|
||||||
|
declare module "*.module.css";
|
||||||
|
|
||||||
|
/* Vite env vars */
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_API_URL: string;
|
||||||
|
}
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { format, parseISO } from "date-fns";
|
||||||
|
|
||||||
|
export const formatTime = (isoDateTime: string): string => format(parseISO(isoDateTime), "yyyy-MM-dd HH:mm");
|
||||||
|
|
||||||
|
export const formatMonth = (isoDateTime: string): string => format(parseISO(isoDateTime), "yyyy-MM");
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { parseISO, subDays } from "date-fns";
|
||||||
|
|
||||||
|
import { AllUsersQuery } from "../types";
|
||||||
|
|
||||||
|
const activeLimitDays = 365 / 2;
|
||||||
|
|
||||||
|
type FilterableUser = AllUsersQuery["allUsers"][0];
|
||||||
|
|
||||||
|
export const activeUserFilter = (user: FilterableUser) => {
|
||||||
|
const balance = user.profile?.balance || 0;
|
||||||
|
const active = parseISO(user.profile?.lastPurchaseDate) >= subDays(new Date(), activeLimitDays);
|
||||||
|
return balance >= 15 && active;
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ["stylelint-config-recommended", "stylelint-prettier/recommended", "stylelint-config-idiomatic-order"],
|
||||||
|
plugins: ["stylelint-order"],
|
||||||
|
rules: {
|
||||||
|
// Useful?
|
||||||
|
"no-descending-specificity": null,
|
||||||
|
},
|
||||||
|
ignoreFiles: ["**/*.md", "**/*.svg", "**/*.jpg", "**/*.graphql", "**/*.tsx", "**/*.ts"],
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2021",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable",
|
||||||
|
"ES2021"
|
||||||
|
],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ES2020",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./src",
|
||||||
|
"node_modules/vite/types/importMeta.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/* eslint-disable import/no-extraneous-dependencies */
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import svgr from "vite-plugin-svgr";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
base: process.env.NODE_ENV !== "production" ? "/" : "/static/modern/",
|
||||||
|
plugins: [svgr()],
|
||||||
|
define: {
|
||||||
|
__DEV__: process.env.NODE_ENV !== "production",
|
||||||
|
},
|
||||||
|
envDir: "..",
|
||||||
|
build: { outDir: "../static/modern", emptyOutDir: true },
|
||||||
|
});
|
||||||
+5
-1
@@ -1,10 +1,14 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from bbs.main.models import *
|
|
||||||
|
from main.models import *
|
||||||
|
|
||||||
|
|
||||||
class ProductAdmin(admin.ModelAdmin):
|
class ProductAdmin(admin.ModelAdmin):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Product, ProductAdmin)
|
admin.site.register(Product, ProductAdmin)
|
||||||
|
admin.site.register(InventoryTransaction)
|
||||||
admin.site.register(Order)
|
admin.site.register(Order)
|
||||||
admin.site.register(OrderLine)
|
admin.site.register(OrderLine)
|
||||||
admin.site.register(Transaction)
|
admin.site.register(Transaction)
|
||||||
|
|||||||
File renamed without changes.
@@ -0,0 +1,22 @@
|
|||||||
|
import strawberry
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.interface
|
||||||
|
class Error:
|
||||||
|
message: str
|
||||||
|
|
||||||
|
@strawberry.field()
|
||||||
|
def error(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class FieldError(Error):
|
||||||
|
field: str
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class FormErrors:
|
||||||
|
fields: list[FieldError]
|
||||||
|
message: str = ""
|
||||||
|
error: bool = True
|
||||||
Whitespace-only changes.
@@ -0,0 +1,137 @@
|
|||||||
|
import strawberry
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import transaction
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
from strawberry import ID
|
||||||
|
|
||||||
|
from main import models
|
||||||
|
from main.api.errors import Error, FieldError, FormErrors
|
||||||
|
from main.api.types.inventory import Transaction
|
||||||
|
from main.api.types.shop import Order
|
||||||
|
|
||||||
|
UserModel = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.input
|
||||||
|
class OrderLineInput:
|
||||||
|
product_id: ID
|
||||||
|
amount: int
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class CreateOrderSuccess:
|
||||||
|
order: Order
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class InsufficientFunds(Error):
|
||||||
|
amount_lacking: float
|
||||||
|
|
||||||
|
|
||||||
|
CreateOrderResponse = strawberry.union("CreateOrderResponse", [CreateOrderSuccess, InsufficientFunds, FormErrors])
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.mutation
|
||||||
|
@transaction.atomic
|
||||||
|
def create_order(customer_id: ID, order_lines: list[OrderLineInput], is_external: bool = False) -> CreateOrderResponse:
|
||||||
|
# clean and validate
|
||||||
|
try:
|
||||||
|
user = UserModel.objects.get(pk=customer_id)
|
||||||
|
except (UserModel.DoesNotExist, ValueError):
|
||||||
|
user = None
|
||||||
|
|
||||||
|
if user is None and not is_external:
|
||||||
|
message = "Du har ikke valgt hvem som skal kjøpe."
|
||||||
|
return FormErrors(
|
||||||
|
message=message,
|
||||||
|
fields=[FieldError(field="customer_id", message=message)],
|
||||||
|
)
|
||||||
|
if not order_lines:
|
||||||
|
message = "Du har ikke valgt hva du skal kjøpe."
|
||||||
|
return FormErrors(
|
||||||
|
message=message,
|
||||||
|
fields=[FieldError(field="order_lines", message=message)],
|
||||||
|
)
|
||||||
|
|
||||||
|
ols = []
|
||||||
|
order_sum = 0
|
||||||
|
for ol in order_lines:
|
||||||
|
product = get_object_or_404(models.Product, pk=ol.product_id)
|
||||||
|
price = product.sale_price_ext if is_external else product.sale_price_int
|
||||||
|
ols.append(models.OrderLine(product=product, amount=ol.amount, unit_price=price))
|
||||||
|
order_sum += ol.amount * price
|
||||||
|
product.inventory_amount -= ol.amount
|
||||||
|
product.save()
|
||||||
|
|
||||||
|
if not is_external and user.profile.balance <= order_sum:
|
||||||
|
return InsufficientFunds(
|
||||||
|
message="Du har ikke nok penger på bok.", amount_lacking=abs(order_sum - user.profile.balance)
|
||||||
|
)
|
||||||
|
|
||||||
|
# create
|
||||||
|
order = models.Order.objects.create(customer=user, order_sum=order_sum)
|
||||||
|
for ol in ols:
|
||||||
|
ol.order = order
|
||||||
|
models.OrderLine.objects.bulk_create(ols)
|
||||||
|
|
||||||
|
# update balance
|
||||||
|
if not is_external:
|
||||||
|
user.profile.balance -= order_sum
|
||||||
|
user.profile.save()
|
||||||
|
|
||||||
|
return CreateOrderSuccess(order=order)
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class CreateDepositSuccess:
|
||||||
|
transaction: Transaction
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class AboveMaxSaldo(Error):
|
||||||
|
max_saldo: float
|
||||||
|
above: float
|
||||||
|
|
||||||
|
|
||||||
|
CreateDepositResponse = strawberry.union("CreateDepositResponse", [CreateDepositSuccess, AboveMaxSaldo, FormErrors])
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.mutation
|
||||||
|
@transaction.atomic
|
||||||
|
def create_deposit(user_id: ID, amount: int) -> CreateDepositResponse:
|
||||||
|
# clean and validate
|
||||||
|
try:
|
||||||
|
user = UserModel.objects.get(pk=user_id)
|
||||||
|
except (UserModel.DoesNotExist, ValueError):
|
||||||
|
message = "Du har ikke valgt hvem som sette inn penger."
|
||||||
|
return FormErrors(
|
||||||
|
message=message,
|
||||||
|
fields=[FieldError(field="user_id", message=message)],
|
||||||
|
)
|
||||||
|
if amount <= 0:
|
||||||
|
message = "Du har ikke valgt beløp som skal settes inn."
|
||||||
|
return FormErrors(
|
||||||
|
message=message,
|
||||||
|
fields=[FieldError(field="amount", message=message)],
|
||||||
|
)
|
||||||
|
# 1337 is special <3
|
||||||
|
if amount + user.profile.balance > settings.BBS_SALDO_MAX and amount + user.profile.balance != 1337:
|
||||||
|
return AboveMaxSaldo(
|
||||||
|
message="Du prøver å sette inn for mye penger",
|
||||||
|
max_saldo=settings.BBS_SALDO_MAX,
|
||||||
|
above=amount + user.profile.balance - settings.BBS_SALDO_MAX,
|
||||||
|
)
|
||||||
|
|
||||||
|
trans = models.Transaction.objects.create(user=user, amount=amount)
|
||||||
|
profile = trans.user.profile
|
||||||
|
profile.balance += trans.amount
|
||||||
|
profile.save()
|
||||||
|
|
||||||
|
return CreateDepositSuccess(transaction=trans)
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class ShopMutations:
|
||||||
|
create_order = create_order
|
||||||
|
create_deposit = create_deposit
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import strawberry
|
||||||
|
import strawberry_django
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import transaction
|
||||||
|
from strawberry_django import auto
|
||||||
|
|
||||||
|
from main import models
|
||||||
|
from main.api.types.users import User
|
||||||
|
|
||||||
|
DjangoUser = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.input(DjangoUser)
|
||||||
|
class UserInput:
|
||||||
|
email: auto
|
||||||
|
first_name: auto
|
||||||
|
last_name: auto
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.mutation
|
||||||
|
@transaction.atomic
|
||||||
|
def register_mutation(input: UserInput) -> User:
|
||||||
|
user = DjangoUser.objects.create_user(
|
||||||
|
first_name=input.first_name, last_name=input.last_name, email=input.email, username=input.email
|
||||||
|
)
|
||||||
|
models.UserProfile.objects.create(user=user)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class UsersMutations:
|
||||||
|
create_user = register_mutation
|
||||||
Whitespace-only changes.
@@ -0,0 +1,9 @@
|
|||||||
|
import strawberry
|
||||||
|
import strawberry_django
|
||||||
|
|
||||||
|
from main.api.types.inventory import Transaction, TransactionOrdering
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class InventoryQueries:
|
||||||
|
transaction_list: list[Transaction] = strawberry_django.field(pagination=True, order=TransactionOrdering)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import strawberry
|
||||||
|
import strawberry_django
|
||||||
|
|
||||||
|
from main import models
|
||||||
|
from main.api.types.shop import Order, OrderOrdering, OrderStats, OrderStatsByTime, Point, Product, ProductOrderStat
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class ShopQueries:
|
||||||
|
all_products: list[Product] = strawberry_django.field()
|
||||||
|
all_orders: list[Order] = strawberry_django.field()
|
||||||
|
order_list: list[Order] = strawberry_django.field(pagination=True, order=OrderOrdering)
|
||||||
|
|
||||||
|
@strawberry.field
|
||||||
|
def order_stats(self) -> OrderStats:
|
||||||
|
return OrderStats(
|
||||||
|
**{
|
||||||
|
period: [
|
||||||
|
OrderStatsByTime(**stat)
|
||||||
|
for stat in models.Order.objects.count_by_created_period(period).order_by("period")
|
||||||
|
]
|
||||||
|
for period in ["yearly", "monthly", "daily", "hourly"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@strawberry.field
|
||||||
|
def product_stats(self) -> list[ProductOrderStat]:
|
||||||
|
return [
|
||||||
|
ProductOrderStat(product_name=stat["product_name"], data=[Point(**p) for p in stat["data"]])
|
||||||
|
for stat in models.OrderLine.objects.product_order_stats().as_series()
|
||||||
|
]
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import strawberry
|
||||||
|
import strawberry_django
|
||||||
|
from django.contrib.auth.models import User as DjangoUser
|
||||||
|
from strawberry import ID
|
||||||
|
|
||||||
|
from main.api.types.users import User
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class UsersQueries:
|
||||||
|
all_users: list[User] = strawberry_django.field()
|
||||||
|
|
||||||
|
@strawberry.field
|
||||||
|
def user(self, user_id: ID) -> User:
|
||||||
|
return DjangoUser.objects.get(pk=user_id)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import strawberry
|
||||||
|
|
||||||
|
from main.api.mutations.shop import ShopMutations
|
||||||
|
from main.api.mutations.users import UsersMutations
|
||||||
|
from main.api.queries.inventory import InventoryQueries
|
||||||
|
from main.api.queries.shop import ShopQueries
|
||||||
|
from main.api.queries.users import UsersQueries
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class Query(InventoryQueries, ShopQueries, UsersQueries):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class Mutation(ShopMutations, UsersMutations):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
schema = strawberry.Schema(query=Query, mutation=Mutation)
|
||||||
Whitespace-only changes.
@@ -0,0 +1,18 @@
|
|||||||
|
import strawberry_django
|
||||||
|
from strawberry_django import auto
|
||||||
|
|
||||||
|
from main import models
|
||||||
|
from main.api.types.users import User
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.ordering.order(models.Transaction)
|
||||||
|
class TransactionOrdering:
|
||||||
|
created: auto
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.type(models.Transaction)
|
||||||
|
class Transaction:
|
||||||
|
id: auto
|
||||||
|
user: "User"
|
||||||
|
amount: auto
|
||||||
|
created: auto
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import strawberry
|
||||||
|
import strawberry_django
|
||||||
|
from strawberry_django import auto
|
||||||
|
|
||||||
|
from main import models
|
||||||
|
from main.api.types.utils import JSONScalar
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.filters.filter(models.Product)
|
||||||
|
class ProductFilter:
|
||||||
|
id: auto
|
||||||
|
active: auto
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.type(models.Product, filters=ProductFilter)
|
||||||
|
class Product:
|
||||||
|
id: auto
|
||||||
|
name: auto
|
||||||
|
image_url: str
|
||||||
|
sale_price_int: auto
|
||||||
|
sale_price_ext: auto
|
||||||
|
# FIXME: stricter types
|
||||||
|
user_counts: JSONScalar
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.ordering.order(models.Order)
|
||||||
|
class OrderOrdering:
|
||||||
|
created: auto
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.type(models.Order)
|
||||||
|
class Order:
|
||||||
|
id: auto
|
||||||
|
customer: strawberry.LazyType["User", "main.api.types.users"]
|
||||||
|
order_sum: auto
|
||||||
|
created: auto
|
||||||
|
orderlines: list["OrderLine"]
|
||||||
|
is_external: bool
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.type(models.OrderLine)
|
||||||
|
class OrderLine:
|
||||||
|
id: auto
|
||||||
|
order: Order
|
||||||
|
product: Product
|
||||||
|
amount: auto
|
||||||
|
unit_price: auto
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class OrderStatsByTime:
|
||||||
|
period: int
|
||||||
|
count: str
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class OrderStats:
|
||||||
|
yearly: list[OrderStatsByTime]
|
||||||
|
monthly: list[OrderStatsByTime]
|
||||||
|
daily: list[OrderStatsByTime]
|
||||||
|
hourly: list[OrderStatsByTime]
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class Point:
|
||||||
|
x: str
|
||||||
|
y: int
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class ProductOrderStat:
|
||||||
|
product_name: str
|
||||||
|
data: list[Point]
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import strawberry
|
||||||
|
import strawberry_django
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from strawberry_django import auto
|
||||||
|
|
||||||
|
from main import models
|
||||||
|
from main.api.types.shop import Point, ProductOrderStat
|
||||||
|
|
||||||
|
UserModel = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry.type
|
||||||
|
class TopMonth:
|
||||||
|
period: str
|
||||||
|
count: int
|
||||||
|
sum: int
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.type(models.UserProfile)
|
||||||
|
class UserProfile:
|
||||||
|
id: auto
|
||||||
|
balance: auto
|
||||||
|
image: auto
|
||||||
|
last_purchase_date: datetime
|
||||||
|
order_sum_total: float
|
||||||
|
|
||||||
|
@strawberry.field()
|
||||||
|
def top_months(self) -> list[TopMonth]:
|
||||||
|
return [TopMonth(**month) for month in self.top_months()]
|
||||||
|
|
||||||
|
@strawberry.field()
|
||||||
|
def product_order_stats(self) -> list[ProductOrderStat]:
|
||||||
|
return [
|
||||||
|
ProductOrderStat(product_name=stat["product_name"], data=[Point(**p) for p in stat["data"]])
|
||||||
|
for stat in self.product_order_stats().as_series()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@strawberry_django.type(UserModel)
|
||||||
|
class User:
|
||||||
|
id: auto
|
||||||
|
first_name: auto
|
||||||
|
last_name: auto
|
||||||
|
profile: UserProfile
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import json
|
||||||
|
from typing import Any, NewType
|
||||||
|
|
||||||
|
import strawberry
|
||||||
|
|
||||||
|
JSONScalar = strawberry.scalar(
|
||||||
|
NewType("JSONScalar", Any),
|
||||||
|
serialize=lambda v: v,
|
||||||
|
parse_value=lambda v: json.loads(v),
|
||||||
|
description="The GenericScalar scalar type represents a generic GraphQL scalar value that could be: List or Object.",
|
||||||
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class BBSAppConfig(AppConfig):
|
||||||
|
name = "main"
|
||||||
|
verbose_name = "Biceps Bar System"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
# all models are loaded, could attach signals
|
||||||
|
pass
|
||||||
+16
-13
@@ -1,18 +1,21 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from models import *
|
from main.models import InventoryTransaction, Product
|
||||||
|
|
||||||
class OrderForm(forms.Form):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class OrderLineForm(forms.ModelForm):
|
class InventoryTransactionForm(forms.ModelForm):
|
||||||
#product = forms.CharField(required=True, widget=forms.HiddenInput)
|
unit_price = forms.FloatField(required=True, localize=True)
|
||||||
#amount = forms.IntegerField(required=True)
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# Only show active products
|
||||||
|
super(InventoryTransactionForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields["product"].queryset = Product.objects.filter(active=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderLine
|
model = InventoryTransaction
|
||||||
fields = ('amount','product')
|
fields = ["product", "amount", "unit_price", "comment"]
|
||||||
widgets = {
|
|
||||||
'product': forms.HiddenInput(),
|
|
||||||
'amount': forms.TextInput(attrs={'size': '2'}),
|
class DateRangeForm(forms.Form):
|
||||||
}
|
start_time = forms.DateTimeField(required=False, label="Fra")
|
||||||
# TODO : Validation
|
end_time = forms.DateTimeField(required=False, label="Til")
|
||||||
Loaded 100 of 157 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user