Author SHA1 Message Date
nikolark 6f6fbc648c ✨ Add stats for all purchases over time 2022-02-19 22:45:47 +01:00
nikolark 7474885f14 ✨ User product purchases over time 2022-02-19 22:21:11 +01:00
nikolark f3a6e563bb ➕ Add @nivo/line 2022-02-19 17:35:19 +01:00
nikolark af45b29b1c ✨ Link to create user form 2022-02-19 17:33:02 +01:00
nikolark 64eb6c717c ✨ Add link to legacy admin 2022-02-19 17:30:12 +01:00
nikolark 2e5fbb6fa6 ✨ Split out stats into multiple sub views 2022-02-19 17:21:44 +01:00
nikolark 158f5c5652 📝 Update TODOs 2022-02-19 16:11:21 +01:00
nikolark cfd0d94537 ✨ Working user create form 2022-02-19 15:47:38 +01:00
nikolark b5c9094b64 ✨ Add user register mutation
🔥 Remove a bunch of legacy code
2022-02-19 15:26:30 +01:00
nikolark a47de48243 🔧 Misc 2022-02-19 14:32:33 +01:00
nikolark 41638e605f ♻ Split types, mutations, queries by model 2022-02-19 14:31:05 +01:00
Nikolai R Kristiansen 3274cb96c0 🚧 Start on UserCreate 2022-02-17 09:58:12 +01:00
Nikolai R Kristiansen 5f332cd365 🏷 Simplify types 2022-02-14 10:22:01 +01:00
Nikolai R Kristiansen aa66694fd5 ♻ Refactor Deposit and Register
🏷 Pick a part query types to simplify typing and fix
incompatible types
2022-02-14 10:19:34 +01:00
Nikolai R Kristiansen 8b0aaa287e 🌐 Norwegian formatting 2022-02-12 13:08:19 +01:00
Nikolai R Kristiansen 2e8b732798 💄 Bold numbers 2022-02-12 13:07:00 +01:00
Nikolai R Kristiansen 87396874c9 🔥 Remove legacy profile 2022-02-12 13:05:09 +01:00
Nikolai R Kristiansen e2a84c25a6 ✨ UserDetail and UserList views 2022-02-12 12:58:04 +01:00
Nikolai R Kristiansen ec0549154d ⬆ Bump node deps 2022-02-12 11:39:36 +01:00
Nikolai R Kristiansen ad96873493 🐛 Week starts on monday, not sunday 2022-02-10 09:47:01 +01:00
Nikolai R Kristiansen 9fa6cfe0ee 🔥 Delete legacy register and deposit
♻ Replace facebook avatar related stuff with gravatars
♻ Replace format-strings with f-strings
✨ Add profile_images command
2022-02-10 09:42:57 +01:00
Nikolai R Kristiansen a9f81737d5 ♻ Refactor Bar charts 2022-02-08 22:10:27 +01:00
Nikolai R Kristiansen 5f7a4e6ff2 ♻ Refactor orderStats
✏ Fix daily typo
2022-02-06 22:27:52 +01:00
Nikolai R Kristiansen d5ed841c9c ⬆ Bump dev deps 2022-02-06 22:16:04 +01:00
Nikolai R Kristiansen 9c7fe11bfc 🚸 Stats: Only show active products 2022-02-06 22:11:14 +01:00
Nikolai R Kristiansen df3aff7fb2 💄 Pointer cursor for pagination links 2022-02-06 22:09:24 +01:00
Nikolai R Kristiansen 264e189698 🔥 remove drf from django settings 2022-02-06 22:05:38 +01:00
Nikolai R Kristiansen 7ba3ead688 🔥 Remove stat and log views
➖ Remove djangorestframework
2022-02-06 21:59:13 +01:00
Nikolai R Kristiansen d7513f389e ✨ Make python aware stat and log are SPA views 2022-02-06 21:48:33 +01:00
Nikolai R Kristiansen bd8dbd750a 🏷 Make tsc happy 2022-02-06 21:45:11 +01:00
Nikolai R Kristiansen 7aea1ead45 🐛 Active styles for log and stats 2022-02-06 21:10:37 +01:00
Nikolai R Kristiansen 771e0c2a3b ✨ Add order stats 2022-02-06 21:05:40 +01:00
Nikolai R Kristiansen 6fa6bf1334 💄 Improve product pies 2022-02-06 16:31:18 +01:00
Nikolai R Kristiansen 9e80669a1f ✨ Add some pies 2022-02-06 12:02:09 +01:00
Nikolai R Kristiansen aead00a893 ✨ Add basic pagination in log view 2022-02-05 22:23:24 +01:00
Nikolai R Kristiansen e240eda1cb 🚸 Format datetimes 2022-02-05 22:02:33 +01:00
Nikolai R Kristiansen 1bd8fddf73 ✨ Sort orders and transactions newest first
🔧 Don't run linting using pipenv
2022-02-05 21:56:56 +01:00
Nikolai R Kristiansen ed99dadcb0 ⬆ Bump frontend deps
♻ Replace vite svg plugin
react-router v6 fixes
2022-02-05 21:44:25 +01:00
Nikolai R Kristiansen 87e2f2292c 👷 Deploy from CI using SSH 2022-02-05 21:10:14 +01:00
Nikolai R Kristiansen a2cfd0ceaf ✨ Start log view with orders & transactions 2022-02-02 23:06:33 +01:00
Nikolai R Kristiansen 85698deba5 🔇 Remove deprecated url usage 2022-01-26 23:42:37 +01:00
Nikolai R Kristiansen 1bd06b7e81 ♻️ Remove test helper 2022-01-26 23:41:13 +01:00
Nikolai R Kristiansen 6791000dea 🔇 Stop using deprecated translation functions 2022-01-26 23:27:49 +01:00
Nikolai R Kristiansen d3016a3925 ✨ Make the SPA load for home and register
Link to legacy routes
2022-01-26 23:23:33 +01:00
Nikolai R Kristiansen 717752ddae 🚨 Lint on CI 2022-01-16 22:53:29 +01:00
Nikolai R Kristiansen dcd8edaaa6 🚨 Add pre-commit
Format code
Add bin/run
2022-01-16 22:49:21 +01:00
nikolark 9d3afadbff ✅ Add basic create order test
🎨 Add MutationTest helper to help write mutation tests with strawberry
➕ add pytest-django
⬆ Bump some python deps
2022-01-16 22:30:47 +01:00
nikolark 549b0cfdd6 Update .gitlab-ci.yml file 2021-12-05 00:13:50 +00:00
nikolark 35d4334627 Update .gitlab-ci.yml file 2021-12-05 00:11:18 +00:00
nikolark 5969e70e89 Update .gitlab-ci.yml file 2021-12-05 00:05:57 +00:00
nikolark db7bdd7181 🚛 Move CI config in the right place 2021-12-05 00:49:03 +01:00
nikolark dbda43077e ✨ Make new frontend available under /modern
👷 Start adding deploy config and CI
2021-12-05 00:45:36 +01:00
nikolark bc4414473c ✨ Search as you type
🐛 Fix regression in register mutation
2021-12-04 23:48:36 +01:00
nikolark 2bcc993aaa ✨ Show details about purchase
🐛 Update user balance
2021-12-04 23:30:44 +01:00
nikolark 3d93a645ad ✨ Add basic deposit functionality 2021-11-18 22:58:32 +01:00
nikolark 40b3cb119f 🚸 Make errors red 2021-11-18 22:58:30 +01:00
Nikolai Kristiansen 9576becb62 🚨 Lint styles 2021-11-13 11:48:01 +01:00
Nikolai Kristiansen cf9c23bf7b ⬆️. Bump deps
🛠 Configure npm and node versions
2021-11-13 11:48:01 +01:00
nikolark 576392ba5f 🚸 improve error handling 2021-11-07 17:24:10 +01:00
nikolark 0a435d3224 ✨ Start on error handling
♻ Use TypedDocumentNode's instead of apollo-react-typescript codegen
💄 Add toast
2021-11-06 21:49:28 +01:00
nikolark ed82ca3891 💄 Fixed grid users 2021-11-02 22:34:55 +01:00
nikolark c44e582706 💄 Working register styles 2021-11-02 22:32:50 +01:00
nikolark d5bff90ee3 ✨ Register: working order sum logic
💄 Add more icons and load them as react components
2021-11-01 20:12:30 +01:00
nikolark d86afd9392 ✨ Add product images and basic nav 2021-10-30 19:05:50 +02:00
nikolark 350d31d8d8 ✨ Dont show inactive users in register view 2021-10-30 18:21:16 +02:00
nikolark 46c52b7618 ⬆ Bump deps 2021-10-30 17:48:03 +02:00
nikolark 2ed0c57b97 🚨 Add eslint and stylelint and format 2021-10-30 17:42:21 +02:00
nikolark f0656b34e9 ✨ Register: Show list of active products
🛠 Generate apollo client typescript helpers from graphql schema
2021-10-30 17:29:16 +02:00
nikolark fa89353525 ✨ Add basic graphql API using strawberry 🍓
✨ Add allProducts query
2021-10-30 16:54:28 +02:00
nikolark e2db9052bc ➕ Add pytest and black 2021-10-30 16:05:14 +02:00
Nikolai R Kristiansen aef0509056 🚧 Start adding new frontend boilerplate 2021-10-30 15:53:52 +02:00
nikolark 0e4ef1b75f 🔥 Remove widget_tweaks and bootstrapform use 2021-10-30 15:50:42 +02:00
nikolark 49870595b0 🔥 Remove legacy sentry client 2021-10-30 15:44:43 +02:00
nikolark 799548a90b ♻ Start using poetry 2021-10-30 15:38:36 +02:00
Nikolai R Kristiansen 2a3188667f 🐛 Use correct template tag 2021-10-27 19:25:58 +02:00
Nikolai R Kristiansen a052397990 🚨 format python code using black 2021-10-27 19:22:25 +02:00
Nikolai R Kristiansen 9c4a5b3d84 🔥 Remove legacy stuff 2021-10-27 19:18:07 +02:00
Nikolai R Kristiansen 1a27e3a521 🐛 fix migrations 2021-10-27 19:15:26 +02:00
Nikolai R Kristiansen d311e65058 ⬆️ Major bumps and django checks
Notably to django 3.2
2021-10-27 19:12:07 +02:00
Nikolai R Kristiansen a1437ab5d2 ⬆️ bump deps 2018-08-12 22:14:11 +02:00
Nikolai R Kristiansen 079af129d7 Minor fixes 2018-03-17 14:05:37 +01:00
Nikolai R Kristiansen 90eae21bc7 Add sentry and fabfile 2018-03-17 13:59:04 +01:00
Nikolai R Kristiansen cd11bd580b ⬆️ Bump most deps 2018-03-17 13:41:19 +01:00
Nikolai R Kristiansen 72562c931d Try again 2017-09-27 22:04:27 +02:00
Nikolai R Kristiansen a84c94e23b Highcharts is back 2017-09-27 22:01:15 +02:00
Nikolai R Kristiansen 16bc430369 Remove bower, bump some deps, fix log img size 2017-09-27 21:59:17 +02:00
Nikolai R Kristiansen 8fb2389d2c Fix some Django 1.11 stuff 2017-09-27 21:15:27 +02:00
Nikolai R Kristiansen c1ef6f6678 pur 2017-09-27 20:59:30 +02:00
Toralf Neby Berge f0e6331630 fix 2017-05-03 22:15:00 +02:00
Toralf Neby Berge 505b9d73d5 majur bug in admin interface 2017-05-03 21:41:28 +02:00
nikolark 2100408b72 Cheat 2016-04-13 21:20:26 +02:00
nikolark 8a4a9fcb26 User creation fixes 2016-04-13 21:17:02 +02:00
nikolark 21c4f11ebc go back 2015-11-30 11:31:00 +01:00
nikolark 35835fc64f ? 2015-11-30 11:29:25 +01:00
nikolark 6e11410972 Open up product stats 2015-11-30 11:24:29 +01:00
nikolark 63f0056898 Oops 2015-11-29 18:56:20 +01:00
nikolark e260b3d5bd Merge branch 'restfest' 2015-11-29 18:54:02 +01:00
nikolark 17545b5261 More stats! 2015-11-29 18:51:31 +01:00
nikolark 6560d3e89b Thumbnail products 2015-11-29 17:00:43 +01:00
nikolark 31cffdb24b Style fixes 2015-11-29 16:35:20 +01:00
nikolark 2b5c3529d6 Initial attempt at using Service Worker
- Caches facebook profile pictures and static resources
- Removes django-compressor and commited bower_components
- rest fixes
2015-11-29 16:17:25 +01:00
nikolark 195c49b0fd Moves product stat view logic to serializer
- Split frontend into multiple files
- Bump frontend deps
- Remove unused stuff
2015-11-28 21:21:58 +01:00
nikolark ff72b8dd0b Allow exact purchases 2015-11-25 20:31:21 +01:00
nikolark 3b7980ffe9 Start using rest framework 2015-11-25 20:29:25 +01:00
nikolark de85988039 Minor 2015-11-22 12:54:47 +01:00
nikolark ec638d51f9 Python 3 fixes 2015-11-22 12:51:12 +01:00
nikolark b69f917d57 Lazy trans 2015-11-21 21:47:47 +01:00
nikolark 42e25f57a4 gunicorn dep 2015-11-21 21:38:18 +01:00
608 changed files with 26836 additions and 100790 deletions

