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}s} {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 ( +
    + + +
    + ); +}; 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 @@