diff --git a/frontend/src/Routes.tsx b/frontend/src/Routes.tsx
index 7d0a58b..65c97d5 100644
--- a/frontend/src/Routes.tsx
+++ b/frontend/src/Routes.tsx
@@ -5,12 +5,16 @@ import { Deposit } from "./features/deposit/Deposit";
import { Log } from "./features/log/Log";
import { Register } from "./features/register/Register";
import { Stats } from "./features/stats/Stats";
+import { UserDetail } from "./features/users/UserDetail";
+import { UserList } from "./features/users/UserList";
export const Routes: React.FC = () => (
} />
} />
} />
+ } />
+ } />
} />
);
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx
index 39a9322..d491393 100644
--- a/frontend/src/components/Header.tsx
+++ b/frontend/src/components/Header.tsx
@@ -1,6 +1,7 @@
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";
@@ -64,6 +65,15 @@ export const Header: React.FC = () => {
Statistikk
+
+
+
+
+
+ {" "}
+ Folk
+
+
diff --git a/frontend/src/features/log/Log.tsx b/frontend/src/features/log/Log.tsx
index 721f63f..7331ddf 100644
--- a/frontend/src/features/log/Log.tsx
+++ b/frontend/src/features/log/Log.tsx
@@ -1,14 +1,12 @@
import { useQuery } from "@apollo/client";
import clsx from "clsx";
-import { parseISO, format } from "date-fns";
import React from "react";
import { useUrlSearchParams } from "use-url-search-params";
import { Order, OrderLine, OrderListDocument, Transaction, TransactionListDocument } from "../../types";
+import { formatTime } from "../../utils/time";
import styles from "./Log.module.css";
-const formatTime = (isoDateTime: string): string => format(parseISO(isoDateTime), "yyyy-MM-dd HH:mm");
-
const PAGE_SIZE = 10;
export const Log = () => {
diff --git a/frontend/src/features/register/useRegister.ts b/frontend/src/features/register/useRegister.ts
index 349eeb0..5437840 100644
--- a/frontend/src/features/register/useRegister.ts
+++ b/frontend/src/features/register/useRegister.ts
@@ -1,10 +1,8 @@
import { useQuery } from "@apollo/client";
-import { parseISO, subDays } from "date-fns";
import { useState } from "react";
import { User, Product, AllProductsDocument, AllUsersDocument } from "../../types";
-
-const activeLimitDays = 365 / 2;
+import { activeUserFilter } from "../../utils/users";
type useRegisterType = {
users: User[];
@@ -20,13 +18,7 @@ export const useRegister = (): useRegisterType => {
const { data: usersData, loading: usersLoading } = useQuery(AllUsersDocument);
const allUsers = usersData?.allUsers || [];
- const users = showAll
- ? allUsers
- : allUsers.filter((user: User) => {
- const balance = user.profile?.balance || 0;
- const active = parseISO(user.profile?.lastPurchaseDate) >= subDays(new Date(), activeLimitDays);
- return balance >= 15 && active;
- });
+ const users = showAll ? allUsers : allUsers.filter(activeUserFilter);
return { loading: loading || usersLoading, users, products: data?.allProducts || [], showAll, setShowAll };
};
diff --git a/frontend/src/features/users/UserDetail.module.css b/frontend/src/features/users/UserDetail.module.css
new file mode 100644
index 0000000..e21b4f8
--- /dev/null
+++ b/frontend/src/features/users/UserDetail.module.css
@@ -0,0 +1,5 @@
+.userDetail {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: repeat(2, 1fr);
+}
diff --git a/frontend/src/features/users/UserDetail.tsx b/frontend/src/features/users/UserDetail.tsx
new file mode 100644
index 0000000..c2cc9cf
--- /dev/null
+++ b/frontend/src/features/users/UserDetail.tsx
@@ -0,0 +1,47 @@
+import { useQuery } from "@apollo/client";
+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 Loading...
;
+
+ const user = data?.user as User;
+ const profile = user?.profile as UserProfile;
+ return (
+
+
+
+ {profile.image &&
} {user.firstName}
+
+
+
Stats
+
+ - Saldo: {profile.balance}
+ - Siste kjøp: {formatTime(profile.lastPurchaseDate)}
+ - Kjøp totalt: {profile.orderSumTotal.toLocaleString()}
+
+
+
+
+
Top 5 måneder
+
+ {profile.topMonths.map((topMonth) => (
+ -
+ {formatMonth(topMonth.period)}: {topMonth.count} kjøp på totalt{" "}
+ {topMonth.sum} kr
+
+ ))}
+
+
+
+
TODO: produktgraf
+
+ );
+};
diff --git a/frontend/src/features/users/UserList.tsx b/frontend/src/features/users/UserList.tsx
new file mode 100644
index 0000000..e8e2d6a
--- /dev/null
+++ b/frontend/src/features/users/UserList.tsx
@@ -0,0 +1,32 @@
+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 Loading...
;
+
+ const allUsers = data?.allUsers || [];
+ const users = showAll ? allUsers : allUsers.filter(activeUserFilter);
+ return (
+
+
+
+ {(users || []).map((user) => (
+ -
+
+ {user.firstName} {user.lastName}
+
+
+ ))}
+
+
+ );
+};
diff --git a/frontend/src/features/users/userDetail.graphql b/frontend/src/features/users/userDetail.graphql
new file mode 100644
index 0000000..837f170
--- /dev/null
+++ b/frontend/src/features/users/userDetail.graphql
@@ -0,0 +1,19 @@
+query userDetail($userId: ID!) {
+ user(userId: $userId) {
+ id
+ firstName
+ lastName
+ profile {
+ id
+ balance
+ image
+ lastPurchaseDate
+ topMonths {
+ period
+ sum
+ count
+ }
+ orderSumTotal
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index acab542..51d924d 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -159,6 +159,7 @@ export type Query = {
orderList: Array;
orderStats: OrderStats;
transactionList: Array;
+ user: User;
};
@@ -178,6 +179,18 @@ export type QueryTransactionListArgs = {
pagination?: InputMaybe;
};
+
+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'];
@@ -204,6 +217,8 @@ export type UserProfile = {
id: Scalars['ID'];
image: Scalars['String'];
lastPurchaseDate: Scalars['DateTime'];
+ orderSumTotal: Scalars['Float'];
+ topMonths: Array;
};
export type CreateDepositMutationVariables = Exact<{
@@ -263,6 +278,13 @@ export type ProductStatsQueryVariables = Exact<{
export type ProductStatsQuery = { __typename?: 'Query', allProducts: Array<{ __typename?: 'Product', id: string, name: string, userCounts: any }> };
+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 }> } | null } };
+
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;
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;
@@ -271,4 +293,5 @@ export const AllProductsDocument = {"kind":"Document","definitions":[{"kind":"Op
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;
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;
\ No newline at end of file
+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
diff --git a/frontend/src/utils/time.ts b/frontend/src/utils/time.ts
new file mode 100644
index 0000000..6e5ec65
--- /dev/null
+++ b/frontend/src/utils/time.ts
@@ -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");
diff --git a/frontend/src/utils/users.ts b/frontend/src/utils/users.ts
new file mode 100644
index 0000000..b3b3871
--- /dev/null
+++ b/frontend/src/utils/users.ts
@@ -0,0 +1,11 @@
+import { parseISO, subDays } from "date-fns";
+
+import { User } from "../types";
+
+const activeLimitDays = 365 / 2;
+
+export const activeUserFilter = (user: User) => {
+ const balance = user.profile?.balance || 0;
+ const active = parseISO(user.profile?.lastPurchaseDate) >= subDays(new Date(), activeLimitDays);
+ return balance >= 15 && active;
+};
diff --git a/main/api/schema.py b/main/api/schema.py
index 24abeaa..249a4ae 100644
--- a/main/api/schema.py
+++ b/main/api/schema.py
@@ -1,5 +1,7 @@
import strawberry
import strawberry_django
+from django.contrib.auth.models import User as DjangoUser
+from strawberry import ID
from main import models
from main.api import mutations
@@ -23,6 +25,10 @@ class Query:
order_list: list[Order] = strawberry_django.field(pagination=True, order=OrderOrdering)
transaction_list: list[Transaction] = strawberry_django.field(pagination=True, order=TransactionOrdering)
+ @strawberry.field
+ def user(self, user_id: ID) -> User:
+ return DjangoUser.objects.get(pk=user_id)
+
@strawberry.field
def order_stats(self) -> OrderStats:
return OrderStats(
diff --git a/main/api/types.py b/main/api/types.py
index 3b649a3..9986344 100644
--- a/main/api/types.py
+++ b/main/api/types.py
@@ -36,15 +36,24 @@ class Product:
user_counts: JSONScalar
+@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 last_purchase_date(self) -> datetime:
- return self.last_purchase_date()
+ def top_months(self) -> list[TopMonth]:
+ return [TopMonth(**month) for month in self.top_months()]
@strawberry_django.type(UserModel)
diff --git a/main/models.py b/main/models.py
index fdb08b6..6218124 100755
--- a/main/models.py
+++ b/main/models.py
@@ -5,7 +5,7 @@ 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
+from django.db.models.functions import Extract, ExtractHour, ExtractIsoWeekDay, ExtractMonth, ExtractYear, TruncMonth
EXTERNAL_USER = "Ekstern"
@@ -178,11 +178,23 @@ class UserProfile(models.Model):
def last_purchase(self):
return self.user.orders.order_by("-created").first()
- def last_purchase_date(self):
+ def last_purchase_date(self) -> datetime:
last = self.last_purchase()
last_p = last.created if last else datetime.min
return last_p
+ def top_months(self, limit=5):
+ return (
+ Order.objects.filter(customer_id=self.user_id)
+ .annotate(period=TruncMonth("created"))
+ .values("period")
+ .annotate(sum=Sum("order_sum"), count=Count("pk"))
+ .order_by("-sum")[:limit]
+ )
+
+ def order_sum_total(self) -> float:
+ return Order.objects.filter(customer_id=self.user_id).aggregate(sum=Sum("order_sum"))["sum"] or 0
+
def image_url(self) -> str:
if self.user.email:
md5_hash = hashlib.md5(self.user.email.lower().strip().encode()).hexdigest()
diff --git a/schema.graphql b/schema.graphql
index a0f0ae2..2a4337a 100644
--- a/schema.graphql
+++ b/schema.graphql
@@ -121,9 +121,16 @@ type Query {
allOrders: [Order!]!
orderList(pagination: OffsetPaginationInput, order: OrderOrdering): [Order!]!
transactionList(pagination: OffsetPaginationInput, order: TransactionOrdering): [Transaction!]!
+ user(userId: ID!): User!
orderStats: OrderStats!
}
+type TopMonth {
+ period: String!
+ count: Int!
+ sum: Int!
+}
+
type Transaction {
id: ID!
user: User!
@@ -147,4 +154,6 @@ type UserProfile {
balance: Float!
image: String!
lastPurchaseDate: DateTime!
+ orderSumTotal: Float!
+ topMonths: [TopMonth!]!
}
\ No newline at end of file
diff --git a/templates/base.html b/templates/base.html
index f97eac1..ee70626 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -26,10 +26,7 @@
{% if not user.is_authenticated %}