From 771e0c2a3b5d534bd781389b08c073fb09188f8e Mon Sep 17 00:00:00 2001 From: Nikolai R Kristiansen Date: Sun, 6 Feb 2022 21:05:40 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20order=20stats?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/package-lock.json | 124 ++++++++++++++++++ frontend/package.json | 1 + frontend/src/features/stats/Stats.module.css | 1 + frontend/src/features/stats/Stats.tsx | 124 ++++++++++++++---- .../src/features/stats/orderStats.graphql | 20 +++ frontend/src/types.ts | 21 +++ main/api/schema.py | 21 ++- main/api/types.py | 14 ++ main/models.py | 24 +++- schema.graphql | 13 ++ 10 files changed, 336 insertions(+), 27 deletions(-) create mode 100644 frontend/src/features/stats/orderStats.graphql diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 82e6aaf..0e4330a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@apollo/client": "^3.5.8", + "@nivo/bar": "^0.79.1", "@nivo/pie": "^0.79.1", "bootstrap": "^5.1.3", "bootstrap-icons": "^1.8.0", @@ -2347,6 +2348,20 @@ "graphql": "^15.4.0 || ^16.0.0" } }, + "node_modules/@nivo/annotations": { + "version": "0.79.1", + "resolved": "https://registry.npmjs.org/@nivo/annotations/-/annotations-0.79.1.tgz", + "integrity": "sha512-lYso9Luu0maSDtIufwvyVt2+Wue7R9Fh3CIjuRDmNR72UjAgAVEcCar27Fy865UXGsj2hRJZ7KY/1s6kT3gu/w==", + "dependencies": { + "@nivo/colors": "0.79.1", + "@react-spring/web": "9.3.1", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@nivo/core": "0.79.0", + "react": ">= 16.14.0 < 18.0.0" + } + }, "node_modules/@nivo/arcs": { "version": "0.79.1", "resolved": "https://registry.npmjs.org/@nivo/arcs/-/arcs-0.79.1.tgz", @@ -2361,6 +2376,43 @@ "react": ">= 16.14.0 < 18.0.0" } }, + "node_modules/@nivo/axes": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@nivo/axes/-/axes-0.79.0.tgz", + "integrity": "sha512-EhSeCPxtWEuxqnifeyF/pIJEzL7pRM3rfygL+MpfT5ypu5NcXYRGQo/Bw0Vh+GF1ML+tNAE0rRvCu2jgLSdVNQ==", + "dependencies": { + "@nivo/scales": "0.79.0", + "@react-spring/web": "9.3.1", + "d3-format": "^1.4.4", + "d3-time-format": "^3.0.0" + }, + "peerDependencies": { + "@nivo/core": "0.79.0", + "prop-types": ">= 15.5.10 < 16.0.0", + "react": ">= 16.14.0 < 18.0.0" + } + }, + "node_modules/@nivo/bar": { + "version": "0.79.1", + "resolved": "https://registry.npmjs.org/@nivo/bar/-/bar-0.79.1.tgz", + "integrity": "sha512-swJ2FtFeRPWJK9O6aZiqTDi2J6GrU2Z6kIHBBCXBlFmq6+vfd5AqOHytdXPTaN80JsKDBBdtY7tqRjpRPlDZwQ==", + "dependencies": { + "@nivo/annotations": "0.79.1", + "@nivo/axes": "0.79.0", + "@nivo/colors": "0.79.1", + "@nivo/legends": "0.79.1", + "@nivo/scales": "0.79.0", + "@nivo/tooltip": "0.79.0", + "@react-spring/web": "9.3.1", + "d3-scale": "^3.2.3", + "d3-shape": "^1.2.2", + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@nivo/core": "0.79.0", + "react": ">= 16.14.0 < 18.0.0" + } + }, "node_modules/@nivo/colors": { "version": "0.79.1", "resolved": "https://registry.npmjs.org/@nivo/colors/-/colors-0.79.1.tgz", @@ -2438,6 +2490,22 @@ "react": ">= 16.14.0 < 18.0.0" } }, + "node_modules/@nivo/scales": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@nivo/scales/-/scales-0.79.0.tgz", + "integrity": "sha512-5fAt5Wejp8yzAk6qmA3KU+celCxNYrrBhfvOi2ECDG8KQi+orbDnrO6qjVF6+ebfOn9az8ZVukcSeGA5HceiMg==", + "dependencies": { + "d3-scale": "^3.2.3", + "d3-time": "^1.0.11", + "d3-time-format": "^3.0.0", + "lodash": "^4.17.21" + } + }, + "node_modules/@nivo/scales/node_modules/d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + }, "node_modules/@nivo/tooltip": { "version": "0.79.0", "resolved": "https://registry.npmjs.org/@nivo/tooltip/-/tooltip-0.79.0.tgz", @@ -14114,6 +14182,16 @@ "dev": true, "requires": {} }, + "@nivo/annotations": { + "version": "0.79.1", + "resolved": "https://registry.npmjs.org/@nivo/annotations/-/annotations-0.79.1.tgz", + "integrity": "sha512-lYso9Luu0maSDtIufwvyVt2+Wue7R9Fh3CIjuRDmNR72UjAgAVEcCar27Fy865UXGsj2hRJZ7KY/1s6kT3gu/w==", + "requires": { + "@nivo/colors": "0.79.1", + "@react-spring/web": "9.3.1", + "lodash": "^4.17.21" + } + }, "@nivo/arcs": { "version": "0.79.1", "resolved": "https://registry.npmjs.org/@nivo/arcs/-/arcs-0.79.1.tgz", @@ -14124,6 +14202,34 @@ "d3-shape": "^1.3.5" } }, + "@nivo/axes": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@nivo/axes/-/axes-0.79.0.tgz", + "integrity": "sha512-EhSeCPxtWEuxqnifeyF/pIJEzL7pRM3rfygL+MpfT5ypu5NcXYRGQo/Bw0Vh+GF1ML+tNAE0rRvCu2jgLSdVNQ==", + "requires": { + "@nivo/scales": "0.79.0", + "@react-spring/web": "9.3.1", + "d3-format": "^1.4.4", + "d3-time-format": "^3.0.0" + } + }, + "@nivo/bar": { + "version": "0.79.1", + "resolved": "https://registry.npmjs.org/@nivo/bar/-/bar-0.79.1.tgz", + "integrity": "sha512-swJ2FtFeRPWJK9O6aZiqTDi2J6GrU2Z6kIHBBCXBlFmq6+vfd5AqOHytdXPTaN80JsKDBBdtY7tqRjpRPlDZwQ==", + "requires": { + "@nivo/annotations": "0.79.1", + "@nivo/axes": "0.79.0", + "@nivo/colors": "0.79.1", + "@nivo/legends": "0.79.1", + "@nivo/scales": "0.79.0", + "@nivo/tooltip": "0.79.0", + "@react-spring/web": "9.3.1", + "d3-scale": "^3.2.3", + "d3-shape": "^1.2.2", + "lodash": "^4.17.21" + } + }, "@nivo/colors": { "version": "0.79.1", "resolved": "https://registry.npmjs.org/@nivo/colors/-/colors-0.79.1.tgz", @@ -14180,6 +14286,24 @@ "react-lifecycles-compat": "^3.0.4" } }, + "@nivo/scales": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/@nivo/scales/-/scales-0.79.0.tgz", + "integrity": "sha512-5fAt5Wejp8yzAk6qmA3KU+celCxNYrrBhfvOi2ECDG8KQi+orbDnrO6qjVF6+ebfOn9az8ZVukcSeGA5HceiMg==", + "requires": { + "d3-scale": "^3.2.3", + "d3-time": "^1.0.11", + "d3-time-format": "^3.0.0", + "lodash": "^4.17.21" + }, + "dependencies": { + "d3-time": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz", + "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==" + } + } + }, "@nivo/tooltip": { "version": "0.79.0", "resolved": "https://registry.npmjs.org/@nivo/tooltip/-/tooltip-0.79.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index add5dd6..babac28 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ }, "dependencies": { "@apollo/client": "^3.5.8", + "@nivo/bar": "^0.79.1", "@nivo/pie": "^0.79.1", "bootstrap": "^5.1.3", "bootstrap-icons": "^1.8.0", diff --git a/frontend/src/features/stats/Stats.module.css b/frontend/src/features/stats/Stats.module.css index 80b5124..c6ad36a 100644 --- a/frontend/src/features/stats/Stats.module.css +++ b/frontend/src/features/stats/Stats.module.css @@ -1,5 +1,6 @@ .products { display: grid; + max-width: 1200px; gap: 1rem; grid-template-columns: repeat(3, 1fr); } diff --git a/frontend/src/features/stats/Stats.tsx b/frontend/src/features/stats/Stats.tsx index b28e0f5..8fd8e05 100644 --- a/frontend/src/features/stats/Stats.tsx +++ b/frontend/src/features/stats/Stats.tsx @@ -1,8 +1,9 @@ import { useQuery } from "@apollo/client"; +import { Bar } from "@nivo/bar"; import { Pie } from "@nivo/pie"; import React from "react"; -import { ProductStatsDocument } from "../../types"; +import { OrderStatsDocument, ProductStatsDocument, OrderStatsByTime } from "../../types"; import styles from "./Stats.module.css"; type UserCount = { @@ -10,38 +11,115 @@ type UserCount = { count: number; }; +const days = { 1: "Mandag", 2: "Tirsdag", 3: "Onsdag", 4: "Torsdag", 5: "Fredag", 6: "Lørdag", 7: "Søndag" }; + +const months = { + 1: "Januar", + 2: "Februar", + 3: "Mars", + 4: "April", + 5: "Mai", + 6: "Juni", + 7: "Juli", + 8: "August", + 9: "September", + 10: "Oktober", + 11: "November", + 12: "Desember", +}; export const Stats = () => { - const { data, loading } = useQuery(ProductStatsDocument); + const { data: productData, loading: productLoading } = useQuery(ProductStatsDocument); + const { data: orderData, loading: orderLoading } = useQuery(OrderStatsDocument); + + const loading = orderLoading || productLoading; + if (loading) { return
Loading...
; } + return (

Stats

Produkter

- {(data?.allProducts || []).map((product) => { - return ( -
- {product.name} - ({ - id: uc.name, - label: uc.name, - value: uc.count, - }))} - width={400} - height={300} - /> -
- ); - })} + {(productData?.allProducts || []).map((product) => ( +
+ {product.name} + ({ + id: uc.name, + label: uc.name, + value: uc.count, + }))} + width={400} + height={300} + /> +
+ ))}
-