No files matched your search

+8 -5
View File
@@ -4,8 +4,11 @@ local_settings.py
db.sqlite3
static/.sass-cache
uploads/[^.]*
venv
node_modules
.idea
/CACHE
media
venv/
node_modules/
.idea/
CACHE/
media/
tmp/
frontend/dist
static/modern
+35
View File
@@ -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
+18
View File
@@ -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]
+34 -14
View File
@@ -1,16 +1,36 @@
# Installation
sudo apt-get install libmysqlclient-dev
sudo apt-get build-dep python-imaging
virtualenv --distribute venv
. venv/bin/active
pip install -r requirements
python manage.py syncdb
python manage.py runserver
# Getting started
# TODO
* gulpfile.js: reload on js update, csso
* Larger pictures
* Compact GUI for max 768 vertical pixels
* Search right of in title
* Light on dark colors
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
+93 -57
View File
@@ -1,5 +1,6 @@
# Django settings for bbs project.
import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
DEBUG = True
@@ -10,90 +11,87 @@ ADMINS = (
MANAGERS = ADMINS
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'db.sqlite3',
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": "db.sqlite3",
},
}
TIME_ZONE = 'Europe/Oslo'
LANGUAGE_CODE = 'nb'
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/'
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = "/media/"
# Static files
STATIC_ROOT = ''
STATIC_URL = '/static/'
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),
)
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',
'compressor.finders.CompressorFinder',
"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'
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'),
"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.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',
"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_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
)
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'
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',
'widget_tweaks',
'bootstrapform',
'compressor',
'django_extensions',
)
LOCAL_APPS = (
'main',
)
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 = '/'
LOGIN_REDIRECT_URL = "/"
SESSION_COOKIE_AGE = 1209600 # 15 minutes
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
@@ -101,8 +99,46 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = True
BBS_SALDO_MAX = 600
BBS_LIMIT_DEPOSITS = False
if not DEBUG:
COMPRESS_ENABLED = True
# 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 *
+12 -15
View File
@@ -1,25 +1,22 @@
from django.conf import settings
from django.conf.urls import include, url
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
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 = [
url(r'', include(main_urls)),
url(r'^accounts/login/$', 'django.contrib.auth.views.login', name='login'),
url(r'^accounts/logout/$', 'main.views.users.logout', name='logout'),
url(r'^admin/', include(admin.site.urls)),
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 += staticfiles_urlpatterns()
if settings.DEBUG:
urlpatterns += [
url(r'^media/(?P<path>.*)$', 'django.views.static.serve', {
'document_root': settings.MEDIA_ROOT,
})
]
urlpatterns += static("/media", document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
+4 -3
View File
@@ -1,6 +1,7 @@
import os
import django.core.handlers.wsgi
os.environ['DJANGO_SETTINGS_MODULE'] = 'bbssite.settings'
from django.core.wsgi import get_wsgi_application
application = django.core.handlers.wsgi.WSGIHandler()
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bbssite.settings")
application = get_wsgi_application()
Executable
+14
View File
@@ -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
View File
@@ -0,0 +1,6 @@
#!/bin/bash
SRC_DIR="main bbssite"
black $SRC_DIR
isort $SRC_DIR
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
python manage.py export_schema
cd frontend && npm run generate
Executable
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
SRC_DIR="main bbssite"
black --check $SRC_DIR
isort --diff $SRC_DIR
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
python manage.py runserver
+1
View File
@@ -0,0 +1 @@
cd frontend && npm run start
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
pytest -v
+19
View File
@@ -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:
+1
View File
@@ -0,0 +1 @@
src/types.ts
+53
View File
@@ -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",
},
},
};
+12
View File
@@ -0,0 +1,12 @@
{
"name": "bbs-schema",
"schemaPath": "../schema.graphql",
"extensions": {
"endpoints": {
"default": {
"url": "http://localhost:8000/graphql/",
"introspect": false
}
}
}
}
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+1
View File
@@ -0,0 +1 @@
v16
+3
View File
@@ -0,0 +1,3 @@
{
"printWidth": 120
}
+8
View File
@@ -0,0 +1,8 @@
schema: ../schema.graphql
documents: ./src/**/*.graphql
generates:
./src/types.ts:
plugins:
- typescript
- typescript-operations
- typed-document-node
+13
View File
@@ -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>
+22024
View File
File diff suppressed because it is too large. Load diff
+69
View File
@@ -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"
}
}
+3
View File
@@ -0,0 +1,3 @@
.app {
margin: 16px;
}
+27
View File
@@ -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;
+26
View File
@@ -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>
);
+82
View File
@@ -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,/9j/4AAQSkZJRgABAgEBLAEsAAD/7AARRHVja3kAAQAEAAAAHgAA/+4AIUFkb2JlAGTAAAAAAQMA EAMCAwYAAAJ5AAAEcgAABnr/2wCEABALCwsMCxAMDBAXDw0PFxsUEBAUGx8XFxcXFx8eFxoaGhoX Hh4jJSclIx4vLzMzLy9AQEBAQEBAQEBAQEBAQEABEQ8PERMRFRISFRQRFBEUGhQWFhQaJhoaHBoa JjAjHh4eHiMwKy4nJycuKzU1MDA1NUBAP0BAQEBAQEBAQEBAQP/CABEIAIUAhQMBIgACEQEDEQH/ xACQAAEAAgMBAQAAAAAAAAAAAAAAAwUEBgcCAQEBAAAAAAAAAAAAAAAAAAAAABAAAAQFAwUBAQEA AAAAAAAAAAECBTIDBAY2FBUnMBETNBYiEmARAAEDAQMMAgIDAAAAAAAAAAABAgQyMXKzMBEhgRLS A5OjNHRFExQQIkFiFRIBAAAAAAAAAAAAAAAAAAAAYP/aAAwDAQACEQMRAAAApKawrD2tMooWw/TX WxDXWxDXWxfDXmwQlKzaku2KMjz6kNzt/FuYX3N+mCzhgs4YPzPFdh3kJo+j9J5yZAMieCc6TZ1t kfQAAAI5PBrPNemczJwZE8E50qyrbI+gAAAePfg1vmfTOZk4MieCc6VZVtkfQAAAPHvwa3zPpnMy cGRPBOdKsqyzPoAAAHj34Nb5n0vmhODIliHTbbW7oy0P0lRCVEJUQkj84hS8333QDKBkYmXSG72P O/R0j7zYdKc1HSnNR0lzYdGxtC+Gz6x5jLQGz1gAAAAAAAZoP//aAAgBAgABBQD/ACP/2gAIAQMA AQUA/wAj/9oACAEBAAEFALwc3KTcm8O43h3G8O43h3G8O43h3G8O43h3G8O43h3G8O43h3G8O43h 3G8O4lujmdu3rlFJTec5LESyTbKTBWukfLJHyyR8skfLJHyyR8skfLJB2wkhMtxKRUNBSymo8a5W M3rlDDL/ALU30SVJlN6exN6BoEDQIGgQNAgaBA0CAdAgKb0Cob0kTpSElLgn+aiVjN65RbSe6myW X8JSRF0aiWRpeZZEl0LtVSsZvXKLYibC/HSmwvRfh19uVjN65Ra8TbB0pkL3A7e3Kxm9coteJtg6 UyF6gdvblYzeuUWvE2wdKZC9QO3tysZvXKLXibYOlMheoHb25WM3rlFrxNsHSmQvUDt7crGb1yi1 4m2DpTIXqB29uVjN65RbETaf46U2F6P8O3tysZvXKLZPspsX+CPuXRnK7Jel/h19qVjN65Rb6/5U 3VJEmVVJMinoHmQPMgeZA8yB5kDzIHnQFVCSFTVF2dp5GlyPvUysZvXKGueUpVI6pQUt7SRE+oG+ oG+oG+oG+oG+oG+oG+oCnxAnPKTKucUrKqX/AHOlYzeuUJUpIKpnENZUDW1I1tSNbUjXVI11SNdU jW1I1tSNbUg6uoMHPmmDMzErGbk+L3vjwceDjwceDjwceDjwceDjwceDjwceDjwceDjwI+H2X//a AAgBAgIGPwAj/9oACAEDAgY/ACP/2gAIAQEBBj8AncLgy+Nw+G17dljOI9rU/RtiIp30jnP3jvpH OfvHfSOc/eO+kc5+8d9I5z9476Rzn7x30jnP3jvpHOfvHfSOc/eO+kc5+8d9I5z9476Rzn7x30jn P3jvpHOfvHfSOc/eJPEWZx/kSbHaj/lftI1eFIVUz57FzIT77cNn4TQUlJSUlJSUlJSUlIugVpJ8 6PgySffbhsNYmgsLCwsLCwsLCwsF0DtAqEnzo+DJJ99uGw1iZNRw4k+dHwZJPvtw2GsTJqOHEnzo +DJJ99uGw1iZNRw4k+dHwZJPvtw2GsTJqOHEnzo+DJJ99uGw1iZNRw4k+dHwZJPvtw2GsTJqOHEn zo+DJJ99uGw1iZNRw4k+dHwZJPvtw2GsTJqOHEnzo+DJJ99uGw1iZNRw4k+dHwZJPvtw2GsTJqOH Enzo+DJJ99uGw1iaS38WlpaWlpaWloukdpHKSfOj4Mkn324bC3+RP2KioqKioqKioqKioXSKpJ86 PgySffbhsNC5jQ5StStStStStStStStStStStTS40knzo+DJJf8Ao/d+5tN+b4tj48+y3Ns59Nh7 Hpnsemex6Z7Hpnsemex6Z7Hpnsemex6Z7Hpnsemex6Z7Hpnsemex6Zxtn731PtcHbo2/l+PjbGb+ 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

