diff --git a/frontend/src/features/users/UserDetail.module.css b/frontend/src/features/users/UserDetail.module.css
index e21b4f8..3e20669 100644
--- a/frontend/src/features/users/UserDetail.module.css
+++ b/frontend/src/features/users/UserDetail.module.css
@@ -1,5 +1,5 @@
.userDetail {
display: grid;
gap: 1rem;
- grid-template-columns: repeat(2, 1fr);
+ grid-template-columns: 400px 1fr;
}
diff --git a/frontend/src/features/users/UserDetail.tsx b/frontend/src/features/users/UserDetail.tsx
index a2ced35..10eab00 100644
--- a/frontend/src/features/users/UserDetail.tsx
+++ b/frontend/src/features/users/UserDetail.tsx
@@ -1,4 +1,5 @@
import { useQuery } from "@apollo/client";
+import { Line } from "@nivo/line";
import React from "react";
import { useParams } from "react-router-dom";
@@ -14,6 +15,15 @@ export const UserDetail = () => {
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 (
@@ -47,7 +57,53 @@ export const UserDetail = () => {
- TODO: produktgraf
+
+
(
+
+
+ {point.data.xFormatted}
+
+
+ {" "}
+ {point.data.yFormatted} {point.serieId}
+
+
+ )}
+ />
+
);
};
diff --git a/frontend/src/features/users/userDetail.graphql b/frontend/src/features/users/userDetail.graphql
index 837f170..d6f76ff 100644
--- a/frontend/src/features/users/userDetail.graphql
+++ b/frontend/src/features/users/userDetail.graphql
@@ -13,6 +13,13 @@ query userDetail($userId: ID!) {
sum
count
}
+ productOrderStats {
+ productName
+ data {
+ x
+ y
+ }
+ }
orderSumTotal
}
}
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index e6fad2a..25c4239 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -142,6 +142,12 @@ export enum Ordering {
Desc = 'DESC'
}
+export type Point = {
+ __typename?: 'Point';
+ x: Scalars['String'];
+ y: Scalars['Int'];
+};
+
export type Product = {
__typename?: 'Product';
id: Scalars['ID'];
@@ -157,6 +163,12 @@ export type ProductFilter = {
id?: InputMaybe;
};
+export type ProductOrderStat = {
+ __typename?: 'ProductOrderStat';
+ data: Array;
+ productName: Scalars['String'];
+};
+
export type Query = {
__typename?: 'Query';
allOrders: Array;
@@ -230,6 +242,7 @@ export type UserProfile = {
image: Scalars['String'];
lastPurchaseDate: Scalars['DateTime'];
orderSumTotal: Scalars['Float'];
+ productOrderStats: Array;
topMonths: Array;
};
@@ -304,7 +317,7 @@ export type UserDetailQueryVariables = Exact<{
}>;
-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 }> } | null } };
+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;
@@ -316,4 +329,4 @@ export const AllUsersDocument = {"kind":"Document","definitions":[{"kind":"Opera
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;
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;
export const ProductStatsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"productStats"},"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;
-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":"orderSumTotal"}}]}}]}}]}}]} as unknown as DocumentNode;
\ No newline at end of file
+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;
\ No newline at end of file
diff --git a/main/api/types/users.py b/main/api/types/users.py
index 916a6aa..995e4b9 100644
--- a/main/api/types/users.py
+++ b/main/api/types/users.py
@@ -17,6 +17,18 @@ class TopMonth:
sum: int
+@strawberry.type
+class Point:
+ x: str
+ y: int
+
+
+@strawberry.type
+class ProductOrderStat:
+ product_name: str
+ data: list[Point]
+
+
@strawberry_django.type(models.UserProfile)
class UserProfile:
id: auto
@@ -29,6 +41,13 @@ class UserProfile:
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:
diff --git a/main/models.py b/main/models.py
index 6218124..183a33c 100755
--- a/main/models.py
+++ b/main/models.py
@@ -1,11 +1,20 @@
import hashlib
from datetime import datetime
+from itertools import groupby
from typing import Type
from django.conf import settings
from django.db import models
-from django.db.models import Count, Sum
-from django.db.models.functions import Extract, ExtractHour, ExtractIsoWeekDay, ExtractMonth, ExtractYear, TruncMonth
+from django.db.models import Count, F, Sum
+from django.db.models.functions import (
+ Extract,
+ ExtractHour,
+ ExtractIsoWeekDay,
+ ExtractMonth,
+ ExtractYear,
+ TruncDay,
+ TruncMonth,
+)
EXTERNAL_USER = "Ekstern"
@@ -115,12 +124,35 @@ class Order(models.Model):
return f"{self.customer or EXTERNAL_USER}: {self.order_sum} kr"
+class OrderLineManager(models.QuerySet):
+ def product_order_stats(self):
+ return (
+ self.annotate(date=TruncDay("order__created"))
+ .values("date", product_name=F("product__name"))
+ .annotate(count=Sum("amount"))
+ .order_by("date")
+ )
+
+ def as_series(self):
+ datas = list(self)
+ datas.sort(key=lambda x: x["product_name"])
+ return [
+ {
+ "product_name": key,
+ "data": [{"x": item["date"].date().isoformat(), "y": item["count"]} for item in list(group)],
+ }
+ for key, group in groupby(datas, key=lambda x: x["product_name"])
+ ]
+
+
class OrderLine(models.Model):
order = models.ForeignKey("main.Order", on_delete=models.CASCADE, related_name="orderlines")
product = models.ForeignKey("main.Product", on_delete=models.CASCADE, related_name="orderlines")
amount = models.IntegerField()
unit_price = models.FloatField(verbose_name="Enhetspris")
+ objects = OrderLineManager.as_manager()
+
@property
def price(self):
return self.amount * self.unit_price
@@ -192,6 +224,9 @@ class UserProfile(models.Model):
.order_by("-sum")[:limit]
)
+ def product_order_stats(self):
+ return OrderLine.objects.filter(order__customer__pk=self.pk).product_order_stats()
+
def order_sum_total(self) -> float:
return Order.objects.filter(customer_id=self.user_id).aggregate(sum=Sum("order_sum"))["sum"] or 0
diff --git a/schema.graphql b/schema.graphql
index a0c7792..eee6dd4 100644
--- a/schema.graphql
+++ b/schema.graphql
@@ -102,6 +102,11 @@ enum Ordering {
DESC
}
+type Point {
+ x: String!
+ y: Int!
+}
+
type Product {
id: ID!
name: String!
@@ -116,6 +121,11 @@ input ProductFilter {
active: Boolean
}
+type ProductOrderStat {
+ productName: String!
+ data: [Point!]!
+}
+
type Query {
transactionList(pagination: OffsetPaginationInput, order: TransactionOrdering): [Transaction!]!
allProducts(filters: ProductFilter): [Product!]!
@@ -162,5 +172,6 @@ type UserProfile {
image: String!
lastPurchaseDate: DateTime!
orderSumTotal: Float!
+ productOrderStats: [ProductOrderStat!]!
topMonths: [TopMonth!]!
}
\ No newline at end of file