Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pages order info #2104

Merged
merged 14 commits into from
Nov 7, 2024
Merged
17 changes: 15 additions & 2 deletions backend/apps/ecommerce/resolvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,22 @@
def resolve_all_user_orders(self, info):
return Order.objects.all()

@login_required
@staff_member_required
def resolve_all_shop_orders(self, info):
return Order.objects.filter(product__shop_item=True)
def resolve_paginated_shop_orders(self, info, limit, offset):
# Apply pagination if limit and offset are provided
orders = Order.objects.filter(product__shop_item=True).order_by(
"delivered_product", "payment_status", "timestamp"
)
if offset is None:
offset = 0

Check warning on line 49 in backend/apps/ecommerce/resolvers.py

View check run for this annotation

Codecov / codecov/patch

backend/apps/ecommerce/resolvers.py#L49

Added line #L49 was not covered by tests
orders = orders[offset:]

if limit is None or limit > 300:
# Add hard cap of maximum 300 orders per query. A bigger query would crash the website
limit = 300

Check warning on line 54 in backend/apps/ecommerce/resolvers.py

View check run for this annotation

Codecov / codecov/patch

backend/apps/ecommerce/resolvers.py#L54

Added line #L54 was not covered by tests
orders = orders[:limit]
return orders

@staff_member_required
def resolve_orders_by_status(self, info: "ResolveInfo", product_id, status):
Expand Down
2 changes: 1 addition & 1 deletion backend/apps/ecommerce/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class EcommerceQueries(graphene.ObjectType, EcommerceResolvers):
order = graphene.Field(OrderType, order_id=graphene.ID(required=True))
user_orders = graphene.List(NonNull(OrderType))
all_user_orders = graphene.List(NonNull(OrderType))
all_shop_orders = graphene.List(NonNull(OrderType))
paginated_shop_orders = graphene.List(graphene.NonNull(OrderType), limit=graphene.Int(), offset=graphene.Int())