Hourly

-

Daily

-

Monthly

-

Yearly

+

Ordre per time

+ ({ + id: `${stat.period.toString().padStart(2, "0")}:00`, + label: stat.period, + value: stat.count, + }))} + margin={{ left: 60, bottom: 40 }} + width={1200} + height={400} + colorBy="indexValue" + enableLabel={false} + tooltipLabel={(d) => d.id.toString()} + axisLeft={{ legend: "Ordre", legendPosition: "middle", legendOffset: -50 }} + /> +

Ordre per ukedag

+ ({ + /* @ts-expect-error Here, stat.period can only be a key in days range. */ + id: days[stat.period], + label: stat.period, + value: stat.count, + }))} + margin={{ left: 60, bottom: 40 }} + width={1200} + height={400} + colorBy="indexValue" + enableLabel={false} + tooltipLabel={(d) => d.id.toString()} + axisLeft={{ legend: "Ordre", legendPosition: "middle", legendOffset: -50 }} + /> +

Ordre per måned

+ ({ + /* @ts-expect-error Here, stat.period can only be a key in months range. */ + id: months[stat.period], + label: stat.period, + value: stat.count, + }))} + margin={{ left: 60, bottom: 40 }} + width={1200} + height={400} + colorBy="indexValue" + enableLabel={false} + tooltipLabel={(d) => d.id.toString()} + axisLeft={{ legend: "Ordre", legendPosition: "middle", legendOffset: -50 }} + /> +

