Compare commits
2
Commits
feature/logout
..
ig
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a2ce30edd | ||
|
|
4484195ddb |
No files matched your search
@@ -26,7 +26,6 @@ npm start
|
|||||||
- improve product listing (light purple background)
|
- improve product listing (light purple background)
|
||||||
- Stats: products over time
|
- Stats: products over time
|
||||||
- inventory views
|
- inventory views
|
||||||
- Stats: Implement users tab
|
|
||||||
|
|
||||||
|
|
||||||
## TODO-old
|
## TODO-old
|
||||||
|
|||||||
@@ -7,9 +7,16 @@ class ProductAdmin(admin.ModelAdmin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class CardAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ("name", "user", "card_id", "active")
|
||||||
|
list_filter = ("active",)
|
||||||
|
search_fields = ("user__username", "name")
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Product, ProductAdmin)
|
admin.site.register(Product, ProductAdmin)
|
||||||
admin.site.register(InventoryTransaction)
|
admin.site.register(InventoryTransaction)
|
||||||
admin.site.register(Order)
|
admin.site.register(Order)
|
||||||
admin.site.register(OrderLine)
|
admin.site.register(OrderLine)
|
||||||
admin.site.register(Transaction)
|
admin.site.register(Transaction)
|
||||||
admin.site.register(UserProfile)
|
admin.site.register(UserProfile)
|
||||||
|
admin.site.register(Card, CardAdmin)
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List
|
from typing import List
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
import strawberry
|
import strawberry
|
||||||
import strawberry_django
|
import strawberry_django
|
||||||
@@ -26,7 +25,7 @@ class UserProfile:
|
|||||||
balance: auto
|
balance: auto
|
||||||
image: auto
|
image: auto
|
||||||
last_purchase_date: datetime
|
last_purchase_date: datetime
|
||||||
order_sum_total: Decimal
|
order_sum_total: float
|
||||||
|
|
||||||
@strawberry.field()
|
@strawberry.field()
|
||||||
def top_months(self) -> List[TopMonth]:
|
def top_months(self) -> List[TopMonth]:
|
||||||
|
|||||||
+1
-3
@@ -2,11 +2,9 @@ from django import forms
|
|||||||
|
|
||||||
from bbs.models import InventoryTransaction, Product
|
from bbs.models import InventoryTransaction, Product
|
||||||
|
|
||||||
MAX_DIGITS = 9
|
|
||||||
DECIMAL_PLACES = 1
|
|
||||||
|
|
||||||
class InventoryTransactionForm(forms.ModelForm):
|
class InventoryTransactionForm(forms.ModelForm):
|
||||||
unit_price = forms.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, required=True, localize=True)
|
unit_price = forms.FloatField(required=True, localize=True)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
# Only show active products
|
# Only show active products
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
# Generated by Django 4.0.2 on 2022-03-05 19:26
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("bbs", "0001_initial"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="inventorytransaction",
|
|
||||||
name="amount",
|
|
||||||
field=models.IntegerField(verbose_name="Antall"),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="inventorytransaction",
|
|
||||||
name="unit_price",
|
|
||||||
field=models.DecimalField(decimal_places=1, max_digits=9, verbose_name="Enhetspris"),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="orderline",
|
|
||||||
name="unit_price",
|
|
||||||
field=models.DecimalField(decimal_places=1, max_digits=9, verbose_name="Enhetspris"),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="product",
|
|
||||||
name="alcohol_percent",
|
|
||||||
field=models.DecimalField(decimal_places=1, max_digits=9, null=True),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="product",
|
|
||||||
name="inventory_amount",
|
|
||||||
field=models.IntegerField(default=0),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="product",
|
|
||||||
name="sale_price_ext",
|
|
||||||
field=models.DecimalField(decimal_places=1, max_digits=9),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="product",
|
|
||||||
name="sale_price_int",
|
|
||||||
field=models.DecimalField(decimal_places=1, max_digits=9),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="product",
|
|
||||||
name="volume_liter",
|
|
||||||
field=models.DecimalField(decimal_places=1, max_digits=9, null=True),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="transaction",
|
|
||||||
name="amount",
|
|
||||||
field=models.DecimalField(decimal_places=1, max_digits=9),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="userprofile",
|
|
||||||
name="balance",
|
|
||||||
field=models.DecimalField(decimal_places=1, default=0, max_digits=9),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 4.0.2 on 2023-06-28 19:11
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('bbs', '0002_alter_inventorytransaction_amount_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='order',
|
|
||||||
name='order_sum',
|
|
||||||
field=models.DecimalField(db_column='sum', decimal_places=1, max_digits=9),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
+25
-20
@@ -2,7 +2,6 @@ import hashlib
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
from typing import Type
|
from typing import Type
|
||||||
from decimal import Decimal
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@@ -18,8 +17,6 @@ from django.db.models.functions import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
EXTERNAL_USER = "Ekstern"
|
EXTERNAL_USER = "Ekstern"
|
||||||
MAX_DIGITS = 9
|
|
||||||
DECIMAL_PLACES = 1
|
|
||||||
|
|
||||||
|
|
||||||
def _format_counts(per_user):
|
def _format_counts(per_user):
|
||||||
@@ -39,15 +36,14 @@ def _format_counts(per_user):
|
|||||||
|
|
||||||
class Product(models.Model):
|
class Product(models.Model):
|
||||||
name = models.CharField(max_length=64)
|
name = models.CharField(max_length=64)
|
||||||
sale_price_int = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES)
|
sale_price_int = models.FloatField()
|
||||||
sale_price_ext = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES)
|
sale_price_ext = models.FloatField()
|
||||||
description = models.CharField(max_length=256, blank=True)
|
description = models.CharField(max_length=256, blank=True)
|
||||||
volume_liter = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, null=True)
|
volume_liter = models.FloatField(null=True)
|
||||||
alcohol_percent = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, null=True)
|
alcohol_percent = models.FloatField(null=True)
|
||||||
image = models.ImageField(upload_to="products", blank=True)
|
image = models.ImageField(upload_to="products", blank=True)
|
||||||
customer_support = models.CharField(max_length=32, blank=True)
|
customer_support = models.CharField(max_length=32, blank=True)
|
||||||
# FIXME: rename to inventory_quantity
|
inventory_amount = models.FloatField(default=0)
|
||||||
inventory_amount = models.IntegerField(default=0)
|
|
||||||
active = models.BooleanField(default=True)
|
active = models.BooleanField(default=True)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
updated = models.DateTimeField(auto_now=True)
|
updated = models.DateTimeField(auto_now=True)
|
||||||
@@ -113,7 +109,7 @@ class Order(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
related_name="orders",
|
related_name="orders",
|
||||||
)
|
)
|
||||||
order_sum = models.DecimalField(db_column="sum", max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES)
|
order_sum = models.FloatField(db_column="sum")
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
objects = OrderManager()
|
objects = OrderManager()
|
||||||
@@ -152,10 +148,8 @@ class OrderLineManager(models.QuerySet):
|
|||||||
class OrderLine(models.Model):
|
class OrderLine(models.Model):
|
||||||
order = models.ForeignKey("bbs.Order", on_delete=models.CASCADE, related_name="orderlines")
|
order = models.ForeignKey("bbs.Order", on_delete=models.CASCADE, related_name="orderlines")
|
||||||
product = models.ForeignKey("bbs.Product", on_delete=models.CASCADE, related_name="orderlines")
|
product = models.ForeignKey("bbs.Product", on_delete=models.CASCADE, related_name="orderlines")
|
||||||
# FIXME: rename to quantity
|
|
||||||
amount = models.IntegerField()
|
amount = models.IntegerField()
|
||||||
# FIXME: rename to price
|
unit_price = models.FloatField(verbose_name="Enhetspris")
|
||||||
unit_price = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, verbose_name="Enhetspris")
|
|
||||||
|
|
||||||
objects = OrderLineManager.as_manager()
|
objects = OrderLineManager.as_manager()
|
||||||
|
|
||||||
@@ -174,7 +168,7 @@ class Transaction(models.Model):
|
|||||||
"""Money"""
|
"""Money"""
|
||||||
|
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="transactions")
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="transactions")
|
||||||
amount = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES)
|
amount = models.FloatField()
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@@ -195,10 +189,8 @@ class InventoryTransaction(models.Model):
|
|||||||
related_name="transactions",
|
related_name="transactions",
|
||||||
verbose_name="Produkt",
|
verbose_name="Produkt",
|
||||||
)
|
)
|
||||||
# FIXME: rename to quantity
|
amount = models.FloatField(verbose_name="Antall")
|
||||||
amount = models.IntegerField(verbose_name="Antall")
|
unit_price = models.FloatField(verbose_name="Enhetspris")
|
||||||
# FIXME: rename to price
|
|
||||||
unit_price = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, verbose_name="Enhetspris")
|
|
||||||
comment = models.TextField(verbose_name="Kommentar", blank=True)
|
comment = models.TextField(verbose_name="Kommentar", blank=True)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
@@ -212,7 +204,7 @@ class InventoryTransaction(models.Model):
|
|||||||
|
|
||||||
class UserProfile(models.Model):
|
class UserProfile(models.Model):
|
||||||
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="profile")
|
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="profile")
|
||||||
balance = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, default=0)
|
balance = models.FloatField(default=0.0)
|
||||||
image = models.URLField(blank=True)
|
image = models.URLField(blank=True)
|
||||||
|
|
||||||
def last_purchase(self):
|
def last_purchase(self):
|
||||||
@@ -235,7 +227,7 @@ class UserProfile(models.Model):
|
|||||||
def product_order_stats(self):
|
def product_order_stats(self):
|
||||||
return OrderLine.objects.filter(order__customer__pk=self.pk).product_order_stats()
|
return OrderLine.objects.filter(order__customer__pk=self.pk).product_order_stats()
|
||||||
|
|
||||||
def order_sum_total(self) -> Decimal:
|
def order_sum_total(self) -> float:
|
||||||
return Order.objects.filter(customer_id=self.user_id).aggregate(sum=Sum("order_sum"))["sum"] or 0
|
return Order.objects.filter(customer_id=self.user_id).aggregate(sum=Sum("order_sum"))["sum"] or 0
|
||||||
|
|
||||||
def image_url(self) -> str:
|
def image_url(self) -> str:
|
||||||
@@ -253,3 +245,16 @@ class UserProfile(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.user}"
|
return f"{self.user}"
|
||||||
|
|
||||||
|
|
||||||
|
class Card(models.Model):
|
||||||
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="cards")
|
||||||
|
card_id = models.CharField(max_length=32)
|
||||||
|
name = models.CharField(max_length=64)
|
||||||
|
# pin = models.IntegerField(max_length=4, blank=True, null=True)
|
||||||
|
# valid_until = models.DateTimeField(blank=True, null=True)
|
||||||
|
active = models.BooleanField(default=True)
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name}"
|
||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/src/assets/brick.svg" />
|
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap" rel="stylesheet">
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { NavLink } from "react-router-dom";
|
|||||||
import styles from "./Header.module.css";
|
import styles from "./Header.module.css";
|
||||||
|
|
||||||
const ADMIN_URL = `${import.meta.env.VITE_API_URL}/admin/`;
|
const ADMIN_URL = `${import.meta.env.VITE_API_URL}/admin/`;
|
||||||
const LOGOUT_URL = `${import.meta.env.VITE_API_URL}/`.replace("://", "://none:none@");
|
|
||||||
|
|
||||||
const navLinkStyles = ({ isActive }: { isActive: boolean }) => clsx(styles.navLink, { [styles.active]: isActive });
|
const navLinkStyles = ({ isActive }: { isActive: boolean }) => clsx(styles.navLink, { [styles.active]: isActive });
|
||||||
|
|
||||||
@@ -49,11 +48,6 @@ export const Header = (): JSX.Element => {
|
|||||||
Admin
|
Admin
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<a href={LOGOUT_URL} className={styles.navLink}>
|
|
||||||
Logg ut
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -51,11 +51,11 @@ const Log = () => {
|
|||||||
<td>
|
<td>
|
||||||
{(order.orderlines ?? []).map((line) => (
|
{(order.orderlines ?? []).map((line) => (
|
||||||
<div key={line.id}>
|
<div key={line.id}>
|
||||||
{Number(line.amount)}x {line.product.name}
|
{line.amount}x {line.product.name}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</td>
|
</td>
|
||||||
<td>{Number(order.orderSum)}</td>
|
<td>{order.orderSum}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -104,7 +104,7 @@ const Log = () => {
|
|||||||
<td>
|
<td>
|
||||||
{trans.user.firstName} {trans.user.lastName}
|
{trans.user.firstName} {trans.user.lastName}
|
||||||
</td>
|
</td>
|
||||||
<td>{Number(trans.amount)}</td>
|
<td>{trans.amount}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useState } from "react";
|
|||||||
import { useToast } from "../../components/ToastProvider";
|
import { useToast } from "../../components/ToastProvider";
|
||||||
import { AllProductsDocument, AllUsersDocument, CreateOrderDocument, OrderLineInput, Product } from "../../types";
|
import { AllProductsDocument, AllUsersDocument, CreateOrderDocument, OrderLineInput, Product } from "../../types";
|
||||||
import { activeUserFilter } from "../../utils/users";
|
import { activeUserFilter } from "../../utils/users";
|
||||||
import {formatCurrency} from "../../utils/currency";
|
|
||||||
|
|
||||||
const USER_EXTERNAL = "external";
|
const USER_EXTERNAL = "external";
|
||||||
|
|
||||||
@@ -54,7 +53,7 @@ export const useRegister = () => {
|
|||||||
setOrder(newOrder);
|
setOrder(newOrder);
|
||||||
};
|
};
|
||||||
|
|
||||||
const total = formatCurrency(sumTotal(products, order, selectedUser === USER_EXTERNAL));
|
const total = sumTotal(products, order, selectedUser === USER_EXTERNAL);
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
let res;
|
let res;
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const StatsNav = () => (
|
|||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<NavLink className="disabled" to="/stats/users" title="TODO" hidden={true}>
|
<NavLink className="disabled" to="/stats/users" title="TODO">
|
||||||
Folk
|
Folk
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import React, { MouseEventHandler } from "react";
|
|||||||
|
|
||||||
import { AllUsersQuery } from "../../types";
|
import { AllUsersQuery } from "../../types";
|
||||||
import styles from "./UserCard.module.css";
|
import styles from "./UserCard.module.css";
|
||||||
import {formatCurrency} from "../../utils/currency";
|
|
||||||
|
|
||||||
type UserCardProps = {
|
type UserCardProps = {
|
||||||
user: AllUsersQuery["allUsers"][0] | null;
|
user: AllUsersQuery["allUsers"][0] | null;
|
||||||
@@ -14,7 +13,6 @@ type UserCardProps = {
|
|||||||
/** If user is null, then it's the special external user */
|
/** If user is null, then it's the special external user */
|
||||||
export const UserCard = ({ user, onUser, active = false }: UserCardProps): JSX.Element => {
|
export const UserCard = ({ user, onUser, active = false }: UserCardProps): JSX.Element => {
|
||||||
const classes = clsx(styles.user, { [styles.active]: active });
|
const classes = clsx(styles.user, { [styles.active]: active });
|
||||||
const userBalanceString = formatCurrency(Number(user?.profile?.balance));
|
|
||||||
return (
|
return (
|
||||||
<button className={classes} onClick={onUser} type="button" title={user?.firstName || "Ekstern"}>
|
<button className={classes} onClick={onUser} type="button" title={user?.firstName || "Ekstern"}>
|
||||||
<div className={styles.avatarWrap}>
|
<div className={styles.avatarWrap}>
|
||||||
@@ -29,7 +27,7 @@ export const UserCard = ({ user, onUser, active = false }: UserCardProps): JSX.E
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.meta}>
|
<div className={styles.meta}>
|
||||||
{user?.firstName || "Ekstern"}
|
{user?.firstName || "Ekstern"}
|
||||||
<span className={styles.balance}>{userBalanceString}</span>
|
<span className={styles.balance}>{user?.profile?.balance || ""}</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
export function formatCurrency(value: number) {
|
|
||||||
if (isNaN(value)) {
|
|
||||||
return "";
|
|
||||||
} else {
|
|
||||||
return value.toFixed(2).replace(/\.00$/, '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user