orders_by_status = graphene.Field(
OrdersByStatusType, product_id=graphene.ID(required=True), status=graphene.String(required=True)
Expand Down
82 changes: 82 additions & 0 deletions backend/apps/ecommerce/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,3 +457,85 @@ def test_delivered_product(self) -> None:

order = Order.objects.get(pk=order.id)
self.assertTrue(order.delivered_product)


class PaginatedShopOrdersResolverTests(ExtendedGraphQLTestCase):
def setUp(self):
# Create a staff user using the StaffUserFactory
self.staff_user = StaffUserFactory(username="staffuser")

# Ensure the user satisfies the requirements for the new decorators
self.staff_user.is_staff = True # Required for @staff_member_required
self.staff_user.is_superuser = True # Required for @superuser_required if added
self.staff_user.save()

# Create a product with `shop_item=True` to pass the resolver's filter condition
self.product = ProductFactory(
name="Test Product",
price=decimal.Decimal("1000.00"),
description="A test product description",
max_buyable_quantity=2,
total_quantity=5,
shop_item=True, # Ensure this matches the resolver's filter condition
)

# Create multiple orders associated with the product and user
self.orders = [
OrderFactory(
product=self.product,
user=self.staff_user,
payment_status=Order.PaymentStatus.INITIATED,
)
for i in range(10)
]

def test_paginated_shop_orders_with_fragment_and_product(self):
query = """
query paginatedShopOrders($limit: Int, $offset: Int) {
paginatedShopOrders(limit: $limit, offset: $offset) {
...Order
}
}

fragment Order on OrderType {
id
quantity
totalPrice
paymentStatus
timestamp
deliveredProduct
product {
...Product
}
}

fragment Product on ProductType {
id
name
price
description
maxBuyableQuantity
shopItem
}
"""

# Execute the query using the query method from ExtendedGraphQLTestCase
response = self.query(query, variables={"limit": 5, "offset": 2}, user=self.staff_user) # Use staff user
data = json.loads(response.content)

# Check if the response data matches expectations
self.assertIn("data", data)
self.assertIn("paginatedShopOrders", data["data"])
self.assertEqual(len(data["data"]["paginatedShopOrders"]), 5)

# Verify the structure of the nested product field in the first order
first_order = data["data"]["paginatedShopOrders"][0]
self.assertIn("product", first_order)
self.assertEqual(first_order["product"]["name"], "Test Product")
self.assertEqual(first_order["product"]["price"], "1000.00") # Adjusted for consistency
self.assertTrue(first_order["product"]["shopItem"])

# Additional checks for other fields
self.assertIn("quantity", first_order)
self.assertIn("paymentStatus", first_order)
self.assertIn("timestamp", first_order)
25 changes: 23 additions & 2 deletions backend/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -301,11 +301,32 @@
}
},
{
"args": [],
"args": [
{
"defaultValue": null,
"description": null,
"name": "limit",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
{
"defaultValue": null,
"description": null,
"name": "offset",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
}
],
"deprecationReason": null,
"description": null,
"isDeprecated": false,
"name": "allShopOrders",
"name": "paginatedShopOrders",
"type": {
"kind": "LIST",
"name": null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import { Swiper, SwiperSlide } from "swiper/react";
// Import Swiper styles
import "swiper/css";

import { Organization, OrganizationLink } from "./OrganizationLink";

import { Link } from "@/app/components/Link";

import { Organization, OrganizationLink } from "./OrganizationLink";

const organizations: Readonly<Organization[]> = [
{ name: "Janus Sosial", internalUrl: "/janus" },
{ name: "Bindeleddet", externalUrl: "https://www.bindeleddet.no" },
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/app/_components/LandingHero/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import { Box, Button, Container, Unstable_Grid2 as Grid, Typography } from "@mui/material";
import Image from "next/image";

import { OrganizationsSlider } from "./OrganizationsSlider";

import { Link } from "@/app/components/Link";
import Hero from "~/public/static/landing/hero.webp";

import { OrganizationsSlider } from "./OrganizationsSlider";

export const LandingHero: React.FC = () => {
return (
<>
Expand Down
93 changes: 66 additions & 27 deletions frontend/src/components/pages/organization/OrgShop.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,58 @@
import { useQuery } from "@apollo/client";
import { Box, Grid, Stack, Typography } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { useState } from "react";

import { AdminOrganizationFragment, AllShopOrdersDocument } from "@/generated/graphql";
import { AdminOrganizationFragment, PaginatedShopOrdersDocument } from "@/generated/graphql";

import { ShopSale } from "../orgs/ShopSale";
import { TableStepper } from "../orgs/TableStepper";

type Props = {
organization: AdminOrganizationFragment;
};
export const OrgProducts: React.FC<Props> = ({ organization }) => {
const { data, error } = useQuery(AllShopOrdersDocument);
const theme = useTheme();
const limit = 5;
const [page, setPage] = useState(0);

const handlePageChange = (newValue: number) => {
setPage(newValue);
};

const { data, error, loading } = useQuery(PaginatedShopOrdersDocument, {
variables: {
limit: limit + 1, // The number of orders you want to fetch
offset: page * limit, // The starting index (e.g., 0 for the first set of results)
},
});
if (error) return <p>Error</p>;

console.log(data);
if (organization.name !== "Janus linjeforening") {
if (organization.name.toLowerCase() !== "janus linjeforening") {
return (
<p>
Per nå har kun Janus tilgang på buttikk administrasjon. Etter hvert vil vi åpne for at flere kan bruke siden
</p>
);
}
if (data?.allShopOrders?.length === 0) {
if (data?.paginatedShopOrders!.length === 0) {
return <p>Ingen ordre</p>;
}
// Check if there is a next page (if we received less than `limit` items)
const hasNextPage = (data?.paginatedShopOrders && data.paginatedShopOrders.length < limit) ?? false;

return (
<>
<Stack direction={"row"} padding={2} border={3}>
<Box display="flex" alignItems="left" justifyContent="left" width={"15%"} padding={1}>
<TableStepper page={page} hasNextPage={hasNextPage} handlePageChange={handlePageChange} />
<Stack
direction={"row"}
marginTop={"36px"}
spacing={0}
padding={1}
borderBottom={1}
sx={{ bgcolor: theme.palette.background.elevated, color: theme.palette.text.primary }}
>
<Box display="flex" alignItems="left" justifyContent="left" width={"25%"} padding={1}>
<Typography variant="body1">Navn på kunde</Typography>
</Box>
<Box display="flex" alignItems="left" justifyContent="left" width={"15%"} padding={1}>
Expand All @@ -36,33 +62,46 @@ export const OrgProducts: React.FC<Props> = ({ organization }) => {
<Typography variant="body1">Antall bestilt</Typography>
</Box>
<Box display="flex" alignItems="left" justifyContent="left" width={"15%"} padding={1}>
<Typography variant="body1"> Betalt status</Typography>
<Typography variant="body1">Betalt status</Typography>
</Box>
<Box display="flex" alignItems="left" justifyContent="left" width={"25%"} padding={1}>
<Box display="flex" alignItems="left" justifyContent="left" width={"15%"} padding={1}>
Mulige handlinger
</Box>
<Box display="flex" alignItems="left" justifyContent="left" width={"15%"} padding={1}>
<Typography variant="body1"> Har vi levert varen</Typography>
<Typography variant="body1">Har vi levert varen</Typography>
</Box>
</Stack>
<Grid container spacing={0}>
{data?.allShopOrders?.map((order) => {
return (
<Grid key={order.id} item xs={12} sm={12} md={12}>
<Stack border={1}>
<ShopSale
name={order.user.firstName + " " + order.user.lastName}
product_name={order.product.name}
quantity={order.quantity}
has_paid={order.paymentStatus === "CAPTURED"}
is_delivered={order.deliveredProduct === true}
order_id={order.id}
/>
</Stack>

{loading ? (
<Typography padding={1}>Loading...</Typography>
) : (
data && (
<>
<Grid container spacing={0}>
{data?.paginatedShopOrders?.slice(0, limit).map((order) => {
return (
<Grid key={order.id} item xs={12} sm={12} md={12}>
<Stack borderBottom={1} borderColor={"gray"}>
<ShopSale
name={order.user.firstName + " " + order.user.lastName}
product_name={order.product.name}
quantity={order.quantity}
has_paid={order.paymentStatus === "CAPTURED"}
is_delivered={order.deliveredProduct === true}
order_id={order.id}
/>
</Stack>
</Grid>
);
})}
</Grid>
);
})}
</Grid>

<Stack marginTop={"18px"}>
<TableStepper page={page} hasNextPage={hasNextPage} handlePageChange={handlePageChange} />
</Stack>
</>
)
)}
</>
);
};
8 changes: 4 additions & 4 deletions frontend/src/components/pages/orgs/ShopSale.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export const ShopSale: React.FC<Props> = ({ name, product_name, quantity, has_pa
deliverProduct({ variables: { orderId: order_id } });
}
return (
<Stack direction={"row"} padding={2} spacing={1}>
<Box display="flex" alignItems="left" justifyContent="left" width={"15%"} padding={1}>
<Stack direction={"row"} padding={1} spacing={0}>
<Box display="flex" alignItems="left" justifyContent="left" width={"25%"} padding={1}>
<Typography variant="body1">{name}</Typography>
</Box>
<Box display="flex" alignItems="left" justifyContent="left" width={"15%"} padding={1}>
Expand All @@ -36,7 +36,7 @@ export const ShopSale: React.FC<Props> = ({ name, product_name, quantity, has_pa
<Box display="flex" alignItems="left" justifyContent="left" width={"15%"} padding={1}>
<Typography variant="body1">Betalt: {has_paid ? "Ja" : "Nei"}</Typography>
</Box>
<Box display="flex" alignItems="left" justifyContent="left" width={"25%"} padding={1}>
<Box display="flex" alignItems="left" justifyContent="left" width={"15%"} padding={1}>
<Stack direction={"row"} spacing={1}>
<Tooltip title="Levert">
<Box display="inline" component="span">
Expand Down Expand Up @@ -69,7 +69,7 @@ export const ShopSale: React.FC<Props> = ({ name, product_name, quantity, has_pa
</Stack>
</Box>
<Box display="flex" alignItems="left" justifyContent="left" width={"15%"} padding={1}>
<Typography variant="body1">Varen er {delivered ? "levert" : "ikke levert"}</Typography>
<Typography variant="body1">{delivered ? "Levert" : "Ikke levert"}</Typography>
</Box>
</Stack>
);
Expand Down
41 changes: 41 additions & 0 deletions frontend/src/components/pages/orgs/TableStepper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { KeyboardArrowLeft, KeyboardArrowRight, KeyboardDoubleArrowLeft, KeyboardDoubleArrowRight } from "@mui/icons-material";
import { Button, Typography, Stack } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import React from "react";

type Props = {
hasNextPage: boolean;
page: number;
handlePageChange: (page: number) => void;
};

export const TableStepper: React.FC<Props> = ({ hasNextPage, page, handlePageChange }) => {
const theme = useTheme();
const nextPage = () => handlePageChange(page + 1);
const prevPage = () => handlePageChange(page > 0 ? page - 1 : 0);
const resetPage = () => handlePageChange(0)

return (
<Stack
sx={{ maxWidth: 300, flexGrow: 1, position: "static", flexDirection: "row", justifyContent: "space-between" }}
>
<Stack sx ={{flexDirection: "row"}}>
<Button size="small" onClick={resetPage} disabled={page === 0}>
{theme.direction === "rtl" ? <KeyboardDoubleArrowRight /> : <KeyboardDoubleArrowLeft />}
First
</Button>
<Button size="small" onClick={prevPage} disabled={page === 0}>
{theme.direction === "rtl" ? <KeyboardArrowRight /> : <KeyboardArrowLeft />}
Back
</Button>
</Stack>

<Typography>{page + 1}</Typography>

<Button size="small" onClick={nextPage} disabled={hasNextPage}>
Next
{theme.direction === "rtl" ? <KeyboardArrowLeft /> : <KeyboardArrowRight />}
</Button>
</Stack>
);
};
Loading
Loading