+8
View File
@@ -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;
+13
View File
@@ -0,0 +1,13 @@
.logo {
position: relative;
top: -1px;
display: inline-block;
width: 24px;
height: 24px;
margin: 0 8px;
}
.navIcon {
position: relative;
top: -2px;
}
+90
View File
@@ -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;
}
+26
View File
@@ -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>
);
};
+5
View File
@@ -0,0 +1,5 @@
.toast {
position: absolute;
bottom: 32px;
left: 32px;
}
+41
View File
@@ -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>
);
};
+51
View File
@@ -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;
}
+31
View File
@@ -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>
);
};
+49
View File
@@ -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;
}
+67
View File
@@ -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,
};
};
+5
View File
@@ -0,0 +1,5 @@
.log {
display: grid;
gap: 1rem;
grid-template-columns: repeat(2, 1fr);
}
+145
View File
@@ -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;
}
+70
View File
@@ -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>
);
};
+41
View File
@@ -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;
}
+110
View File
@@ -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>
);
};
+36
View File
@@ -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
}
}
}
+21
View File
@@ -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 };
}
+11
View File
@@ -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")
);
+339
View File
@@ -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>;
+15
View File
@@ -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;
}
+5
View File
@@ -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");
+13
View File
@@ -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;
};
+9
View File
@@ -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"],
};
+27
View File
@@ -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"
]
}
+14
View File
@@ -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 },
});
+2
View File
@@ -1,10 +1,12 @@
from django.contrib import admin
from main.models import *
class ProductAdmin(admin.ModelAdmin):
pass
admin.site.register(Product, ProductAdmin)
admin.site.register(InventoryTransaction)
admin.site.register(Order)
View File
Whitespace-only changes.
+22
View File
@@ -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
View File
Whitespace-only changes.
+137
View File
@@ -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
+32
View File
@@ -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
View File
Whitespace-only changes.
+9
View File
@@ -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)
+31
View File
@@ -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()
]
+15
View File
@@ -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)
+20
View File
@@ -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)
View File
Whitespace-only changes.
+18
View File
@@ -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
+73
View File
@@ -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]
+46
View File
@@ -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
+11
View File
@@ -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.",
)
+10
View File
@@ -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
+5 -74
View File
@@ -1,75 +1,6 @@
# coding: utf-8
from __future__ import unicode_literals
from django import forms
from django.contrib.auth.models import User
from django.utils.translation import ugettext as _
from main.models import Order, Transaction, InventoryTransaction, Product
from main.utils import format_username
class OrderForm(forms.ModelForm):
customer_name = forms.CharField(required=False)
def clean(self):
if self.cleaned_data['customer'] is None and self.cleaned_data['customer_name'] != "Ekstern":
raise forms.ValidationError(_("Du har ikke valgt hvem som skal kjøpe."))
return self.cleaned_data
def clean_customer(self):
if self.cleaned_data['customer'] is None:
return self.cleaned_data['customer']
if self.cleaned_data['customer'].profile.balance <= 0:
raise forms.ValidationError(_("Du har negativ saldo og kan ikke kjøpe noe."))
return self.cleaned_data['customer']
class Meta:
model = Order
fields = ('customer',)
class OrderLineForm(forms.Form):
product = forms.IntegerField(required=True)
amount = forms.IntegerField(required=True)
unit_price_int = forms.FloatField(required=True, localize=True)
unit_price_ext = forms.FloatField(required=True, localize=True)
class DepositForm(forms.ModelForm):
class Meta:
model = Transaction
fields = ['user', 'amount']
class SimpleCreateUserForm(forms.Form):
first_name = forms.CharField(required=True, label="Fornavn")
last_name = forms.CharField(required=True, label="Etternavn")
email = forms.EmailField(required=True, label="E-post")
facebook_username = forms.CharField(required=False, label="Facebook-brukernavn")
def _get_facebook_url(self):
if self.cleaned_data['facebook_username'] == "":
return ""
return "https://graph.facebook.com/{0}/picture".format(self.cleaned_data['facebook_username'])
def save(self, commit=True):
username = format_username(self.cleaned_data['first_name'], self.cleaned_data['last_name'])
user = User.objects.create_user(username, email=self.cleaned_data['email'])
user.first_name = self.cleaned_data['first_name']
user.last_name = self.cleaned_data['last_name']
profile = user.profile
profile.image = self._get_facebook_url()
if commit:
user.save()
profile.save()
return user
from main.models import InventoryTransaction, Product
class InventoryTransactionForm(forms.ModelForm):
@@ -78,13 +9,13 @@ class InventoryTransactionForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
# Only show active products
super(InventoryTransactionForm, self).__init__(*args, **kwargs)
self.fields['product'].queryset = Product.objects.filter(active=True)
self.fields["product"].queryset = Product.objects.filter(active=True)
class Meta:
model = InventoryTransaction
fields = ['product', 'amount', 'unit_price', 'comment']
fields = ["product", "amount", "unit_price", "comment"]
class DateRangeForm(forms.Form):
start_time = forms.DateTimeField(required=False, label=_("Fra"))
end_time = forms.DateTimeField(required=False, label=_("Til"))
start_time = forms.DateTimeField(required=False, label="Fra")
end_time = forms.DateTimeField(required=False, label="Til")
+1 -1
View File
@@ -1 +1 @@
__author__ = 'Nikolai'
__author__ = "Nikolai"
+1 -1
View File
@@ -1 +1 @@
__author__ = 'Nikolai'
__author__ = "Nikolai"
+23
View File
@@ -0,0 +1,23 @@
from pathlib import Path
from django.core.management import BaseCommand, CommandParser
from strawberry.printer import print_schema
from bbssite.settings import BASE_DIR
from main.api.schema import schema as schema_symbol
class Command(BaseCommand):
help = " Write graphql schema to file or stdout"
def add_arguments(self, parser: CommandParser):
parser.add_argument("--filename", default="schema.graphql", help="`-` prints to stdout")
def handle(self, *args, filename=None, **options):
schema_output = print_schema(schema_symbol)
if filename != "-":
with Path(BASE_DIR, filename).open("w+") as fp:
fp.write(schema_output)
self.stdout.write(self.style.SUCCESS("OK"))
else:
print(schema_output)
-47
View File
@@ -1,47 +0,0 @@
# coding: utf-8
from django.contrib.auth.models import User
import difflib
from django.core.management.base import BaseCommand, CommandError
import facebook
from main.models import UserProfile
class Command(BaseCommand):
"""
- Fetches members of KAK Facebook group
- Matches names against the local DB
- Saves the users Facebook picture in his/her user profile.
"""
KAK_GROUP_ID = '108265352570560'
graph = None
def get_kak_members(self):
members = self.graph.get_connections(id=self.KAK_GROUP_ID, connection_name='members')
return [(member['name'], member['id']) for member in members['data']]
def handle(self, *args, **options):
if len(args) != 1:
raise CommandError('Missing access token')
access_token = args[0]
self.graph = facebook.GraphAPI(access_token=access_token)
group_members = self.get_kak_members()
names = [] # to fuzzy search for a name
name_lookup = {} # to update the matching UserProfile object
for u in User.objects.all().order_by('id'):
fullname = u'{} {}'.format(u.first_name, u.last_name)
names.append(fullname)
name_lookup[fullname] = u.id
for member in group_members:
match = difflib.get_close_matches(member[0], names, 1) # max 1 match
if match:
u = UserProfile.objects.get(pk=name_lookup[match[0]])
if str(u.image) is "":
u.image = 'https://graph.facebook.com/{0}/picture'.format(member[1])
u.save()
@@ -0,0 +1,20 @@
from django.core.management.base import BaseCommand, CommandParser
from main.models import UserProfile
class Command(BaseCommand):
"""Set profile image URL for all users"""
def add_arguments(self, parser: CommandParser):
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--force", action="store_true")
def handle(self, *args, dry_run=False, force=False, **options):
for profile in UserProfile.objects.all().order_by("id"):
if dry_run:
print(profile.user.email, profile.image_url())
else:
if force or not profile.image:
profile.image = profile.image_url()
profile.save()
+140 -56
View File
@@ -1,8 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
@@ -13,89 +10,176 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
name='InventoryTransaction',
name="InventoryTransaction",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('amount', models.FloatField(verbose_name='Antall')),
('unit_price', models.FloatField(verbose_name='Enhetspris')),
('comment', models.TextField(verbose_name='Kommentar', blank=True)),
('created', models.DateTimeField(auto_now_add=True)),
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("amount", models.FloatField(verbose_name="Antall")),
("unit_price", models.FloatField(verbose_name="Enhetspris")),
("comment", models.TextField(verbose_name="Kommentar", blank=True)),
("created", models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='Order',
name="Order",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('order_sum', models.FloatField(db_column='sum')),
('created', models.DateTimeField(auto_now_add=True)),
('customer', models.ForeignKey(related_name='orders', blank=True, to=settings.AUTH_USER_MODEL, null=True)),
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("order_sum", models.FloatField(db_column="sum")),
("created", models.DateTimeField(auto_now_add=True)),
(
"customer",
models.ForeignKey(
related_name="orders",
on_delete=models.CASCADE,
blank=True,
to=settings.AUTH_USER_MODEL,
null=True,
),
),
],
),
migrations.CreateModel(
name='OrderLine',
name="OrderLine",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('amount', models.IntegerField()),
('unit_price', models.FloatField(verbose_name='Enhetspris')),
('order', models.ForeignKey(related_name='orderlines', to='main.Order')),
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("amount", models.IntegerField()),
("unit_price", models.FloatField(verbose_name="Enhetspris")),
(
"order",
models.ForeignKey(
on_delete=models.CASCADE,
related_name="orderlines",
to="main.Order",
),
),
],
),
migrations.CreateModel(
name='Product',
name="Product",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=64)),
('sale_price_int', models.FloatField()),
('sale_price_ext', models.FloatField()),
('description', models.CharField(max_length=256, blank=True)),
('volume_liter', models.FloatField(null=True)),
('alcohol_percent', models.FloatField(null=True)),
('image', models.ImageField(upload_to='products', blank=True)),
('customer_support', models.CharField(max_length=32, blank=True)),
('inventory_amount', models.FloatField(default=0)),
('active', models.BooleanField(default=True)),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("name", models.CharField(max_length=64)),
("sale_price_int", models.FloatField()),
("sale_price_ext", models.FloatField()),
("description", models.CharField(max_length=256, blank=True)),
("volume_liter", models.FloatField(null=True)),
("alcohol_percent", models.FloatField(null=True)),
("image", models.ImageField(upload_to="products", blank=True)),
("customer_support", models.CharField(max_length=32, blank=True)),
("inventory_amount", models.FloatField(default=0)),
("active", models.BooleanField(default=True)),
("created", models.DateTimeField(auto_now_add=True)),
("updated", models.DateTimeField(auto_now=True)),
],
options={
'ordering': ['name'],
"ordering": ["name"],
},
),
migrations.CreateModel(
name='Transaction',
name="Transaction",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('amount', models.FloatField()),
('created', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(related_name='transactions', to=settings.AUTH_USER_MODEL)),
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("amount", models.FloatField()),
("created", models.DateTimeField(auto_now_add=True)),
(
"user",
models.ForeignKey(
on_delete=models.CASCADE,
related_name="transactions",
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.CreateModel(
name='UserProfile',
name="UserProfile",
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('balance', models.FloatField(default=0.0)),
('image', models.URLField(blank=True)),
('user', models.OneToOneField(related_name='profile', to=settings.AUTH_USER_MODEL)),
(
"id",
models.AutoField(
verbose_name="ID",
serialize=False,
auto_created=True,
primary_key=True,
),
),
("balance", models.FloatField(default=0.0)),
("image", models.URLField(blank=True)),
(
"user",
models.OneToOneField(
on_delete=models.CASCADE,
related_name="profile",
to=settings.AUTH_USER_MODEL,
),
),
],
),
migrations.AddField(
model_name='orderline',
name='product',
field=models.ForeignKey(related_name='orderlines', to='main.Product'),
model_name="orderline",
name="product",
field=models.ForeignKey(on_delete=models.CASCADE, related_name="orderlines", to="main.Product"),
),
migrations.AddField(
model_name='inventorytransaction',
name='product',
field=models.ForeignKey(related_name='transactions', verbose_name='Produkt', to='main.Product'),
model_name="inventorytransaction",
name="product",
field=models.ForeignKey(
on_delete=models.CASCADE,
related_name="transactions",
verbose_name="Produkt",
to="main.Product",
),
),
migrations.AddField(
model_name='inventorytransaction',
name='user',
field=models.ForeignKey(related_name='inventory_transactions', to=settings.AUTH_USER_MODEL),
model_name="inventorytransaction",
name="user",
field=models.ForeignKey(
on_delete=models.CASCADE,
related_name="inventory_transactions",
to=settings.AUTH_USER_MODEL,
),
),
migrations.AlterUniqueTogether(
name='orderline',
unique_together=set([('order', 'product')]),
name="orderline",
unique_together=set([("order", "product")]),
),
]
Loaded 100 of 608 files, more files were not shown because too many files have changed in this diff. Show more