Ordre per år

+ ({ + id: stat.period, + label: stat.period, + value: stat.count, + }))} + margin={{ left: 60, bottom: 40 }} + width={1200} + height={400} + colorBy="indexValue" + enableLabel={false} + tooltipLabel={(d) => d.id.toString()} + axisLeft={{ legend: "Ordre", legendPosition: "middle", legendOffset: -50 }} + />
); }; diff --git a/frontend/src/features/stats/orderStats.graphql b/frontend/src/features/stats/orderStats.graphql new file mode 100644 index 0000000..92e16d3 --- /dev/null +++ b/frontend/src/features/stats/orderStats.graphql @@ -0,0 +1,20 @@ +query orderStats { + orderStats { + yearly { + period + count + } + monthly { + period + count + } + dayly { + period + count + } + hourly { + period + count + } + } +} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index ba4d0b8..5db0428 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -117,6 +117,20 @@ export type OrderOrdering = { created?: InputMaybe; }; +export type OrderStats = { + __typename?: 'OrderStats'; + dayly: Array; + hourly: Array; + monthly: Array; + yearly: Array; +}; + +export type OrderStatsByTime = { + __typename?: 'OrderStatsByTime'; + count: Scalars['String']; + period: Scalars['Int']; +}; + export enum Ordering { Asc = 'ASC', Desc = 'DESC' @@ -143,6 +157,7 @@ export type Query = { allProducts: Array; allUsers: Array; orderList: Array; + orderStats: OrderStats; transactionList: Array; }; @@ -236,6 +251,11 @@ export type CreateOrderMutationVariables = Exact<{ export type CreateOrderMutation = { __typename?: 'Mutation', createOrder: { __typename?: 'CreateOrderSuccess', order: { __typename?: 'Order', id: string, isExternal: boolean, orderSum: number, customer?: { __typename?: 'User', id: string, firstName: string, lastName: string, profile?: { __typename?: 'UserProfile', id: string, balance: number } | null } | null, orderlines?: Array<{ __typename?: 'OrderLine', id: string, amount: number, unitPrice: number, product: { __typename?: 'Product', id: string, name: string, salePriceExt: number, salePriceInt: number } }> | null } } | { __typename?: 'FormErrors', error: boolean, message: string, fields: Array<{ __typename?: 'FieldError', field: string, message: string }> } | { __typename?: 'InsufficientFunds', error: boolean, message: string, amountLacking: number } }; +export type OrderStatsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type OrderStatsQuery = { __typename?: 'Query', orderStats: { __typename?: 'OrderStats', yearly: Array<{ __typename?: 'OrderStatsByTime', period: number, count: string }>, monthly: Array<{ __typename?: 'OrderStatsByTime', period: number, count: string }>, dayly: Array<{ __typename?: 'OrderStatsByTime', period: number, count: string }>, hourly: Array<{ __typename?: 'OrderStatsByTime', period: number, count: string }> } }; + export type ProductStatsQueryVariables = Exact<{ active?: InputMaybe; }>; @@ -250,4 +270,5 @@ export const TransactionListDocument = {"kind":"Document","definitions":[{"kind" export const AllProductsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"allProducts"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"active"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"allProducts"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filters"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"active"},"value":{"kind":"Variable","name":{"kind":"Name","value":"active"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"imageUrl"}},{"kind":"Field","name":{"kind":"Name","value":"salePriceExt"}},{"kind":"Field","name":{"kind":"Name","value":"salePriceInt"}}]}}]}}]} as unknown as DocumentNode; 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":"dayly"},"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 diff --git a/main/api/schema.py b/main/api/schema.py index 9f10495..9792e79 100644 --- a/main/api/schema.py +++ b/main/api/schema.py @@ -1,8 +1,18 @@ import strawberry import strawberry_django +from main import models from main.api import mutations -from main.api.types import Order, OrderOrdering, Product, Transaction, TransactionOrdering, User +from main.api.types import ( + Order, + OrderOrdering, + OrderStats, + OrderStatsByTime, + Product, + Transaction, + TransactionOrdering, + User, +) @strawberry.type @@ -13,6 +23,15 @@ 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 order_stats(self) -> OrderStats: + return OrderStats( + yearly=[OrderStatsByTime(**stat) for stat in models.Order.objects.count_by_year().order_by("period")], + monthly=[OrderStatsByTime(**stat) for stat in models.Order.objects.count_by_month().order_by("period")], + dayly=[OrderStatsByTime(**stat) for stat in models.Order.objects.count_by_weekday().order_by("period")], + hourly=[OrderStatsByTime(**stat) for stat in models.Order.objects.count_by_hour().order_by("period")], + ) + @strawberry.type class Mutation: diff --git a/main/api/types.py b/main/api/types.py index 54f1120..2bfac56 100644 --- a/main/api/types.py +++ b/main/api/types.py @@ -79,6 +79,20 @@ class OrderLine: unit_price: auto +@strawberry.type +class OrderStatsByTime: + period: int + count: str + + +@strawberry.type +class OrderStats: + yearly: list[OrderStatsByTime] + monthly: list[OrderStatsByTime] + dayly: list[OrderStatsByTime] + hourly: list[OrderStatsByTime] + + @strawberry_django.ordering.order(models.Transaction) class TransactionOrdering: created: auto diff --git a/main/models.py b/main/models.py index d8033d7..c839e8d 100755 --- a/main/models.py +++ b/main/models.py @@ -2,8 +2,8 @@ from datetime import datetime from django.conf import settings from django.db import models -from django.db.models import Sum -from django.templatetags.static import static +from django.db.models import Count, Sum +from django.db.models.functions import ExtractHour, ExtractMonth, ExtractWeekDay, ExtractYear EXTERNAL_USER = "Ekstern" @@ -79,6 +79,22 @@ class Product(models.Model): ordering = ["name"] +class OrderManager(models.Manager): + def count_by_year(self): + return self.get_queryset().annotate(period=ExtractYear("created")).values("period").annotate(count=Count("id")) + + def count_by_month(self): + return self.get_queryset().annotate(period=ExtractMonth("created")).values("period").annotate(count=Count("id")) + + def count_by_weekday(self): + return ( + self.get_queryset().annotate(period=ExtractWeekDay("created")).values("period").annotate(count=Count("id")) + ) + + def count_by_hour(self): + return self.get_queryset().annotate(period=ExtractHour("created")).values("period").annotate(count=Count("id")) + + class Order(models.Model): customer = models.ForeignKey( settings.AUTH_USER_MODEL, @@ -90,6 +106,8 @@ class Order(models.Model): order_sum = models.FloatField(db_column="sum") created = models.DateTimeField(auto_now_add=True) + objects = OrderManager() + def is_external(self): return self.customer is None @@ -97,7 +115,7 @@ class Order(models.Model): return "{}".format(", ".join([str(ol) for ol in self.orderlines.all()])) def __str__(self): - return "{}: {} kr".format(self.customer or "Ekstern", self.order_sum) + return "{}: {} kr".format(self.customer or EXTERNAL_USER, self.order_sum) class OrderLine(models.Model): diff --git a/schema.graphql b/schema.graphql index 35c3019..42712e3 100644 --- a/schema.graphql +++ b/schema.graphql @@ -84,6 +84,18 @@ input OrderOrdering { created: Ordering } +type OrderStats { + yearly: [OrderStatsByTime!]! + monthly: [OrderStatsByTime!]! + dayly: [OrderStatsByTime!]! + hourly: [OrderStatsByTime!]! +} + +type OrderStatsByTime { + period: Int! + count: String! +} + enum Ordering { ASC DESC @@ -109,6 +121,7 @@ type Query { allOrders: [Order!]! orderList(pagination: OffsetPaginationInput, order: OrderOrdering): [Order!]! transactionList(pagination: OffsetPaginationInput, order: TransactionOrdering): [Transaction!]! + orderStats: OrderStats! } type Transaction {