Skip to content
This repository has been archived by the owner on Nov 2, 2024. It is now read-only.

Commit

Permalink
✨ Add ability to see members by product
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverlevay committed Aug 22, 2023
1 parent bc1ac53 commit 47aedb3
Show file tree
Hide file tree
Showing 14 changed files with 684 additions and 5 deletions.
76 changes: 76 additions & 0 deletions backend/services/core/graphql.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -7339,6 +7339,49 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "MemberByProduct",
"description": null,
"fields": [
{
"name": "member",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Member",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "userInventoryItem",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "UserInventoryItem",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "MemberFilter",
Expand Down Expand Up @@ -10160,6 +10203,39 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "getMembersByProduct",
"description": null,
"args": [
{
"name": "productId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "UUID",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "MemberByProduct",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "getSubscriptionTypes",
"description": null,
Expand Down
18 changes: 18 additions & 0 deletions backend/services/core/src/datasources/WebshopAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { addMinutes } from '../shared/utils';
import * as gql from '../types/graphql';
import * as sql from '../types/webshop';
import { Member } from '../types/database';
import { convertMember } from './Member';

let transactions = 0;

Expand Down Expand Up @@ -85,6 +86,23 @@ export default class WebshopAPI extends dbUtils.KnexDataSource {
logger.info('Finished checking for expired carts.');
}

getMembersByProduct(ctx: context.UserContext, productId: UUID): Promise<gql.MemberByProduct[]> {
return this.withAccess('webshop:read', ctx, async () => {
const product = await this.knex<sql.Product>(TABLE.PRODUCT).where({ id: productId }).first();
if (!product) throw new Error('Product not found');
const inventories = await this.knex<sql.ProductInventory>(TABLE.PRODUCT_INVENTORY).where({
product_id: productId,
});
const userInventoryItems = await this.knex<sql.UserInventoryItem>(TABLE.USER_INVENTORY_ITEM).whereIn('product_inventory_id', inventories.map((i) => i.id));
const studentIds = userInventoryItems.map((i) => i.student_id);
const members = await this.knex<Member>('members').whereIn('student_id', studentIds);
return userInventoryItems.map((item) => ({
member: convertMember(members.find((m) => m.student_id === item.student_id), ctx)!,
userInventoryItem: convertUserInventoryItem(item),
}));
});
}

getProducts(ctx: context.UserContext, categoryId?: string): Promise<gql.Product[]> {
return this.withAccess('webshop:read', ctx, async () => {
let query = this.knex<sql.Product>(TABLE.PRODUCT);
Expand Down
2 changes: 2 additions & 0 deletions backend/services/core/src/resolvers/webshopResolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ const webshopResolvers: Resolvers<context.UserContext & DataSourceContext> = {
dataSources.webshopAPI.getPayment({ user, roles }, id),
chest: (_parent, { studentId }, { user, roles, dataSources }) =>
dataSources.webshopAPI.getUserInventory({ user, roles }, studentId),
getMembersByProduct: async (_, { productId }, { user, roles, dataSources }) =>
dataSources.webshopAPI.getMembersByProduct({ user, roles }, productId),
},
Mutation: {
webshop: () => ({}),
Expand Down
6 changes: 6 additions & 0 deletions backend/services/core/src/schemas/webshop.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ extend type Query {
myCart: Cart
payment(id: UUID!): Payment
chest(studentId: String!): UserInventory
getMembersByProduct(productId: UUID!): [MemberByProduct]
}

extend type Mutation {
Expand All @@ -27,6 +28,11 @@ type WebshopMutations {
consumeItem(itemId: UUID!): UserInventory
}

type MemberByProduct {
member: Member!
userInventoryItem: UserInventoryItem!
}

enum PaymentStatus {
PENDING
PAID
Expand Down
22 changes: 22 additions & 0 deletions backend/services/core/src/types/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,12 @@ export type MemberMandatesArgs = {
onlyActive?: InputMaybe<Scalars['Boolean']['input']>;
};

export type MemberByProduct = {
__typename?: 'MemberByProduct';
member: Member;
userInventoryItem: UserInventoryItem;
};

export type MemberFilter = {
class_programme?: InputMaybe<Scalars['String']['input']>;
class_year?: InputMaybe<Scalars['Int']['input']>;
Expand Down Expand Up @@ -1230,6 +1236,7 @@ export type Query = {
event?: Maybe<Event>;
events?: Maybe<EventPagination>;
files?: Maybe<Array<FileData>>;
getMembersByProduct?: Maybe<Array<Maybe<MemberByProduct>>>;
getSubscriptionTypes: Array<SubscriptionType>;
governingDocument?: Maybe<GoverningDocument>;
governingDocuments: Array<GoverningDocument>;
Expand Down Expand Up @@ -1346,6 +1353,11 @@ export type QueryFilesArgs = {
};


export type QueryGetMembersByProductArgs = {
productId: Scalars['UUID']['input'];
};


export type QueryGoverningDocumentArgs = {
id: Scalars['UUID']['input'];
};
Expand Down Expand Up @@ -2013,6 +2025,7 @@ export type ResolversTypes = ResolversObject<{
MarkdownMutations: ResolverTypeWrapper<MarkdownMutations>;
MarkdownPayload: ResolverTypeWrapper<MarkdownPayload>;
Member: ResolverTypeWrapper<Member>;
MemberByProduct: ResolverTypeWrapper<MemberByProduct>;
MemberFilter: MemberFilter;
MemberMutations: ResolverTypeWrapper<MemberMutations>;
MemberPagination: ResolverTypeWrapper<MemberPagination>;
Expand Down Expand Up @@ -2153,6 +2166,7 @@ export type ResolversParentTypes = ResolversObject<{
MarkdownMutations: MarkdownMutations;
MarkdownPayload: MarkdownPayload;
Member: Member;
MemberByProduct: MemberByProduct;
MemberFilter: MemberFilter;
MemberMutations: MemberMutations;
MemberPagination: MemberPagination;
Expand Down Expand Up @@ -2682,6 +2696,12 @@ export type MemberResolvers<ContextType = any, ParentType extends ResolversParen
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

export type MemberByProductResolvers<ContextType = any, ParentType extends ResolversParentTypes['MemberByProduct'] = ResolversParentTypes['MemberByProduct']> = ResolversObject<{
member?: Resolver<ResolversTypes['Member'], ParentType, ContextType>;
userInventoryItem?: Resolver<ResolversTypes['UserInventoryItem'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

export type MemberMutationsResolvers<ContextType = any, ParentType extends ResolversParentTypes['MemberMutations'] = ResolversParentTypes['MemberMutations']> = ResolversObject<{
create?: Resolver<Maybe<ResolversTypes['Member']>, ParentType, ContextType, RequireFields<MemberMutationsCreateArgs, 'input'>>;
ping?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType, RequireFields<MemberMutationsPingArgs, 'id'>>;
Expand Down Expand Up @@ -2865,6 +2885,7 @@ export type QueryResolvers<ContextType = any, ParentType extends ResolversParent
event?: Resolver<Maybe<ResolversTypes['Event']>, ParentType, ContextType, Partial<QueryEventArgs>>;
events?: Resolver<Maybe<ResolversTypes['EventPagination']>, ParentType, ContextType, Partial<QueryEventsArgs>>;
files?: Resolver<Maybe<Array<ResolversTypes['FileData']>>, ParentType, ContextType, RequireFields<QueryFilesArgs, 'bucket' | 'prefix'>>;
getMembersByProduct?: Resolver<Maybe<Array<Maybe<ResolversTypes['MemberByProduct']>>>, ParentType, ContextType, RequireFields<QueryGetMembersByProductArgs, 'productId'>>;
getSubscriptionTypes?: Resolver<Array<ResolversTypes['SubscriptionType']>, ParentType, ContextType>;
governingDocument?: Resolver<Maybe<ResolversTypes['GoverningDocument']>, ParentType, ContextType, RequireFields<QueryGoverningDocumentArgs, 'id'>>;
governingDocuments?: Resolver<Array<ResolversTypes['GoverningDocument']>, ParentType, ContextType>;
Expand Down Expand Up @@ -3116,6 +3137,7 @@ export type Resolvers<ContextType = any> = ResolversObject<{
MarkdownMutations?: MarkdownMutationsResolvers<ContextType>;
MarkdownPayload?: MarkdownPayloadResolvers<ContextType>;
Member?: MemberResolvers<ContextType>;
MemberByProduct?: MemberByProductResolvers<ContextType>;
MemberMutations?: MemberMutationsResolvers<ContextType>;
MemberPagination?: MemberPaginationResolvers<ContextType>;
Mutation?: MutationResolvers<ContextType>;
Expand Down
32 changes: 31 additions & 1 deletion frontend/api/products.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,35 @@ query ProductCategories {
}
}

query GetMembersByProduct($productId: UUID!) {
getMembersByProduct(productId: $productId) {
member {
id
student_id
first_name
nickname
last_name
picture_path
food_preference
}
userInventoryItem {
id
name
description
paidPrice
imageUrl
variant
category {
id
name
description
}
paidAt
consumedAt
}
}
}

mutation CreateProduct($input: CreateProductInput!) {
webshop {
createProduct(input: $input) {
Expand Down Expand Up @@ -152,4 +181,5 @@ mutation DeleteProduct($productId: UUID!) {
webshop {
deleteProduct(productId: $productId)
}
}
}

5 changes: 4 additions & 1 deletion frontend/components/Webshop/ManageInventoryItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { useSnackbar } from '~/providers/SnackbarProvider';

export default function ManageInventoryItem({
inventoryItem,
refetch,
}: {
inventoryItem: ProductQuery['product']['inventory'][number]
refetch: () => Promise<any>
}) {
const [variant, setVariant] = useState(inventoryItem.variant);
const [quantity, setQuantity] = useState(inventoryItem.quantity.toString());
Expand All @@ -24,7 +26,8 @@ export default function ManageInventoryItem({
});

const [deleteInventory] = useDeleteInventoryMutation({
onCompleted: () => {
onCompleted: async () => {
await refetch();
showMessage('Product inventory deleted', 'success');
},
onError: (error) => {
Expand Down
6 changes: 5 additions & 1 deletion frontend/components/Webshop/ManageProductInventory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ export default function ManageProductInventory({
<Stack direction="row" spacing={2}>
{
inventory?.map((inventoryItem) => (
<ManageInventoryItem key={inventoryItem.id} inventoryItem={inventoryItem} />
<ManageInventoryItem
refetch={refetch}
key={inventoryItem.id}
inventoryItem={inventoryItem}
/>
))
}
</Stack>
Expand Down
63 changes: 63 additions & 0 deletions frontend/components/Webshop/MembersByProduct.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Stack, Typography } from '@mui/material';
import MUIDataTable from 'mui-datatables';
import { GetMembersByProductQuery, useGetMembersByProductQuery } from '~/generated/graphql';
import LoadingButton from '../LoadingButton';

const columns = [
'First Name',
'Last Name',
'Student ID',
'Food Preference',
'Variant',
'Consumed At',
'Paid At',
];

const options = {
filterType: 'checkbox',
};

function createDataItem(item: GetMembersByProductQuery['getMembersByProduct'][number]) {
return [
item.member.first_name,
item.member.last_name,
item.member.student_id,
item.member.food_preference,
item.userInventoryItem.variant,
item.userInventoryItem.consumedAt,
item.userInventoryItem.paidAt,
];
}

export default function MembersByProduct({ productId }: { productId: string }) {
const { data: graphqlData, refetch } = useGetMembersByProductQuery({
variables: {
productId,
},
});
const data = graphqlData?.getMembersByProduct.map(createDataItem) ?? [
'Loading...',
];
return (
<Stack spacing={1}>
<h2>Members by product</h2>
<Typography>
Total purchases:
{' '}
{graphqlData?.getMembersByProduct.length}
</Typography>
<LoadingButton onClick={async () => {
await refetch();
}}
>
Reload data
</LoadingButton>
<MUIDataTable
title="Members with this item in their inventory"
data={data}
columns={columns}
options={options}
/>
</Stack>
);
}
Loading

0 comments on commit 47aedb3

Please sign in to comment.