From b482437a76bade51b3b21f366c4c9b52e724e6d7 Mon Sep 17 00:00:00 2001 From: rachelhlin Date: Thu, 1 May 2025 20:38:38 -0700 Subject: [PATCH 1/5] Updates to Order Detail and List Views updated front-end for about half of order detail view, currently editing order card files to fit the new figma files --- .../_components/OrderCard.module.scss | 12 +- .../order-cards/_components/OrderCard.tsx | 2 +- .../(app)/orders/order-cards/page.module.scss | 8 +- app/(pages)/(app)/orders/order-cards/page.tsx | 4 +- .../_components/OrderPart.module.scss | 68 ++++++++ .../order-details/_components/OrderPart.tsx | 31 ++++ .../order-details/_components/material.jpg | Bin 0 -> 731031 bytes .../orders/order-details/page.module.scss | 151 ++++++++++++++++++ .../(app)/orders/order-details/page.tsx | 59 +++++++ 9 files changed, 326 insertions(+), 9 deletions(-) create mode 100644 app/(pages)/(app)/orders/order-details/_components/OrderPart.module.scss create mode 100644 app/(pages)/(app)/orders/order-details/_components/OrderPart.tsx create mode 100644 app/(pages)/(app)/orders/order-details/_components/material.jpg create mode 100644 app/(pages)/(app)/orders/order-details/page.module.scss create mode 100644 app/(pages)/(app)/orders/order-details/page.tsx diff --git a/app/(pages)/(app)/orders/order-cards/_components/OrderCard.module.scss b/app/(pages)/(app)/orders/order-cards/_components/OrderCard.module.scss index 2423aaa..43d3591 100644 --- a/app/(pages)/(app)/orders/order-cards/_components/OrderCard.module.scss +++ b/app/(pages)/(app)/orders/order-cards/_components/OrderCard.module.scss @@ -81,10 +81,14 @@ } .info_button { - height: 47px; - width: 47px; + width: 172px; + height: 64px; cursor: pointer; - background-color: var(--purple); + background-color: #3A2779; border-style: none; - border-radius: 10%; + border-radius: 10px; + +} +.text{ + color: white; } \ No newline at end of file diff --git a/app/(pages)/(app)/orders/order-cards/_components/OrderCard.tsx b/app/(pages)/(app)/orders/order-cards/_components/OrderCard.tsx index 8820c35..5a09cc5 100644 --- a/app/(pages)/(app)/orders/order-cards/_components/OrderCard.tsx +++ b/app/(pages)/(app)/orders/order-cards/_components/OrderCard.tsx @@ -68,7 +68,7 @@ const OrderCard: React.FC = (props) => { ); diff --git a/app/(pages)/(app)/orders/order-cards/page.module.scss b/app/(pages)/(app)/orders/order-cards/page.module.scss index cfa24ed..7a87d6c 100644 --- a/app/(pages)/(app)/orders/order-cards/page.module.scss +++ b/app/(pages)/(app)/orders/order-cards/page.module.scss @@ -3,14 +3,18 @@ .page_container { display: flex; flex-direction: column; - background-color: var(--background-primary); + background-color: #F6F6F6; padding: var(--small-spacer) var(--medium-spacer); gap: var(--small-spacer); - + margin-left: 300px; h4 { font-weight: 600; padding: var(--small-spacer) 0; } + +} +.text { + font-size: 48px; } .order_form { diff --git a/app/(pages)/(app)/orders/order-cards/page.tsx b/app/(pages)/(app)/orders/order-cards/page.tsx index 6cb6d55..4b1dcfc 100644 --- a/app/(pages)/(app)/orders/order-cards/page.tsx +++ b/app/(pages)/(app)/orders/order-cards/page.tsx @@ -59,13 +59,13 @@ export default function ViewOrderCards() { return (
-

Orders

+

Orders

+ + +
+ +
+ +
+ + + ); +} \ No newline at end of file From 11ec9c1bbc47fcc993049f110d0c8569c7d471eb Mon Sep 17 00:00:00 2001 From: rachelhlin Date: Thu, 15 May 2025 20:22:13 -0700 Subject: [PATCH 2/5] updated order cards Update to Order Cards and list page to look at all cards --- .../_components/OrderCard.module.scss | 24 ++++++--- .../order-cards/_components/OrderCard.tsx | 54 ++++++++++--------- .../(app)/orders/order-cards/page.module.scss | 48 +++++++++++++++-- app/(pages)/(app)/orders/order-cards/page.tsx | 41 +++++++++++--- 4 files changed, 121 insertions(+), 46 deletions(-) diff --git a/app/(pages)/(app)/orders/order-cards/_components/OrderCard.module.scss b/app/(pages)/(app)/orders/order-cards/_components/OrderCard.module.scss index 43d3591..9de6820 100644 --- a/app/(pages)/(app)/orders/order-cards/_components/OrderCard.module.scss +++ b/app/(pages)/(app)/orders/order-cards/_components/OrderCard.module.scss @@ -1,7 +1,9 @@ @use 'media'; +@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700&display=swap'); .container { - width: 60%; + width: 100%; + height: 260px; display: flex; flex-direction: row; align-items: center; @@ -10,17 +12,22 @@ background-color: var(--secondary); background-color: var(--background-secondary); color: var(--text-light); - border-style: solid; - border-width: 2px; // change from hard code - border-color: var(--gray-100); + box-shadow: 0px 4px 20px 0px #3333330F; + border-radius: 10px; @include media.tablet { flex-direction: column; gap: var(--small-spacer); } } - +.imginfo{ + width: 70%; + display: flex; + align-items: center; + +} .item_image { + margin-right: 40px; width: 185px; height: 140px; position: relative; @@ -33,10 +40,11 @@ display: flex; flex-direction: column; gap: var(--tiny-spacer); - - h4 { + + p { + font-family: 'Manrope', sans-serif; font-size: 1.2rem; - font-weight: 600; + font-weight: 400; } } diff --git a/app/(pages)/(app)/orders/order-cards/_components/OrderCard.tsx b/app/(pages)/(app)/orders/order-cards/_components/OrderCard.tsx index 5a09cc5..e62b866 100644 --- a/app/(pages)/(app)/orders/order-cards/_components/OrderCard.tsx +++ b/app/(pages)/(app)/orders/order-cards/_components/OrderCard.tsx @@ -17,10 +17,10 @@ interface TextboxProps { const OrderCard: React.FC = (props) => { const itemStatuses = [ - 'Customer', - 'Payment', - 'Shipping', - 'Confirm', + 'Ordered', + 'Packed', + 'Shipped', + 'In Transit', 'Delivered', ]; @@ -43,29 +43,31 @@ const OrderCard: React.FC = (props) => { } return (
-
- product image -
-
-

{props.title}

-

Order Placed: {props.date.toDateString()}

-
-
- {itemStatuses.map((status, index) => ( -
- {props.status <= 4 && - displayIcon(props.status, props.icon, index) !== '' ? ( - check icon - ) : ( -
- )} -

{status}

-
- ))} +
+
+ product image
+
+

{props.title}

+

Order Placed: {props.date.toDateString()}

+
+
+ {itemStatuses.map((status, index) => ( +
+ {props.status <= 4 && + displayIcon(props.status, props.icon, index) !== '' ? ( + check icon + ) : ( +
+ )} +

{status}

+
+ ))} +
+
+ ))} +
{progressList.map((item, index) => (
From acf393a60e392d32db538aa1cafcd22dd0a3574a Mon Sep 17 00:00:00 2001 From: brandonw504 Date: Thu, 10 Jul 2025 00:04:04 -0700 Subject: [PATCH 3/5] Order modifications --- app/(api)/_datalib/_resolvers/Order.ts | 9 +- app/(api)/_datalib/_services/Orders.ts | 125 ++++++----- app/(api)/_datalib/_typeDefs/Order.ts | 18 +- app/(api)/_types/Order.ts | 15 +- .../_components/Hero/Hero.module.scss | 41 ---- .../(index-page)/_components/Hero/Hero.tsx | 21 -- app/(pages)/(app)/(index-page)/page.tsx | 9 - .../_components/OrderCard.module.scss | 182 ++++++++++++++++ .../(app)/(orders)/_components/OrderCard.tsx | 77 +++++++ .../_components/OrderPart.module.scss | 0 .../order-details/_components/OrderPart.tsx | 31 +++ .../order-details/_components/material.jpg | Bin .../order-details/page.module.scss | 0 .../(app)/(orders)/order-details/page.tsx | 57 +++++ app/(pages)/(app)/(orders)/page.module.scss | 204 ++++++++++++++++++ app/(pages)/(app)/(orders)/page.tsx | 187 ++++++++++++++++ .../ContactInfo/ContactInfo.module.scss | 0 .../_components/ContactInfo/ContactInfo.tsx | 0 .../CreditCardPayment.module.scss | 0 .../CreditCardPayment/CreditCardPayment.tsx | 0 .../Navigator/Navigator.module.scss | 0 .../_components/Navigator/Navigator.tsx | 0 .../OrderDescription.module.scss | 0 .../OrderDescription/OrderDescription.tsx | 0 .../ProgessBar/ProgressBar.module.scss | 0 .../_components/ProgessBar/ProgressBar.tsx | 0 .../ShippingLabel/ShippingLabel.module.scss | 0 .../ShippingLabel/ShippingLabel.tsx | 0 .../view-order/page.module.scss | 0 .../{orders => (orders)}/view-order/page.tsx | 0 .../_components/OrderCard.module.scss | 102 --------- .../order-cards/_components/OrderCard.tsx | 79 ------- .../(app)/orders/order-cards/page.module.scss | 114 ---------- app/(pages)/(app)/orders/order-cards/page.tsx | 141 ------------ .../order-details/_components/OrderPart.tsx | 31 --- .../(app)/orders/order-details/page.tsx | 59 ----- .../_components/Sidebar/Sidebar.module.scss | 2 +- app/(pages)/_components/Sidebar/Sidebar.tsx | 28 +-- .../20250710064028_order_status/migration.sql | 12 ++ prisma/schema/Order.prisma | 12 +- 40 files changed, 872 insertions(+), 684 deletions(-) delete mode 100644 app/(pages)/(app)/(index-page)/_components/Hero/Hero.module.scss delete mode 100644 app/(pages)/(app)/(index-page)/_components/Hero/Hero.tsx delete mode 100644 app/(pages)/(app)/(index-page)/page.tsx create mode 100644 app/(pages)/(app)/(orders)/_components/OrderCard.module.scss create mode 100644 app/(pages)/(app)/(orders)/_components/OrderCard.tsx rename app/(pages)/(app)/{orders => (orders)}/order-details/_components/OrderPart.module.scss (100%) create mode 100644 app/(pages)/(app)/(orders)/order-details/_components/OrderPart.tsx rename app/(pages)/(app)/{orders => (orders)}/order-details/_components/material.jpg (100%) rename app/(pages)/(app)/{orders => (orders)}/order-details/page.module.scss (100%) create mode 100644 app/(pages)/(app)/(orders)/order-details/page.tsx create mode 100644 app/(pages)/(app)/(orders)/page.module.scss create mode 100644 app/(pages)/(app)/(orders)/page.tsx rename app/(pages)/(app)/{orders => (orders)}/view-order/_components/ContactInfo/ContactInfo.module.scss (100%) rename app/(pages)/(app)/{orders => (orders)}/view-order/_components/ContactInfo/ContactInfo.tsx (100%) rename app/(pages)/(app)/{orders => (orders)}/view-order/_components/CreditCardPayment/CreditCardPayment.module.scss (100%) rename app/(pages)/(app)/{orders => (orders)}/view-order/_components/CreditCardPayment/CreditCardPayment.tsx (100%) rename app/(pages)/(app)/{orders => (orders)}/view-order/_components/Navigator/Navigator.module.scss (100%) rename app/(pages)/(app)/{orders => (orders)}/view-order/_components/Navigator/Navigator.tsx (100%) rename app/(pages)/(app)/{orders => (orders)}/view-order/_components/OrderDescription/OrderDescription.module.scss (100%) rename app/(pages)/(app)/{orders => (orders)}/view-order/_components/OrderDescription/OrderDescription.tsx (100%) rename app/(pages)/(app)/{orders => (orders)}/view-order/_components/ProgessBar/ProgressBar.module.scss (100%) rename app/(pages)/(app)/{orders => (orders)}/view-order/_components/ProgessBar/ProgressBar.tsx (100%) rename app/(pages)/(app)/{orders => (orders)}/view-order/_components/ShippingLabel/ShippingLabel.module.scss (100%) rename app/(pages)/(app)/{orders => (orders)}/view-order/_components/ShippingLabel/ShippingLabel.tsx (100%) rename app/(pages)/(app)/{orders => (orders)}/view-order/page.module.scss (100%) rename app/(pages)/(app)/{orders => (orders)}/view-order/page.tsx (100%) delete mode 100644 app/(pages)/(app)/orders/order-cards/_components/OrderCard.module.scss delete mode 100644 app/(pages)/(app)/orders/order-cards/_components/OrderCard.tsx delete mode 100644 app/(pages)/(app)/orders/order-cards/page.module.scss delete mode 100644 app/(pages)/(app)/orders/order-cards/page.tsx delete mode 100644 app/(pages)/(app)/orders/order-details/_components/OrderPart.tsx delete mode 100644 app/(pages)/(app)/orders/order-details/page.tsx create mode 100644 prisma/migrations/20250710064028_order_status/migration.sql diff --git a/app/(api)/_datalib/_resolvers/Order.ts b/app/(api)/_datalib/_resolvers/Order.ts index 858b56c..e218b95 100644 --- a/app/(api)/_datalib/_resolvers/Order.ts +++ b/app/(api)/_datalib/_resolvers/Order.ts @@ -1,5 +1,10 @@ import Orders from '../_services/Orders'; -import { OrderInput, Order, OrderProductInput } from '@datatypes/Order'; +import { + OrderInput, + Order, + OrderProductInput, + OrderStatus, +} from '@datatypes/Order'; import { ApolloContext } from '../apolloServer'; const resolvers = { @@ -13,7 +18,7 @@ const resolvers = { orders: ( _: never, args: { - statuses: string[]; + statuses: OrderStatus[]; search: string; offset: number; limit: number; diff --git a/app/(api)/_datalib/_services/Orders.ts b/app/(api)/_datalib/_services/Orders.ts index c38a7ee..d61881c 100644 --- a/app/(api)/_datalib/_services/Orders.ts +++ b/app/(api)/_datalib/_services/Orders.ts @@ -1,6 +1,11 @@ import revalidateCache from '@actions/revalidateCache'; import prisma from '../_prisma/client'; -import { OrderInput, OrderProductInput } from '@datatypes/Order'; +import { + OrderInput, + OrderProductInput, + OrderStatus, + OrderUpdateInput, +} from '@datatypes/Order'; import { ApolloContext } from '../apolloServer'; import { Prisma } from '@prisma/client'; import Stripe from 'stripe'; @@ -14,7 +19,7 @@ export default class Orders { data: { ...input, // Spread the input fields total: 0, - status: 'pending', // Default status + status: OrderStatus.PENDING, // Default status created_at: new Date(), // Current timestamp }, }); @@ -34,7 +39,7 @@ export default class Orders { } static async findMany( - statuses: string[], + statuses: OrderStatus[], search: string, offset: number, limit: number, @@ -109,7 +114,7 @@ export default class Orders { } //UPDATE - static async update(id: number, input: OrderInput, ctx: ApolloContext) { + static async update(id: number, input: OrderUpdateInput, ctx: ApolloContext) { if (!ctx.isOwner && !ctx.hasValidApiKey) return null; try { @@ -288,7 +293,7 @@ export default class Orders { }); if (order) { - if (order.status == 'refunded') { + if (order.status == 'REFUNDED') { await prisma.order.delete({ where: { id, @@ -302,7 +307,7 @@ export default class Orders { id, }, data: { - status: 'needs refund', + status: 'CANCELLED', }, }); // could possibly add a message? "this order need to be refunded" or smth @@ -321,70 +326,62 @@ export default class Orders { products: OrderProductInput[], ctx: ApolloContext ) { - if (!ctx.isOwner && !ctx.hasValidApiKey) return null; - - try { - const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { - apiVersion: '2025-05-28.basil', // explicitly set the API version - }); - - // Lookup product prices from DB - const productIds = products.map((p) => p.product_id); - const dbProducts = await prisma.product.findMany({ - where: { id: { in: productIds } }, - }); - - const productMap = Object.fromEntries(dbProducts.map((p) => [p.id, p])); - - const total = products.reduce((sum, item) => { - const product = productMap[item.product_id]; - return sum + (product?.price ?? 0) * item.quantity; - }, 0); + if (!ctx.isOwner && !ctx.hasValidApiKey) { + throw new Error('Unauthorized'); + } - // Stripe counts payment amounts in cents - const amountInCents = Math.round(total * 100); + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, { + apiVersion: '2025-05-28.basil', + }); - // Create the order - const createdOrder = await prisma.order.create({ - data: { - ...input, - total: total, - status: 'pending', - created_at: new Date(), - products: { - create: products.map((p) => ({ - quantity: p.quantity, - product: { connect: { id: p.product_id } }, - })), - }, - }, - include: { products: { include: { product: true } } }, - }); + let total = 0; + await Promise.all( + products.map(async (p) => { + const product = await prisma.product.findUnique({ + where: { id: p.product_id }, + }); + if (!product) { + throw new Error(`Product with ID ${p.product_id} not found`); + } + total += (product.price - (product.discount ?? 0)) * p.quantity; + }) + ); - // Create Stripe PaymentIntent - const paymentIntent = await stripe.paymentIntents.create({ - amount: amountInCents, - currency: 'usd', - metadata: { - orderId: createdOrder.id, + const order = await prisma.order.create({ + data: { + ...input, + total, + status: OrderStatus.ORDERED, + created_at: new Date(), + products: { + create: products.map((p) => ({ + product_id: p.product_id, + quantity: p.quantity, + })), }, - }); + }, + include: { + products: true, + }, + }); - // Save paymentIntentId to order - const updatedOrder = await prisma.order.update({ - where: { id: createdOrder.id }, - data: { paymentIntentId: paymentIntent.id }, - include: { products: { include: { product: true } } }, - }); + const paymentIntent = await stripe.paymentIntents.create({ + amount: Math.round(total * 100), + currency: 'usd', + metadata: { + orderId: order.id, + }, + }); - revalidateCache(['orders', 'products']); + await prisma.order.update({ + where: { id: order.id }, + data: { paymentIntentId: paymentIntent.id }, + }); - return { - order: updatedOrder, - clientSecret: paymentIntent.client_secret, - }; - } catch (e) { - return e; - } + revalidateCache(['orders']); + return { + order, + clientSecret: paymentIntent.client_secret, + }; } } diff --git a/app/(api)/_datalib/_typeDefs/Order.ts b/app/(api)/_datalib/_typeDefs/Order.ts index f2a1755..e3f1dd8 100644 --- a/app/(api)/_datalib/_typeDefs/Order.ts +++ b/app/(api)/_datalib/_typeDefs/Order.ts @@ -1,6 +1,16 @@ import gql from 'graphql-tag'; const typeDefs = gql` + enum OrderStatus { + PENDING + ORDERED + SHIPPED + IN_TRANSIT + DELIVERED + CANCELLED + REFUNDED + } + type Order { id: ID! paymentIntentId: String @@ -19,7 +29,7 @@ const typeDefs = gql` shipping_city: String! shipping_zip: String! shipping_country: String! - status: String! + status: OrderStatus! created_at: String! } @@ -37,7 +47,7 @@ const typeDefs = gql` shipping_city: String! shipping_zip: String! shipping_country: String! - status: String! + status: OrderStatus } input OrderUpdateInput { @@ -55,7 +65,7 @@ const typeDefs = gql` shipping_city: String shipping_zip: String shipping_country: String - status: String + status: OrderStatus } input OrderProductInput { @@ -76,7 +86,7 @@ const typeDefs = gql` type Query { order(id: ID!): Order orders( - statuses: [String] + statuses: [OrderStatus] search: String offset: Int! limit: Int! diff --git a/app/(api)/_types/Order.ts b/app/(api)/_types/Order.ts index e71ca62..b5dfadf 100644 --- a/app/(api)/_types/Order.ts +++ b/app/(api)/_types/Order.ts @@ -1,5 +1,15 @@ import { Product } from './Product'; +export enum OrderStatus { + PENDING = 'PENDING', + ORDERED = 'ORDERED', + SHIPPED = 'SHIPPED', + IN_TRANSIT = 'IN_TRANSIT', + DELIVERED = 'DELIVERED', + CANCELLED = 'CANCELLED', + REFUNDED = 'REFUNDED', +} + export type Order = { id: number; total: number; @@ -16,8 +26,8 @@ export type Order = { shipping_city: string; shipping_zip: string; shipping_country: string; - status: string; - created_at: Date; + status: OrderStatus; + created_at: string; products: ProductToOrder[]; }; @@ -53,6 +63,7 @@ export type OrderInput = { shipping_city: string; shipping_zip: string; shipping_country: string; + status?: OrderStatus; }; //make all inputs optional diff --git a/app/(pages)/(app)/(index-page)/_components/Hero/Hero.module.scss b/app/(pages)/(app)/(index-page)/_components/Hero/Hero.module.scss deleted file mode 100644 index b31ed3b..0000000 --- a/app/(pages)/(app)/(index-page)/_components/Hero/Hero.module.scss +++ /dev/null @@ -1,41 +0,0 @@ -@use 'media'; - -.container { - display: flex; - flex-direction: row; - justify-content: center; - height: calc(100vh - var(--navbar-height)); - width: 100%; - position: relative; - padding: var(--large-spacer); - @include media.phone { - padding: var(--medium-spacer); - } - - .img_container { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: black; - z-index: -1; - } - - .welcome { - width: 100%; - max-width: 1620px; - height: 100%; - border-radius: var(--b-radius); - border: solid 2px var(--light-background); - display: flex; - flex-direction: column; - justify-content: center; - text-align: center; - padding: var(--spacer); - - > h1 { - color: var(--text-light); - } - } -} \ No newline at end of file diff --git a/app/(pages)/(app)/(index-page)/_components/Hero/Hero.tsx b/app/(pages)/(app)/(index-page)/_components/Hero/Hero.tsx deleted file mode 100644 index 772990b..0000000 --- a/app/(pages)/(app)/(index-page)/_components/Hero/Hero.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Image from 'next/image'; - -import styles from './Hero.module.scss'; - -export default function Hero() { - return ( -
-
- serene forest scene in the night -
-
-

ESTORE MANAGEMENT TOOL

-
-
- ); -} diff --git a/app/(pages)/(app)/(index-page)/page.tsx b/app/(pages)/(app)/(index-page)/page.tsx deleted file mode 100644 index 998843f..0000000 --- a/app/(pages)/(app)/(index-page)/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import Hero from './_components/Hero/Hero'; - -export default function Home() { - return ( -
- -
- ); -} diff --git a/app/(pages)/(app)/(orders)/_components/OrderCard.module.scss b/app/(pages)/(app)/(orders)/_components/OrderCard.module.scss new file mode 100644 index 0000000..0566d48 --- /dev/null +++ b/app/(pages)/(app)/(orders)/_components/OrderCard.module.scss @@ -0,0 +1,182 @@ +@use 'media'; +@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700&display=swap'); + +.container { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + padding: 24px; + background-color: var(--background-secondary); + border-radius: 12px; + box-shadow: 0px 4px 20px rgba(51, 51, 51, 0.06); + margin-bottom: 16px; + + @include media.tablet { + flex-direction: column; + gap: 16px; + } +} + +.content { + display: flex; + align-items: center; + flex: 1; + gap: 24px; +} + +.product_image { + width: 240px; + height: 160px; + position: relative; + border-radius: 8px; + overflow: hidden; + border: 1px solid var(--gray-300); + flex-shrink: 0; + + .image { + object-fit: cover; + } +} + +.order_info { + flex: 1; + display: flex; + flex-direction: column; + gap: 16px; +} + +.order_details { + display: flex; + flex-direction: column; + gap: 4px; +} + +.order_id { + font-family: 'Manrope', sans-serif; + font-size: 18px; + font-weight: 600; + color: var(--text-dark); + margin: 0; +} + +.shipping_info { + font-family: 'Manrope', sans-serif; + font-size: 14px; + font-weight: 400; + color: var(--purple); + margin: 0; +} + +.order_date { + font-family: 'Manrope', sans-serif; + font-size: 14px; + font-weight: 400; + color: var(--gray-600); + margin: 0; +} + +.progress_section { + margin-top: 8px; +} + +.progress_bar { + display: flex; + align-items: center; + justify-content: space-between; + position: relative; + width: 100%; + max-width: 500px; + + &::before { + content: ''; + position: absolute; + top: 20%; + left: 0; + right: 0; + height: 2px; + background-color: var(--gray-300); + z-index: 1; + } +} + +.status_item { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + position: relative; + z-index: 2; +} + +.status_dot { + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid var(--gray-300); + background-color: var(--background-secondary); + position: relative; + + &.completed { + background-color: var(--purple); + border-color: var(--purple); + + &::after { + content: '✓'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: var(--text-light); + font-size: 12px; + font-weight: bold; + } + } + + &.current { + border-color: var(--purple); + background-color: var(--purple); + + &::after { + content: '✓'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: var(--text-light); + font-size: 12px; + font-weight: bold; + } + } + + &.pending { + background-color: var(--background-secondary); + border-color: var(--gray-300); + } +} + +.status_label { + font-family: 'Manrope', sans-serif; + font-size: 12px; + font-weight: 400; + color: var(--gray-600); + text-align: center; + white-space: nowrap; +} + +.view_order_btn { + padding: 12px 24px; + background-color: var(--purple); + color: var(--text-light); + border: none; + border-radius: 8px; + font-family: 'Manrope', sans-serif; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s ease; + + &:hover { + background-color: #2A1B5E; + } +} \ No newline at end of file diff --git a/app/(pages)/(app)/(orders)/_components/OrderCard.tsx b/app/(pages)/(app)/(orders)/_components/OrderCard.tsx new file mode 100644 index 0000000..206de4b --- /dev/null +++ b/app/(pages)/(app)/(orders)/_components/OrderCard.tsx @@ -0,0 +1,77 @@ +'use client'; + +import Image from 'next/image'; +import styles from './OrderCard.module.scss'; +import { OrderStatus } from '@datatypes/Order'; + +interface OrderCardProps { + id: number; + created_at: string; + status: OrderStatus; + image: string; +} + +export default function OrderCard({ + id, + created_at, + status, + image, +}: OrderCardProps) { + const statusLabels = { + [OrderStatus.ORDERED]: 'Ordered', + [OrderStatus.SHIPPED]: 'Shipped', + [OrderStatus.IN_TRANSIT]: 'In Transit', + [OrderStatus.DELIVERED]: 'Delivered', + [OrderStatus.PENDING]: 'Pending', + [OrderStatus.CANCELLED]: 'Cancelled', + [OrderStatus.REFUNDED]: 'Refunded', + }; + + const statusValues = Object.values(OrderStatus); + const currentStatusIndex = statusValues.indexOf(status); + + return ( +
+
+
+ Product +
+ +
+
+

Order #{id}

+

+ Order Placed:{' '} + {new Date(Number(created_at)).toLocaleDateString(undefined, { + year: 'numeric', + month: 'long', + day: 'numeric', + })} +

+
+ +
+
+ {statusValues.map((value, index) => ( +
+
+ + {statusLabels[value]} + +
+ ))} +
+
+
+
+ + +
+ ); +} diff --git a/app/(pages)/(app)/orders/order-details/_components/OrderPart.module.scss b/app/(pages)/(app)/(orders)/order-details/_components/OrderPart.module.scss similarity index 100% rename from app/(pages)/(app)/orders/order-details/_components/OrderPart.module.scss rename to app/(pages)/(app)/(orders)/order-details/_components/OrderPart.module.scss diff --git a/app/(pages)/(app)/(orders)/order-details/_components/OrderPart.tsx b/app/(pages)/(app)/(orders)/order-details/_components/OrderPart.tsx new file mode 100644 index 0000000..e0f3f84 --- /dev/null +++ b/app/(pages)/(app)/(orders)/order-details/_components/OrderPart.tsx @@ -0,0 +1,31 @@ +'use client'; +import Image from 'next/image'; +import styles from './OrderPart.module.scss'; +import material from './material.jpg'; +export default function OrderPart() { + return ( +
+
+ material image +
+

Material Name

+

Category

+
+
+
+
+

1

+
+
+

X

+
+
+

$15.00

+
+
+
+

$15.00

+
+
+ ); +} diff --git a/app/(pages)/(app)/orders/order-details/_components/material.jpg b/app/(pages)/(app)/(orders)/order-details/_components/material.jpg similarity index 100% rename from app/(pages)/(app)/orders/order-details/_components/material.jpg rename to app/(pages)/(app)/(orders)/order-details/_components/material.jpg diff --git a/app/(pages)/(app)/orders/order-details/page.module.scss b/app/(pages)/(app)/(orders)/order-details/page.module.scss similarity index 100% rename from app/(pages)/(app)/orders/order-details/page.module.scss rename to app/(pages)/(app)/(orders)/order-details/page.module.scss diff --git a/app/(pages)/(app)/(orders)/order-details/page.tsx b/app/(pages)/(app)/(orders)/order-details/page.tsx new file mode 100644 index 0000000..57fdbd3 --- /dev/null +++ b/app/(pages)/(app)/(orders)/order-details/page.tsx @@ -0,0 +1,57 @@ +'use client'; +import OrderPart from './_components/OrderPart'; +import React, { useState } from 'react'; +import styles from './page.module.scss'; + +export default function OrderDetails() { + const [isOpen, setIsOpen] = useState(false); + const toggleDropdown = () => { + setIsOpen((prevState) => !prevState); + }; + return ( +
+
+
+
+ +
+
+
+
+

Order #48219

+ +
+
+ + {isOpen && ( + + )} +
+
+
+
+ + + +
+
+
+
+
+
+
+
+ ); +} diff --git a/app/(pages)/(app)/(orders)/page.module.scss b/app/(pages)/(app)/(orders)/page.module.scss new file mode 100644 index 0000000..944749d --- /dev/null +++ b/app/(pages)/(app)/(orders)/page.module.scss @@ -0,0 +1,204 @@ +@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700&display=swap'); + +.page_container { + display: flex; + flex-direction: column; + background-color: whitesmoke; + padding: 32px; + gap: 32px; + min-height: 100vh; + width: 100%; +} + +.header { + display: flex; + flex-direction: column; + gap: 24px; + width: 100%; +} + +.title { + font-family: 'Manrope', sans-serif; + font-size: 48px; + font-weight: 700; + color: var(--text-dark); + margin: 0; +} + +.searchbar { + display: flex; + align-items: center; + gap: 16px; + max-width: 600px; + width: 100%; + + .search { + flex: 1; + padding: 12px 20px; + font-family: 'Manrope', sans-serif; + font-size: 16px; + border: 1px solid var(--gray-300); + border-radius: 50px; + outline: none; + transition: border-color 0.2s ease; + + &:focus { + border-color: var(--purple); + } + + &::placeholder { + color: var(--gray-500); + } + } + + .search_submit { + padding: 12px 24px; + font-family: 'Manrope', sans-serif; + font-size: 16px; + font-weight: 600; + border: none; + border-radius: 10px; + background-color: var(--purple); + color: var(--text-light); + cursor: pointer; + transition: background-color 0.2s ease; + + &:hover { + background-color: #2A1B5E; + } + } +} + +.progress_section { + display: flex; + flex-direction: column; + gap: 24px; + width: 100%; +} + +.section_header { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + + h2 { + font-family: 'Manrope', sans-serif; + font-size: 24px; + font-weight: 600; + color: var(--text-dark); + margin: 0; + } +} + +.pagination { + display: flex; + align-items: center; + gap: 16px; +} + +.pagination_btn { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--gray-300); + border-radius: 6px; + background-color: var(--background-secondary); + color: var(--gray-600); + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background-color: var(--background-primary); + border-color: var(--gray-400); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.pagination_text { + font-family: 'Manrope', sans-serif; + font-size: 14px; + font-weight: 500; + color: var(--gray-600); +} + +.status_filters { + display: flex; + gap: 8px; + flex-wrap: wrap; + width: 100%; +} + +.filter_btn { + padding: 12px 20px; + font-family: 'Manrope', sans-serif; + font-size: 14px; + font-weight: 500; + border: 1px solid var(--gray-300); + border-radius: 10px; + background-color: var(--background-secondary); + color: var(--gray-700); + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0px 4px 20px rgba(51, 51, 51, 0.06); + + &:hover { + background-color: var(--background-primary); + border-color: var(--gray-400); + } + + &.selected { + background-color: var(--purple); + color: var(--text-light); + border-color: var(--purple); + } + + // Individual button widths to match design + &.all-btn { + min-width: 81px; + } + + &.ordered { + min-width: 130px; + } + + &.packed { + min-width: 122px; + } + + &.shipped { + min-width: 130px; + } + + &.transit { + min-width: 138px; + } +} + +.order_cards { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; +} + +.past_orders_section { + display: flex; + flex-direction: column; + gap: 24px; + width: 100%; + + h2 { + font-family: 'Manrope', sans-serif; + font-size: 24px; + font-weight: 600; + color: var(--text-dark); + margin: 0; + } +} \ No newline at end of file diff --git a/app/(pages)/(app)/(orders)/page.tsx b/app/(pages)/(app)/(orders)/page.tsx new file mode 100644 index 0000000..c0ef6a2 --- /dev/null +++ b/app/(pages)/(app)/(orders)/page.tsx @@ -0,0 +1,187 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { gql } from 'graphql-tag'; +import OrderCard from './_components/OrderCard'; +import sendApolloRequest from '../../_utils/sendApolloRequest'; +import { Order } from '@datatypes/Order'; +import styles from './page.module.scss'; + +const query = gql` + query OrderQuery($offset: Int!, $limit: Int!) { + orders(offset: $offset, limit: $limit) { + id + customer_name + customer_email + customer_phone_num + created_at + status + total + shipping_address_line_1 + shipping_address_line_2 + shipping_city + shipping_country + shipping_zip + billing_address_line_1 + billing_address_line_2 + billing_city + billing_country + billing_zip + products { + product { + id + name + price + discount + } + quantity + } + } + } +`; + +// Example IDs +const variables = { + offset: 0, + limit: 5, +}; + +export default function ViewOrderCards() { + const [orders, setOrders] = useState([]); + const [selectedStatus, setSelectedStatus] = useState('All'); + + const statusClassMap: { [key: string]: string } = { + All: 'all-btn', + Ordered: 'ordered', + Packed: 'packed', + Shipped: 'shipped', + 'In Transit': 'transit', + }; + + const statusOptions = ['All', 'Ordered', 'Packed', 'Shipped', 'In Transit']; + + // Updated order data to better match the design + // const orderData = [ + // { + // orderId: '#48219', + // shippingInfo: 'Pack by April 30', + // orderDate: 'April 26, 2025', + // status: 1, // Packed + // image: '/sample-product/puffer.png', + // }, + // { + // orderId: '#73056', + // shippingInfo: 'Ship by April 30', + // orderDate: 'April 22, 2025', + // status: 2, // Shipped + // image: '/sample-product/watch.png', + // }, + // { + // orderId: '#19547', + // shippingInfo: 'Ship by April 30', + // orderDate: 'April 18, 2025', + // status: 3, // In Transit + // image: '/sample-product/sneaker.png', + // }, + // ]; + + // const pastOrders = [ + // { + // orderId: '#15432', + // shippingInfo: 'Delivered', + // orderDate: 'April 15, 2025', + // status: 4, // Delivered + // image: '/sample-product/cap.png', + // }, + // { + // orderId: '#12853', + // shippingInfo: 'Delivered', + // orderDate: 'April 12, 2025', + // status: 4, // Delivered + // image: '/sample-product/bear.png', + // }, + // ]; + + useEffect(() => { + const fetchOrders = async () => { + const orderData = await sendApolloRequest({ request: query, variables }); + setOrders(orderData.data.orders); + }; + + fetchOrders(); + }, []); + + return ( +
+
+

Orders

+ +
+ + +
+
+ +
+
+

In Progress

+
+ + 1 of 5 + +
+
+ +
+ {statusOptions.map((status) => ( + + ))} +
+ +
+ {orders.map((order, index) => ( + + ))} +
+
+ + {/*
+

Past Orders

+
+ {pastOrders.map((order, index) => ( + + ))} +
+
*/} +
+ ); +} diff --git a/app/(pages)/(app)/orders/view-order/_components/ContactInfo/ContactInfo.module.scss b/app/(pages)/(app)/(orders)/view-order/_components/ContactInfo/ContactInfo.module.scss similarity index 100% rename from app/(pages)/(app)/orders/view-order/_components/ContactInfo/ContactInfo.module.scss rename to app/(pages)/(app)/(orders)/view-order/_components/ContactInfo/ContactInfo.module.scss diff --git a/app/(pages)/(app)/orders/view-order/_components/ContactInfo/ContactInfo.tsx b/app/(pages)/(app)/(orders)/view-order/_components/ContactInfo/ContactInfo.tsx similarity index 100% rename from app/(pages)/(app)/orders/view-order/_components/ContactInfo/ContactInfo.tsx rename to app/(pages)/(app)/(orders)/view-order/_components/ContactInfo/ContactInfo.tsx diff --git a/app/(pages)/(app)/orders/view-order/_components/CreditCardPayment/CreditCardPayment.module.scss b/app/(pages)/(app)/(orders)/view-order/_components/CreditCardPayment/CreditCardPayment.module.scss similarity index 100% rename from app/(pages)/(app)/orders/view-order/_components/CreditCardPayment/CreditCardPayment.module.scss rename to app/(pages)/(app)/(orders)/view-order/_components/CreditCardPayment/CreditCardPayment.module.scss diff --git a/app/(pages)/(app)/orders/view-order/_components/CreditCardPayment/CreditCardPayment.tsx b/app/(pages)/(app)/(orders)/view-order/_components/CreditCardPayment/CreditCardPayment.tsx similarity index 100% rename from app/(pages)/(app)/orders/view-order/_components/CreditCardPayment/CreditCardPayment.tsx rename to app/(pages)/(app)/(orders)/view-order/_components/CreditCardPayment/CreditCardPayment.tsx diff --git a/app/(pages)/(app)/orders/view-order/_components/Navigator/Navigator.module.scss b/app/(pages)/(app)/(orders)/view-order/_components/Navigator/Navigator.module.scss similarity index 100% rename from app/(pages)/(app)/orders/view-order/_components/Navigator/Navigator.module.scss rename to app/(pages)/(app)/(orders)/view-order/_components/Navigator/Navigator.module.scss diff --git a/app/(pages)/(app)/orders/view-order/_components/Navigator/Navigator.tsx b/app/(pages)/(app)/(orders)/view-order/_components/Navigator/Navigator.tsx similarity index 100% rename from app/(pages)/(app)/orders/view-order/_components/Navigator/Navigator.tsx rename to app/(pages)/(app)/(orders)/view-order/_components/Navigator/Navigator.tsx diff --git a/app/(pages)/(app)/orders/view-order/_components/OrderDescription/OrderDescription.module.scss b/app/(pages)/(app)/(orders)/view-order/_components/OrderDescription/OrderDescription.module.scss similarity index 100% rename from app/(pages)/(app)/orders/view-order/_components/OrderDescription/OrderDescription.module.scss rename to app/(pages)/(app)/(orders)/view-order/_components/OrderDescription/OrderDescription.module.scss diff --git a/app/(pages)/(app)/orders/view-order/_components/OrderDescription/OrderDescription.tsx b/app/(pages)/(app)/(orders)/view-order/_components/OrderDescription/OrderDescription.tsx similarity index 100% rename from app/(pages)/(app)/orders/view-order/_components/OrderDescription/OrderDescription.tsx rename to app/(pages)/(app)/(orders)/view-order/_components/OrderDescription/OrderDescription.tsx diff --git a/app/(pages)/(app)/orders/view-order/_components/ProgessBar/ProgressBar.module.scss b/app/(pages)/(app)/(orders)/view-order/_components/ProgessBar/ProgressBar.module.scss similarity index 100% rename from app/(pages)/(app)/orders/view-order/_components/ProgessBar/ProgressBar.module.scss rename to app/(pages)/(app)/(orders)/view-order/_components/ProgessBar/ProgressBar.module.scss diff --git a/app/(pages)/(app)/orders/view-order/_components/ProgessBar/ProgressBar.tsx b/app/(pages)/(app)/(orders)/view-order/_components/ProgessBar/ProgressBar.tsx similarity index 100% rename from app/(pages)/(app)/orders/view-order/_components/ProgessBar/ProgressBar.tsx rename to app/(pages)/(app)/(orders)/view-order/_components/ProgessBar/ProgressBar.tsx diff --git a/app/(pages)/(app)/orders/view-order/_components/ShippingLabel/ShippingLabel.module.scss b/app/(pages)/(app)/(orders)/view-order/_components/ShippingLabel/ShippingLabel.module.scss similarity index 100% rename from app/(pages)/(app)/orders/view-order/_components/ShippingLabel/ShippingLabel.module.scss rename to app/(pages)/(app)/(orders)/view-order/_components/ShippingLabel/ShippingLabel.module.scss diff --git a/app/(pages)/(app)/orders/view-order/_components/ShippingLabel/ShippingLabel.tsx b/app/(pages)/(app)/(orders)/view-order/_components/ShippingLabel/ShippingLabel.tsx similarity index 100% rename from app/(pages)/(app)/orders/view-order/_components/ShippingLabel/ShippingLabel.tsx rename to app/(pages)/(app)/(orders)/view-order/_components/ShippingLabel/ShippingLabel.tsx diff --git a/app/(pages)/(app)/orders/view-order/page.module.scss b/app/(pages)/(app)/(orders)/view-order/page.module.scss similarity index 100% rename from app/(pages)/(app)/orders/view-order/page.module.scss rename to app/(pages)/(app)/(orders)/view-order/page.module.scss diff --git a/app/(pages)/(app)/orders/view-order/page.tsx b/app/(pages)/(app)/(orders)/view-order/page.tsx similarity index 100% rename from app/(pages)/(app)/orders/view-order/page.tsx rename to app/(pages)/(app)/(orders)/view-order/page.tsx diff --git a/app/(pages)/(app)/orders/order-cards/_components/OrderCard.module.scss b/app/(pages)/(app)/orders/order-cards/_components/OrderCard.module.scss deleted file mode 100644 index 9de6820..0000000 --- a/app/(pages)/(app)/orders/order-cards/_components/OrderCard.module.scss +++ /dev/null @@ -1,102 +0,0 @@ -@use 'media'; -@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700&display=swap'); - -.container { - width: 100%; - height: 260px; - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - padding: var(--small-spacer) var(--medium-spacer); - background-color: var(--secondary); - background-color: var(--background-secondary); - color: var(--text-light); - box-shadow: 0px 4px 20px 0px #3333330F; - border-radius: 10px; - - @include media.tablet { - flex-direction: column; - gap: var(--small-spacer); - } -} -.imginfo{ - width: 70%; - display: flex; - align-items: center; - -} -.item_image { - margin-right: 40px; - width: 185px; - height: 140px; - position: relative; - border-style: solid; - border-width: 1px; - border-color: var(--gray-300); -} - -.item_info { - display: flex; - flex-direction: column; - gap: var(--tiny-spacer); - - p { - font-family: 'Manrope', sans-serif; - font-size: 1.2rem; - font-weight: 400; - } -} - -.item_status { - width: 90%; - padding: var(--tiny-spacer); - display: flex; - justify-content: space-between; - position: relative; - - .status_bar { - height: 2px; - background-color: var(--gray-500); - position: absolute; - width: 80%; - bottom: 65%; - left: 10%; - right: 10%; - } - - .status_options { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--tiny-spacer); - position: relative; - - .status_circle { - width: 32px; - height: 32px; - background-color: var(--background-secondary); - border-style: solid; - border-width: 1px; - border-color: var(--gray-500); - border-radius: 50%; - } - - p { - color: var(--gray-700); - } - } -} - -.info_button { - width: 172px; - height: 64px; - cursor: pointer; - background-color: #3A2779; - border-style: none; - border-radius: 10px; - -} -.text{ - color: white; -} \ No newline at end of file diff --git a/app/(pages)/(app)/orders/order-cards/_components/OrderCard.tsx b/app/(pages)/(app)/orders/order-cards/_components/OrderCard.tsx deleted file mode 100644 index e62b866..0000000 --- a/app/(pages)/(app)/orders/order-cards/_components/OrderCard.tsx +++ /dev/null @@ -1,79 +0,0 @@ -'use client'; -import styles from './OrderCard.module.scss'; -import Image from 'next/image'; -import arrowRight from '/public/icons/arrow_right_line.svg'; -import purpleCheck from '/public/icons/purple_check.svg'; -import issue from '/public/icons/yellow_error.svg'; -import error from '/public/icons/red_error.svg'; -import greenCheck from '/public/icons/green_check.svg'; - -interface TextboxProps { - title: string; - date: Date; - status: number; - image: string; - icon: number; -} - -const OrderCard: React.FC = (props) => { - const itemStatuses = [ - 'Ordered', - 'Packed', - 'Shipped', - 'In Transit', - 'Delivered', - ]; - - function displayIcon(status: number, icon: number, index: number) { - if (index < status) { - return purpleCheck; - } else if (index === status) { - if (icon === 0) { - return purpleCheck; - } else if (icon === 1) { - return issue; - } else if (icon === 2) { - return error; - } else { - return greenCheck; - } - } else { - return ''; - } - } - return ( -
-
-
- product image -
-
-

{props.title}

-

Order Placed: {props.date.toDateString()}

-
-
- {itemStatuses.map((status, index) => ( -
- {props.status <= 4 && - displayIcon(props.status, props.icon, index) !== '' ? ( - check icon - ) : ( -
- )} -

{status}

-
- ))} -
-
-
- -
- ); -}; - -export default OrderCard; diff --git a/app/(pages)/(app)/orders/order-cards/page.module.scss b/app/(pages)/(app)/orders/order-cards/page.module.scss deleted file mode 100644 index da65d1b..0000000 --- a/app/(pages)/(app)/orders/order-cards/page.module.scss +++ /dev/null @@ -1,114 +0,0 @@ -@use 'media'; - -.page_container { - display: flex; - flex-direction: column; - background-color: #F6F6F6; - padding: var(--small-spacer) var(--medium-spacer); - gap: var(--small-spacer); - margin-left: 300px; - h4 { - font-weight: 600; - padding: var(--small-spacer) 0; - } - -} -.text { - font-size: 48px; -} - -.order_form { - width: 60%; - display: grid; - grid-template-columns: 70% 30%; -} - -.order_cards { - display: flex; - flex-direction: column; - gap: var(--small-spacer); -} - -.searchbar { - display: flex; - justify-self: start; - width: 100%; - align-self: center; - - .search { - display: inline-block; - margin-right:16px; - padding: 10px 20px 10px 20px; - font-size: 1rem; - border: 1px solid var(--gray-500); - border-radius: 50px; - width: 100%; - outline: none; - } - - .search_submit { - height: 48px; - width: 120px; - display: inline-block; - padding: 11px 30px 11px 30px; - font-size: 1rem; - border: 1px solid var(--gray-500); - border-left: none; - border-radius: 10px; - background-color: var(--purple); - color: var(--text-light); - cursor: pointer; - } -} - -.filter { - justify-self: end; - align-self: center; - width: 80%; - - select { - padding: var(--tiny-spacer); - font-size: 1rem; - width: 100%; - border-radius: 5px; - border-color: var(--gray-500); - outline: none; - } -} -.status { - width: 100%; - display:flex; - margin-bottom: 24px; -} -.all { - color: black; - background-color: white; - box-shadow: 0px 4px 20px 0px #3333330F; - width: 81px; - height: 45px; - border-radius: 10px; - padding: 10px 30px 10px 30px; - border: none; - margin-right: 8px; - - &.ordered { - width: 130px; - height: 45px; - } - &.packed { - width: 122px; - height: 45px; - } - &.shipped { - width: 130px; - height: 45px; - } - &.transit { - width: 138px; - height: 45px; - } -} -.clicked{ - color: white; - background-color: #3A2779; -} \ No newline at end of file diff --git a/app/(pages)/(app)/orders/order-cards/page.tsx b/app/(pages)/(app)/orders/order-cards/page.tsx deleted file mode 100644 index b16a4e0..0000000 --- a/app/(pages)/(app)/orders/order-cards/page.tsx +++ /dev/null @@ -1,141 +0,0 @@ -'use client'; -import React, { useState } from 'react'; -import OrderCard from './_components/OrderCard'; -import styles from './page.module.scss'; - -export default function ViewOrderCards() { - const [selectedStatus, setSelectedStatus] = useState('All'); - - const statusClassMap = { - All: 'all-btn', - Ordered: 'ordered', - Packed: 'packed', - Shipped: 'shipped', - 'In Transit': 'transit', - }; - - const dateTime = new Date('2024-03-01T10:36:01.516Z'); - const progressList = [ - { - name: 'Lightweight Water-Resistant Hooded Puffer Coat', - date: dateTime, - status: 3, - image: '/sample-product/puffer.png', - icon: 0, - }, - { - name: 'Lightweight Water-Resistant Hooded Puffer Coat', - date: dateTime, - status: 1, - image: '/sample-product/watch.png', - icon: 2, - }, - { - name: 'Lightweight Water-Resistant Hooded Puffer Coat', - date: dateTime, - status: 1, - image: '/sample-product/sneaker.png', - icon: 1, - }, - { - name: 'Lightweight Water-Resistant Hooded Puffer Coat', - date: dateTime, - status: 1, - image: '/sample-product/plant.png', - icon: 0, - }, - ]; - const deliveredList = [ - { - name: 'Lightweight Water-Resistant Hooded Puffer Coat', - date: dateTime, - status: 4, - image: '/sample-product/cap.png', - icon: 3, - }, - { - name: 'Lightweight Water-Resistant Hooded Puffer Coat', - date: dateTime, - status: 4, - image: '/sample-product/bear.png', - icon: 3, - }, - { - name: 'Lightweight Water-Resistant Hooded Puffer Coat', - date: dateTime, - status: 4, - image: '/sample-product/hydro.png', - icon: 3, - }, - ]; - return ( - -
-
-

Orders

- -
- - -
- - -
-
-

In Progress

-
- {['All', 'Ordered', 'Packed', 'Shipped', 'In Transit'].map((status) => ( - - ))} -
-
- {progressList.map((item, index) => ( -
- -
- ))} -
-
-
-

Past Orders

-
- {deliveredList.map((item, index) => ( -
- -
- ))} -
-
-
- ); -} diff --git a/app/(pages)/(app)/orders/order-details/_components/OrderPart.tsx b/app/(pages)/(app)/orders/order-details/_components/OrderPart.tsx deleted file mode 100644 index 57f353f..0000000 --- a/app/(pages)/(app)/orders/order-details/_components/OrderPart.tsx +++ /dev/null @@ -1,31 +0,0 @@ -'use client'; -import Image from 'next/image'; -import styles from './OrderPart.module.scss'; -import material from './material.jpg' -export default function OrderPart() { - return ( -
-
- material image -
-

Material Name

-

Category

-
-
-
-
-

1

-
-
-

X

-
-
-

$15.00

-
-
-
-

$15.00

-
-
- ); - } \ No newline at end of file diff --git a/app/(pages)/(app)/orders/order-details/page.tsx b/app/(pages)/(app)/orders/order-details/page.tsx deleted file mode 100644 index 420dda7..0000000 --- a/app/(pages)/(app)/orders/order-details/page.tsx +++ /dev/null @@ -1,59 +0,0 @@ -'use client'; -import OrderPart from './_components/OrderPart'; -import React, { useState } from 'react'; -import styles from './page.module.scss'; - -export default function OrderDetails() { - const [isOpen, setIsOpen] = useState(false); - const toggleDropdown = () => { - setIsOpen((prevState) => !prevState); - }; - return ( -
-
-
-
- - - -
- -
-
-
-

Order #48219

- -
-
- - {isOpen && ( - - )} -
-
-
-
- - - -
-
- -
-
-
- -
-
-
-
- -
- ); -} \ No newline at end of file diff --git a/app/(pages)/_components/Sidebar/Sidebar.module.scss b/app/(pages)/_components/Sidebar/Sidebar.module.scss index de85dc4..c92d9dd 100644 --- a/app/(pages)/_components/Sidebar/Sidebar.module.scss +++ b/app/(pages)/_components/Sidebar/Sidebar.module.scss @@ -6,7 +6,7 @@ left: 0; height: 100vh; width: fit-content; - background-color: whitesmoke; + background-color: white; .header { display: flex; diff --git a/app/(pages)/_components/Sidebar/Sidebar.tsx b/app/(pages)/_components/Sidebar/Sidebar.tsx index 1fb9596..c99ead7 100644 --- a/app/(pages)/_components/Sidebar/Sidebar.tsx +++ b/app/(pages)/_components/Sidebar/Sidebar.tsx @@ -1,12 +1,13 @@ 'use client'; import { useState } from 'react'; +import { usePathname } from 'next/navigation'; import Link from 'next/link'; -import styles from './Sidebar.module.scss'; -import { CgProfile } from 'react-icons/cg'; -import { IoPricetagOutline } from 'react-icons/io5'; -import { FiShoppingCart } from 'react-icons/fi'; +import { FiEdit, FiShoppingCart } from 'react-icons/fi'; +import { RiFileList3Line } from 'react-icons/ri'; + +import styles from './Sidebar.module.scss'; interface NavLink { name: string; @@ -16,24 +17,25 @@ interface NavLink { const navLinks: NavLink[] = [ { - name: 'Profile', - slug: '/profile/account-settings', - icon: , + name: 'Orders', + slug: '/', + icon: , }, { - name: 'Product', + name: 'Product Listings', slug: '/products', - icon: , + icon: , }, { - name: 'Order', - slug: '/orders', - icon: , + name: 'Edit Store', + slug: '/profile/account-settings', + icon: , }, ]; export default function Sidebar() { - const [selectedSlug, setSelectedSlug] = useState(null); + const pathname = usePathname(); + const [selectedSlug, setSelectedSlug] = useState(pathname); const handleSelect = (slug: string) => { setSelectedSlug(slug); diff --git a/prisma/migrations/20250710064028_order_status/migration.sql b/prisma/migrations/20250710064028_order_status/migration.sql new file mode 100644 index 0000000..d209377 --- /dev/null +++ b/prisma/migrations/20250710064028_order_status/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Changed the type of `status` on the `Order` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required. + +*/ +-- CreateEnum +CREATE TYPE "OrderStatus" AS ENUM ('PENDING', 'ORDERED', 'SHIPPED', 'IN_TRANSIT', 'DELIVERED', 'CANCELLED', 'REFUNDED'); + +-- AlterTable +ALTER TABLE "Order" DROP COLUMN "status", +ADD COLUMN "status" "OrderStatus" NOT NULL; diff --git a/prisma/schema/Order.prisma b/prisma/schema/Order.prisma index d599fd2..c256c5b 100644 --- a/prisma/schema/Order.prisma +++ b/prisma/schema/Order.prisma @@ -15,7 +15,17 @@ model Order { shipping_city String shipping_zip String shipping_country String - status String + status OrderStatus created_at DateTime products ProductToOrder[] } + +enum OrderStatus { + PENDING + ORDERED + SHIPPED + IN_TRANSIT + DELIVERED + CANCELLED + REFUNDED +} From 2d0ce0f5bb868cebc60dc1466a4ad7ebe523f997 Mon Sep 17 00:00:00 2001 From: brandonw504 Date: Thu, 10 Jul 2025 13:04:55 -0700 Subject: [PATCH 4/5] Order filtering and cancellation statuses --- app/(api)/_datalib/_resolvers/Order.ts | 13 +- app/(api)/_datalib/_services/Orders.ts | 40 +++- app/(api)/_datalib/_typeDefs/Order.ts | 7 + app/(api)/_types/Order.ts | 5 + .../_components/OrderCard.module.scss | 23 ++ .../(app)/(orders)/_components/OrderCard.tsx | 43 +++- .../OrderSection/OrderSection.module.scss | 97 ++++++++ .../_components/OrderSection/OrderSection.tsx | 67 ++++++ app/(pages)/(app)/(orders)/page.module.scss | 37 --- app/(pages)/(app)/(orders)/page.tsx | 224 +++++++++--------- app/(pages)/_globals/styles/colors.scss | 5 + .../migration.sql | 20 ++ prisma/schema/Order.prisma | 6 +- 13 files changed, 418 insertions(+), 169 deletions(-) create mode 100644 app/(pages)/(app)/(orders)/_components/OrderSection/OrderSection.module.scss create mode 100644 app/(pages)/(app)/(orders)/_components/OrderSection/OrderSection.tsx create mode 100644 prisma/migrations/20250710200308_order_cancellation_status/migration.sql diff --git a/app/(api)/_datalib/_resolvers/Order.ts b/app/(api)/_datalib/_resolvers/Order.ts index e218b95..7de4f41 100644 --- a/app/(api)/_datalib/_resolvers/Order.ts +++ b/app/(api)/_datalib/_resolvers/Order.ts @@ -1,9 +1,10 @@ import Orders from '../_services/Orders'; import { - OrderInput, Order, + OrderInput, OrderProductInput, OrderStatus, + CancellationStatus, } from '@datatypes/Order'; import { ApolloContext } from '../apolloServer'; @@ -19,13 +20,21 @@ const resolvers = { _: never, args: { statuses: OrderStatus[]; + cancellation_statuses: CancellationStatus[]; search: string; offset: number; limit: number; }, ctx: ApolloContext ) => - Orders.findMany(args.statuses, args.search, args.offset, args.limit, ctx), + Orders.findMany( + args.statuses, + args.cancellation_statuses, + args.search, + args.offset, + args.limit, + ctx + ), }, Mutation: { updateOrder: ( diff --git a/app/(api)/_datalib/_services/Orders.ts b/app/(api)/_datalib/_services/Orders.ts index d61881c..24aaa54 100644 --- a/app/(api)/_datalib/_services/Orders.ts +++ b/app/(api)/_datalib/_services/Orders.ts @@ -1,6 +1,7 @@ import revalidateCache from '@actions/revalidateCache'; import prisma from '../_prisma/client'; import { + CancellationStatus, OrderInput, OrderProductInput, OrderStatus, @@ -40,19 +41,40 @@ export default class Orders { static async findMany( statuses: OrderStatus[], + cancellation_statuses: CancellationStatus[], search: string, offset: number, limit: number, ctx: ApolloContext ) { if (!ctx.isOwner && !ctx.hasValidApiKey) return null; - if (offset < 0 || limit <= 0) return null; - const whereClause: Prisma.OrderWhereInput = {}; + const queryConditions: Prisma.OrderWhereInput[] = []; + + const statusFilters: Prisma.OrderWhereInput[] = []; + if (statuses?.length) { + statusFilters.push({ status: { in: statuses } }); + } + if (cancellation_statuses?.length) { + statusFilters.push({ + cancellation_status: { in: cancellation_statuses }, + }); + } + + if (statusFilters.length > 1) { + queryConditions.push({ OR: statusFilters }); + } else if (statusFilters.length === 1) { + queryConditions.push(statusFilters[0]); + } - if (statuses && statuses.length > 0) { - whereClause.status = { in: statuses }; + // For in progress requests, ensure there's no cancellation status. + const isInProgressRequest = + statuses?.length && + !cancellation_statuses?.length && + !statuses.includes(OrderStatus.DELIVERED); + if (isInProgressRequest) { + queryConditions.push({ cancellation_status: null }); } if (search) { @@ -80,11 +102,11 @@ export default class Orders { }); } - whereClause.OR = searchConditions; + queryConditions.push({ OR: searchConditions }); } return prisma.order.findMany({ - where: whereClause, + where: { AND: queryConditions }, orderBy: { created_at: 'desc', }, @@ -288,12 +310,12 @@ export default class Orders { id, }, select: { - status: true, + cancellation_status: true, }, }); if (order) { - if (order.status == 'REFUNDED') { + if (order.cancellation_status == 'REFUNDED') { await prisma.order.delete({ where: { id, @@ -307,7 +329,7 @@ export default class Orders { id, }, data: { - status: 'CANCELLED', + cancellation_status: CancellationStatus.CANCELLED, }, }); // could possibly add a message? "this order need to be refunded" or smth diff --git a/app/(api)/_datalib/_typeDefs/Order.ts b/app/(api)/_datalib/_typeDefs/Order.ts index e3f1dd8..1673b5e 100644 --- a/app/(api)/_datalib/_typeDefs/Order.ts +++ b/app/(api)/_datalib/_typeDefs/Order.ts @@ -7,6 +7,9 @@ const typeDefs = gql` SHIPPED IN_TRANSIT DELIVERED + } + + enum CancellationStatus { CANCELLED REFUNDED } @@ -30,6 +33,7 @@ const typeDefs = gql` shipping_zip: String! shipping_country: String! status: OrderStatus! + cancellation_status: CancellationStatus created_at: String! } @@ -48,6 +52,7 @@ const typeDefs = gql` shipping_zip: String! shipping_country: String! status: OrderStatus + cancellation_status: CancellationStatus } input OrderUpdateInput { @@ -66,6 +71,7 @@ const typeDefs = gql` shipping_zip: String shipping_country: String status: OrderStatus + cancellation_status: CancellationStatus } input OrderProductInput { @@ -87,6 +93,7 @@ const typeDefs = gql` order(id: ID!): Order orders( statuses: [OrderStatus] + cancellation_statuses: [CancellationStatus] search: String offset: Int! limit: Int! diff --git a/app/(api)/_types/Order.ts b/app/(api)/_types/Order.ts index b5dfadf..b0bcb04 100644 --- a/app/(api)/_types/Order.ts +++ b/app/(api)/_types/Order.ts @@ -6,6 +6,9 @@ export enum OrderStatus { SHIPPED = 'SHIPPED', IN_TRANSIT = 'IN_TRANSIT', DELIVERED = 'DELIVERED', +} + +export enum CancellationStatus { CANCELLED = 'CANCELLED', REFUNDED = 'REFUNDED', } @@ -27,6 +30,7 @@ export type Order = { shipping_zip: string; shipping_country: string; status: OrderStatus; + cancellation_status: CancellationStatus | null; created_at: string; products: ProductToOrder[]; }; @@ -64,6 +68,7 @@ export type OrderInput = { shipping_zip: string; shipping_country: string; status?: OrderStatus; + cancellation_status?: CancellationStatus; }; //make all inputs optional diff --git a/app/(pages)/(app)/(orders)/_components/OrderCard.module.scss b/app/(pages)/(app)/(orders)/_components/OrderCard.module.scss index 0566d48..dd8be4e 100644 --- a/app/(pages)/(app)/(orders)/_components/OrderCard.module.scss +++ b/app/(pages)/(app)/(orders)/_components/OrderCard.module.scss @@ -164,6 +164,29 @@ white-space: nowrap; } +.order_id_container { + display: flex; + align-items: center; + gap: 1rem; +} + +.status_badge { + padding: 0.25rem 0.75rem; + border-radius: 1rem; + font-size: 0.875rem; + font-weight: 500; + + &.refunded { + background-color: var(--error-light); + color: var(--error-dark); + } + + &.cancelled { + background-color: var(--gray-200); + color: var(--gray-800); + } +} + .view_order_btn { padding: 12px 24px; background-color: var(--purple); diff --git a/app/(pages)/(app)/(orders)/_components/OrderCard.tsx b/app/(pages)/(app)/(orders)/_components/OrderCard.tsx index 206de4b..378d97e 100644 --- a/app/(pages)/(app)/(orders)/_components/OrderCard.tsx +++ b/app/(pages)/(app)/(orders)/_components/OrderCard.tsx @@ -2,12 +2,13 @@ import Image from 'next/image'; import styles from './OrderCard.module.scss'; -import { OrderStatus } from '@datatypes/Order'; +import { CancellationStatus, OrderStatus } from '@datatypes/Order'; interface OrderCardProps { id: number; created_at: string; status: OrderStatus; + cancellation_status: CancellationStatus | null; image: string; } @@ -15,20 +16,31 @@ export default function OrderCard({ id, created_at, status, + cancellation_status, image, }: OrderCardProps) { - const statusLabels = { + const progressBarStatuses = [ + OrderStatus.PENDING, + OrderStatus.ORDERED, + OrderStatus.SHIPPED, + OrderStatus.IN_TRANSIT, + OrderStatus.DELIVERED, + ]; + + const progressBarLabels: { [key: string]: string } = { + [OrderStatus.PENDING]: 'Pending', [OrderStatus.ORDERED]: 'Ordered', [OrderStatus.SHIPPED]: 'Shipped', [OrderStatus.IN_TRANSIT]: 'In Transit', [OrderStatus.DELIVERED]: 'Delivered', - [OrderStatus.PENDING]: 'Pending', - [OrderStatus.CANCELLED]: 'Cancelled', - [OrderStatus.REFUNDED]: 'Refunded', }; - const statusValues = Object.values(OrderStatus); - const currentStatusIndex = statusValues.indexOf(status); + const badgeStatusLabels: { [key: string]: string } = { + [CancellationStatus.CANCELLED]: 'Cancelled', + [CancellationStatus.REFUNDED]: 'Refunded', + }; + + const currentStatusIndex = progressBarStatuses.indexOf(status); return (
@@ -39,7 +51,18 @@ export default function OrderCard({
-

Order #{id}

+
+

Order #{id}

+ {cancellation_status && ( + + {badgeStatusLabels[cancellation_status]} + + )} +

Order Placed:{' '} {new Date(Number(created_at)).toLocaleDateString(undefined, { @@ -52,7 +75,7 @@ export default function OrderCard({

- {statusValues.map((value, index) => ( + {progressBarStatuses.map((value, index) => (
- {statusLabels[value]} + {progressBarLabels[value]}
))} diff --git a/app/(pages)/(app)/(orders)/_components/OrderSection/OrderSection.module.scss b/app/(pages)/(app)/(orders)/_components/OrderSection/OrderSection.module.scss new file mode 100644 index 0000000..7f8644c --- /dev/null +++ b/app/(pages)/(app)/(orders)/_components/OrderSection/OrderSection.module.scss @@ -0,0 +1,97 @@ +.container { + display: flex; + flex-direction: column; + gap: 24px; + width: 100%; +} + +.section_header { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + + h2 { + font-family: 'Manrope', sans-serif; + font-size: 24px; + font-weight: 600; + color: var(--text-dark); + margin: 0; + } +} + +.pagination { + display: flex; + align-items: center; + gap: 16px; +} + +.pagination_btn { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--gray-300); + border-radius: 6px; + background-color: var(--background-secondary); + color: var(--gray-600); + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background-color: var(--background-primary); + border-color: var(--gray-400); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.pagination_text { + font-family: 'Manrope', sans-serif; + font-size: 14px; + font-weight: 500; + color: var(--gray-600); +} + +.status_filters { + display: flex; + gap: 8px; + flex-wrap: wrap; + width: 100%; +} + +.filter_btn { + padding: 12px 20px; + font-family: 'Manrope', sans-serif; + font-size: 14px; + font-weight: 500; + border: 1px solid var(--gray-300); + border-radius: 10px; + background-color: var(--background-secondary); + color: var(--gray-700); + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0px 4px 20px rgba(51, 51, 51, 0.06); + + &:hover { + background-color: var(--background-primary); + border-color: var(--gray-400); + } + + &.selected { + background-color: var(--purple); + color: var(--text-light); + border-color: var(--purple); + } +} + +.order_cards { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; +} \ No newline at end of file diff --git a/app/(pages)/(app)/(orders)/_components/OrderSection/OrderSection.tsx b/app/(pages)/(app)/(orders)/_components/OrderSection/OrderSection.tsx new file mode 100644 index 0000000..d33ff16 --- /dev/null +++ b/app/(pages)/(app)/(orders)/_components/OrderSection/OrderSection.tsx @@ -0,0 +1,67 @@ +import { Order } from '@datatypes/Order'; +import OrderCard from '../OrderCard'; +import styles from './OrderSection.module.scss'; + +interface OrderSectionProps { + title: string; + orders: Order[]; + statusOptions: (T | 'All')[]; + statusClassMap: { [key: string]: string }; + selectedStatus: T | 'All'; + onStatusChange: (status: T | 'All') => void; +} + +export default function OrderSection({ + title, + orders, + statusOptions, + statusClassMap, + selectedStatus, + onStatusChange, +}: OrderSectionProps) { + return ( +
+
+

{title}

+
+ + 1 of 5 + +
+
+ +
+ {statusOptions.map((status) => ( + + ))} +
+ +
+ {orders.map((order) => ( + + ))} +
+
+ ); +} diff --git a/app/(pages)/(app)/(orders)/page.module.scss b/app/(pages)/(app)/(orders)/page.module.scss index 944749d..b121200 100644 --- a/app/(pages)/(app)/(orders)/page.module.scss +++ b/app/(pages)/(app)/(orders)/page.module.scss @@ -69,28 +69,6 @@ } } -.progress_section { - display: flex; - flex-direction: column; - gap: 24px; - width: 100%; -} - -.section_header { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - - h2 { - font-family: 'Manrope', sans-serif; - font-size: 24px; - font-weight: 600; - color: var(--text-dark); - margin: 0; - } -} - .pagination { display: flex; align-items: center; @@ -186,19 +164,4 @@ flex-direction: column; gap: 16px; width: 100%; -} - -.past_orders_section { - display: flex; - flex-direction: column; - gap: 24px; - width: 100%; - - h2 { - font-family: 'Manrope', sans-serif; - font-size: 24px; - font-weight: 600; - color: var(--text-dark); - margin: 0; - } } \ No newline at end of file diff --git a/app/(pages)/(app)/(orders)/page.tsx b/app/(pages)/(app)/(orders)/page.tsx index c0ef6a2..90457a7 100644 --- a/app/(pages)/(app)/(orders)/page.tsx +++ b/app/(pages)/(app)/(orders)/page.tsx @@ -2,20 +2,31 @@ import { useState, useEffect } from 'react'; import { gql } from 'graphql-tag'; -import OrderCard from './_components/OrderCard'; import sendApolloRequest from '../../_utils/sendApolloRequest'; -import { Order } from '@datatypes/Order'; +import { Order, OrderStatus, CancellationStatus } from '@datatypes/Order'; import styles from './page.module.scss'; +import OrderSection from './_components/OrderSection/OrderSection'; const query = gql` - query OrderQuery($offset: Int!, $limit: Int!) { - orders(offset: $offset, limit: $limit) { + query OrderQuery( + $offset: Int! + $limit: Int! + $statuses: [OrderStatus!] + $cancellation_statuses: [CancellationStatus!] + ) { + orders( + offset: $offset + limit: $limit + statuses: $statuses + cancellation_statuses: $cancellation_statuses + ) { id customer_name customer_email customer_phone_num created_at status + cancellation_status total shipping_address_line_1 shipping_address_line_2 @@ -40,76 +51,105 @@ const query = gql` } `; -// Example IDs const variables = { offset: 0, limit: 5, }; export default function ViewOrderCards() { - const [orders, setOrders] = useState([]); - const [selectedStatus, setSelectedStatus] = useState('All'); + const [inProgressOrders, setInProgressOrders] = useState([]); + const [completedOrders, setCompletedOrders] = useState([]); + const [selectedStatus, setSelectedStatus] = useState( + 'All' + ); + const [selectedCompletedStatus, setSelectedCompletedStatus] = useState< + CancellationStatus | 'All' | 'DELIVERED' + >('All'); const statusClassMap: { [key: string]: string } = { All: 'all-btn', - Ordered: 'ordered', - Packed: 'packed', - Shipped: 'shipped', - 'In Transit': 'transit', + [OrderStatus.PENDING]: 'pending', + [OrderStatus.ORDERED]: 'ordered', + [OrderStatus.SHIPPED]: 'shipped', + [OrderStatus.IN_TRANSIT]: 'transit', + }; + + const completedStatusClassMap: { [key: string]: string } = { + All: 'all-btn', + DELIVERED: 'delivered', + [CancellationStatus.CANCELLED]: 'cancelled', + [CancellationStatus.REFUNDED]: 'refunded', }; - const statusOptions = ['All', 'Ordered', 'Packed', 'Shipped', 'In Transit']; - - // Updated order data to better match the design - // const orderData = [ - // { - // orderId: '#48219', - // shippingInfo: 'Pack by April 30', - // orderDate: 'April 26, 2025', - // status: 1, // Packed - // image: '/sample-product/puffer.png', - // }, - // { - // orderId: '#73056', - // shippingInfo: 'Ship by April 30', - // orderDate: 'April 22, 2025', - // status: 2, // Shipped - // image: '/sample-product/watch.png', - // }, - // { - // orderId: '#19547', - // shippingInfo: 'Ship by April 30', - // orderDate: 'April 18, 2025', - // status: 3, // In Transit - // image: '/sample-product/sneaker.png', - // }, - // ]; - - // const pastOrders = [ - // { - // orderId: '#15432', - // shippingInfo: 'Delivered', - // orderDate: 'April 15, 2025', - // status: 4, // Delivered - // image: '/sample-product/cap.png', - // }, - // { - // orderId: '#12853', - // shippingInfo: 'Delivered', - // orderDate: 'April 12, 2025', - // status: 4, // Delivered - // image: '/sample-product/bear.png', - // }, - // ]; + const statusOptions: (OrderStatus | 'All')[] = [ + 'All', + OrderStatus.PENDING, + OrderStatus.ORDERED, + OrderStatus.SHIPPED, + OrderStatus.IN_TRANSIT, + ]; + + const completedStatusOptions: (CancellationStatus | 'All' | 'DELIVERED')[] = [ + 'All', + 'DELIVERED', + ...Object.values(CancellationStatus), + ]; useEffect(() => { const fetchOrders = async () => { - const orderData = await sendApolloRequest({ request: query, variables }); - setOrders(orderData.data.orders); + const inProgressStatuses = + selectedStatus === 'All' + ? [ + OrderStatus.PENDING, + OrderStatus.ORDERED, + OrderStatus.SHIPPED, + OrderStatus.IN_TRANSIT, + ] + : [selectedStatus]; + const inProgressPromise = sendApolloRequest({ + request: query, + variables: { + ...variables, + statuses: inProgressStatuses, + }, + }); + + let completed_statuses: OrderStatus[] = []; + let cancellation_statuses: CancellationStatus[] = []; + + if (selectedCompletedStatus === 'All') { + completed_statuses = [OrderStatus.DELIVERED]; + cancellation_statuses = Object.values(CancellationStatus); + } else if (selectedCompletedStatus === 'DELIVERED') { + completed_statuses = [OrderStatus.DELIVERED]; + } else { + cancellation_statuses = [selectedCompletedStatus as CancellationStatus]; + } + + const completedPromise = sendApolloRequest({ + request: query, + variables: { + ...variables, + statuses: + completed_statuses.length > 0 ? completed_statuses : undefined, + cancellation_statuses: + cancellation_statuses.length > 0 + ? cancellation_statuses + : undefined, + }, + }); + + const [inProgressData, completedData] = await Promise.all([ + inProgressPromise, + completedPromise, + ]); + + setInProgressOrders(inProgressData.data.orders); + setCompletedOrders(completedData.data.orders); }; fetchOrders(); - }, []); + }, [selectedStatus, selectedCompletedStatus]); return (
@@ -129,59 +169,23 @@ export default function ViewOrderCards() {
-
-
-

In Progress

-
- - 1 of 5 - -
-
- -
- {statusOptions.map((status) => ( - - ))} -
- -
- {orders.map((order, index) => ( - - ))} -
-
- - {/*
-

Past Orders

-
- {pastOrders.map((order, index) => ( - - ))} -
-
*/} + + +
); } diff --git a/app/(pages)/_globals/styles/colors.scss b/app/(pages)/_globals/styles/colors.scss index 3f5c1e3..72a03f1 100644 --- a/app/(pages)/_globals/styles/colors.scss +++ b/app/(pages)/_globals/styles/colors.scss @@ -5,6 +5,9 @@ --blue: #0D6EFD; --light-purple: #DBADED; + --error-light: #FFEDED; + --error-dark: #DE5D4B; + --background-primary: #F4F2FA; --background-secondary: #FFFFFF; --background-tertiary: #DEE2E7; @@ -12,11 +15,13 @@ --purple-highlight: #7B61FF; --text-dark: #1C1C1C; + --gray-800: #333333; --gray-700: #4E5566; --gray-600: #6E7485; --gray-500: #8B96A5; --gray-400: #CFCFCF; --gray-300: #DEE2E7; + --gray-200: #DEE2E7; --gray-100: #E9EAF0; --text-light: white; diff --git a/prisma/migrations/20250710200308_order_cancellation_status/migration.sql b/prisma/migrations/20250710200308_order_cancellation_status/migration.sql new file mode 100644 index 0000000..a672f1c --- /dev/null +++ b/prisma/migrations/20250710200308_order_cancellation_status/migration.sql @@ -0,0 +1,20 @@ +/* + Warnings: + + - The values [CANCELLED,REFUNDED] on the enum `OrderStatus` will be removed. If these variants are still used in the database, this will fail. + +*/ +-- CreateEnum +CREATE TYPE "CancellationStatus" AS ENUM ('CANCELLED', 'REFUNDED'); + +-- AlterEnum +BEGIN; +CREATE TYPE "OrderStatus_new" AS ENUM ('PENDING', 'ORDERED', 'SHIPPED', 'IN_TRANSIT', 'DELIVERED'); +ALTER TABLE "Order" ALTER COLUMN "status" TYPE "OrderStatus_new" USING ("status"::text::"OrderStatus_new"); +ALTER TYPE "OrderStatus" RENAME TO "OrderStatus_old"; +ALTER TYPE "OrderStatus_new" RENAME TO "OrderStatus"; +DROP TYPE "OrderStatus_old"; +COMMIT; + +-- AlterTable +ALTER TABLE "Order" ADD COLUMN "cancellation_status" "CancellationStatus"; diff --git a/prisma/schema/Order.prisma b/prisma/schema/Order.prisma index c256c5b..5e8b919 100644 --- a/prisma/schema/Order.prisma +++ b/prisma/schema/Order.prisma @@ -1,5 +1,5 @@ model Order { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) paymentIntentId String? total Float customer_name String @@ -16,6 +16,7 @@ model Order { shipping_zip String shipping_country String status OrderStatus + cancellation_status CancellationStatus? created_at DateTime products ProductToOrder[] } @@ -26,6 +27,9 @@ enum OrderStatus { SHIPPED IN_TRANSIT DELIVERED +} + +enum CancellationStatus { CANCELLED REFUNDED } From c3f57e3c2bd66722376cc12e236176641f71347d Mon Sep 17 00:00:00 2001 From: brandonw504 Date: Thu, 10 Jul 2025 17:04:25 -0700 Subject: [PATCH 5/5] Order list page --- app/(api)/_datalib/_resolvers/Order.ts | 10 ++ app/(api)/_datalib/_services/Orders.ts | 69 ++++++++- app/(api)/_datalib/_typeDefs/Order.ts | 5 + .../_components/OrderCard.module.scss | 13 -- .../OrderSection/OrderSection.module.scss | 24 ++-- .../_components/OrderSection/OrderSection.tsx | 27 +++- app/(pages)/(app)/(orders)/page.module.scss | 7 - app/(pages)/(app)/(orders)/page.tsx | 132 ++++++++++++++---- app/(pages)/_globals/styles/colors.scss | 2 +- 9 files changed, 227 insertions(+), 62 deletions(-) diff --git a/app/(api)/_datalib/_resolvers/Order.ts b/app/(api)/_datalib/_resolvers/Order.ts index 7de4f41..0cd3593 100644 --- a/app/(api)/_datalib/_resolvers/Order.ts +++ b/app/(api)/_datalib/_resolvers/Order.ts @@ -35,6 +35,16 @@ const resolvers = { args.limit, ctx ), + ordersCount: ( + _: never, + args: { + statuses: OrderStatus[]; + cancellation_statuses: CancellationStatus[]; + search: string; + }, + ctx: ApolloContext + ) => + Orders.count(args.statuses, args.cancellation_statuses, args.search, ctx), }, Mutation: { updateOrder: ( diff --git a/app/(api)/_datalib/_services/Orders.ts b/app/(api)/_datalib/_services/Orders.ts index 24aaa54..616628c 100644 --- a/app/(api)/_datalib/_services/Orders.ts +++ b/app/(api)/_datalib/_services/Orders.ts @@ -110,11 +110,78 @@ export default class Orders { orderBy: { created_at: 'desc', }, - skip: offset * limit, + skip: offset, take: limit, }); } + static async count( + statuses: OrderStatus[], + cancellation_statuses: CancellationStatus[], + search: string, + ctx: ApolloContext + ) { + if (!ctx.isOwner && !ctx.hasValidApiKey) return null; + + const queryConditions: Prisma.OrderWhereInput[] = []; + + const statusFilters: Prisma.OrderWhereInput[] = []; + if (statuses?.length) { + statusFilters.push({ status: { in: statuses } }); + } + if (cancellation_statuses?.length) { + statusFilters.push({ + cancellation_status: { in: cancellation_statuses }, + }); + } + + if (statusFilters.length > 1) { + queryConditions.push({ OR: statusFilters }); + } else if (statusFilters.length === 1) { + queryConditions.push(statusFilters[0]); + } + + const isInProgressRequest = + statuses?.length && + !cancellation_statuses?.length && + !statuses.includes(OrderStatus.DELIVERED); + if (isInProgressRequest) { + queryConditions.push({ cancellation_status: null }); + } + + if (search) { + const searchConditions: Prisma.OrderWhereInput[] = [ + { customer_name: { contains: search, mode: 'insensitive' } }, + { customer_email: { contains: search, mode: 'insensitive' } }, + { customer_phone_num: { contains: search, mode: 'insensitive' } }, + ]; + + const searchAsNumber = parseInt(search, 10); + if (!isNaN(searchAsNumber)) { + searchConditions.push({ id: searchAsNumber }); + searchConditions.push({ + id: { + in: await prisma.order + .findMany({ + select: { id: true }, + }) + .then((orders) => + orders + .filter((order) => order.id.toString().includes(search)) + .map((order) => order.id) + ), + }, + }); + } + + queryConditions.push({ OR: searchConditions }); + } + + return prisma.order.count({ + where: { AND: queryConditions }, + }); + } + static async getProducts(order_id: number, ctx: ApolloContext) { if (!ctx.isOwner && !ctx.hasValidApiKey) return null; diff --git a/app/(api)/_datalib/_typeDefs/Order.ts b/app/(api)/_datalib/_typeDefs/Order.ts index 1673b5e..9ce45f8 100644 --- a/app/(api)/_datalib/_typeDefs/Order.ts +++ b/app/(api)/_datalib/_typeDefs/Order.ts @@ -98,6 +98,11 @@ const typeDefs = gql` offset: Int! limit: Int! ): [Order] + ordersCount( + statuses: [OrderStatus] + cancellation_statuses: [CancellationStatus] + search: String + ): Int } type Mutation { diff --git a/app/(pages)/(app)/(orders)/_components/OrderCard.module.scss b/app/(pages)/(app)/(orders)/_components/OrderCard.module.scss index dd8be4e..5d38b74 100644 --- a/app/(pages)/(app)/(orders)/_components/OrderCard.module.scss +++ b/app/(pages)/(app)/(orders)/_components/OrderCard.module.scss @@ -1,6 +1,3 @@ -@use 'media'; -@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700&display=swap'); - .container { width: 100%; display: flex; @@ -11,11 +8,6 @@ border-radius: 12px; box-shadow: 0px 4px 20px rgba(51, 51, 51, 0.06); margin-bottom: 16px; - - @include media.tablet { - flex-direction: column; - gap: 16px; - } } .content { @@ -53,7 +45,6 @@ } .order_id { - font-family: 'Manrope', sans-serif; font-size: 18px; font-weight: 600; color: var(--text-dark); @@ -61,7 +52,6 @@ } .shipping_info { - font-family: 'Manrope', sans-serif; font-size: 14px; font-weight: 400; color: var(--purple); @@ -69,7 +59,6 @@ } .order_date { - font-family: 'Manrope', sans-serif; font-size: 14px; font-weight: 400; color: var(--gray-600); @@ -156,7 +145,6 @@ } .status_label { - font-family: 'Manrope', sans-serif; font-size: 12px; font-weight: 400; color: var(--gray-600); @@ -193,7 +181,6 @@ color: var(--text-light); border: none; border-radius: 8px; - font-family: 'Manrope', sans-serif; font-size: 14px; font-weight: 600; cursor: pointer; diff --git a/app/(pages)/(app)/(orders)/_components/OrderSection/OrderSection.module.scss b/app/(pages)/(app)/(orders)/_components/OrderSection/OrderSection.module.scss index 7f8644c..d4cdd22 100644 --- a/app/(pages)/(app)/(orders)/_components/OrderSection/OrderSection.module.scss +++ b/app/(pages)/(app)/(orders)/_components/OrderSection/OrderSection.module.scss @@ -12,7 +12,6 @@ width: 100%; h2 { - font-family: 'Manrope', sans-serif; font-size: 24px; font-weight: 600; color: var(--text-dark); @@ -23,7 +22,10 @@ .pagination { display: flex; align-items: center; - gap: 16px; + gap: 1rem; + background-color: var(--background-secondary); + padding: 0.5rem; + border-radius: 2rem; } .pagination_btn { @@ -32,29 +34,30 @@ display: flex; align-items: center; justify-content: center; - border: 1px solid var(--gray-300); - border-radius: 6px; - background-color: var(--background-secondary); - color: var(--gray-600); + border: none; + border-radius: 50%; + background-color: var(--background-primary); + color: var(--purple); cursor: pointer; transition: all 0.2s ease; &:hover { - background-color: var(--background-primary); - border-color: var(--gray-400); + background-color: var(--light-purple); + color: var(--text-light) } &:disabled { opacity: 0.5; cursor: not-allowed; + background-color: var(--gray-100); + color: var(--gray-400); } } .pagination_text { - font-family: 'Manrope', sans-serif; font-size: 14px; font-weight: 500; - color: var(--gray-600); + color: var(--text-dark); } .status_filters { @@ -66,7 +69,6 @@ .filter_btn { padding: 12px 20px; - font-family: 'Manrope', sans-serif; font-size: 14px; font-weight: 500; border: 1px solid var(--gray-300); diff --git a/app/(pages)/(app)/(orders)/_components/OrderSection/OrderSection.tsx b/app/(pages)/(app)/(orders)/_components/OrderSection/OrderSection.tsx index d33ff16..011bb10 100644 --- a/app/(pages)/(app)/(orders)/_components/OrderSection/OrderSection.tsx +++ b/app/(pages)/(app)/(orders)/_components/OrderSection/OrderSection.tsx @@ -1,3 +1,4 @@ +import { FaChevronLeft, FaChevronRight } from 'react-icons/fa'; import { Order } from '@datatypes/Order'; import OrderCard from '../OrderCard'; import styles from './OrderSection.module.scss'; @@ -9,6 +10,9 @@ interface OrderSectionProps { statusClassMap: { [key: string]: string }; selectedStatus: T | 'All'; onStatusChange: (status: T | 'All') => void; + page: number; + onPageChange: (page: number) => void; + highestPage: number; } export default function OrderSection({ @@ -18,15 +22,32 @@ export default function OrderSection({ statusClassMap, selectedStatus, onStatusChange, + page, + onPageChange, + highestPage, }: OrderSectionProps) { return (

{title}

- - 1 of 5 - + + + {page + 1} of {highestPage + 1} + +
diff --git a/app/(pages)/(app)/(orders)/page.module.scss b/app/(pages)/(app)/(orders)/page.module.scss index b121200..a57b6dc 100644 --- a/app/(pages)/(app)/(orders)/page.module.scss +++ b/app/(pages)/(app)/(orders)/page.module.scss @@ -1,5 +1,3 @@ -@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700&display=swap'); - .page_container { display: flex; flex-direction: column; @@ -18,7 +16,6 @@ } .title { - font-family: 'Manrope', sans-serif; font-size: 48px; font-weight: 700; color: var(--text-dark); @@ -35,7 +32,6 @@ .search { flex: 1; padding: 12px 20px; - font-family: 'Manrope', sans-serif; font-size: 16px; border: 1px solid var(--gray-300); border-radius: 50px; @@ -53,7 +49,6 @@ .search_submit { padding: 12px 24px; - font-family: 'Manrope', sans-serif; font-size: 16px; font-weight: 600; border: none; @@ -100,7 +95,6 @@ } .pagination_text { - font-family: 'Manrope', sans-serif; font-size: 14px; font-weight: 500; color: var(--gray-600); @@ -115,7 +109,6 @@ .filter_btn { padding: 12px 20px; - font-family: 'Manrope', sans-serif; font-size: 14px; font-weight: 500; border: 1px solid var(--gray-300); diff --git a/app/(pages)/(app)/(orders)/page.tsx b/app/(pages)/(app)/(orders)/page.tsx index 90457a7..865d325 100644 --- a/app/(pages)/(app)/(orders)/page.tsx +++ b/app/(pages)/(app)/(orders)/page.tsx @@ -4,21 +4,25 @@ import { useState, useEffect } from 'react'; import { gql } from 'graphql-tag'; import sendApolloRequest from '../../_utils/sendApolloRequest'; import { Order, OrderStatus, CancellationStatus } from '@datatypes/Order'; -import styles from './page.module.scss'; import OrderSection from './_components/OrderSection/OrderSection'; +import styles from './page.module.scss'; -const query = gql` +const QUERY_LIMIT = 3; + +const ordersQuery = gql` query OrderQuery( $offset: Int! $limit: Int! $statuses: [OrderStatus!] $cancellation_statuses: [CancellationStatus!] + $search: String ) { orders( offset: $offset limit: $limit statuses: $statuses cancellation_statuses: $cancellation_statuses + search: $search ) { id customer_name @@ -51,20 +55,35 @@ const query = gql` } `; -const variables = { - offset: 0, - limit: 5, -}; +const countQuery = gql` + query OrdersCount( + $statuses: [OrderStatus!] + $cancellation_statuses: [CancellationStatus!] + $search: String + ) { + ordersCount( + statuses: $statuses + cancellation_statuses: $cancellation_statuses + search: $search + ) + } +`; export default function ViewOrderCards() { const [inProgressOrders, setInProgressOrders] = useState([]); + const [inProgressOrdersCount, setInProgressOrdersCount] = useState(0); const [completedOrders, setCompletedOrders] = useState([]); + const [completedOrdersCount, setCompletedOrdersCount] = useState(0); const [selectedStatus, setSelectedStatus] = useState( 'All' ); const [selectedCompletedStatus, setSelectedCompletedStatus] = useState< CancellationStatus | 'All' | 'DELIVERED' >('All'); + const [inProgressPage, setInProgressPage] = useState(0); + const [completedPage, setCompletedPage] = useState(0); + const [searchInput, setSearchInput] = useState(''); + const [searchTerm, setSearchTerm] = useState(''); const statusClassMap: { [key: string]: string } = { All: 'all-btn', @@ -95,8 +114,15 @@ export default function ViewOrderCards() { ...Object.values(CancellationStatus), ]; + const handleSearch = (e: React.FormEvent) => { + e.preventDefault(); + setSearchTerm(searchInput); + setInProgressPage(0); + setCompletedPage(0); + }; + useEffect(() => { - const fetchOrders = async () => { + const fetchInProgressOrders = async () => { const inProgressStatuses = selectedStatus === 'All' ? [ @@ -106,14 +132,34 @@ export default function ViewOrderCards() { OrderStatus.IN_TRANSIT, ] : [selectedStatus]; - const inProgressPromise = sendApolloRequest({ - request: query, + const ordersPromise = sendApolloRequest({ + request: ordersQuery, + variables: { + offset: inProgressPage * QUERY_LIMIT, + limit: QUERY_LIMIT, + statuses: inProgressStatuses, + search: searchTerm, + }, + }); + const countPromise = sendApolloRequest({ + request: countQuery, variables: { - ...variables, statuses: inProgressStatuses, + search: searchTerm, }, }); + const [ordersData, countData] = await Promise.all([ + ordersPromise, + countPromise, + ]); + setInProgressOrders(ordersData.data.orders); + setInProgressOrdersCount(countData.data.ordersCount); + }; + fetchInProgressOrders(); + }, [selectedStatus, inProgressPage, searchTerm]); + useEffect(() => { + const fetchCompletedOrders = async () => { let completed_statuses: OrderStatus[] = []; let cancellation_statuses: CancellationStatus[] = []; @@ -126,47 +172,69 @@ export default function ViewOrderCards() { cancellation_statuses = [selectedCompletedStatus as CancellationStatus]; } - const completedPromise = sendApolloRequest({ - request: query, + const ordersPromise = sendApolloRequest({ + request: ordersQuery, variables: { - ...variables, + offset: completedPage * QUERY_LIMIT, + limit: QUERY_LIMIT, statuses: completed_statuses.length > 0 ? completed_statuses : undefined, cancellation_statuses: cancellation_statuses.length > 0 ? cancellation_statuses : undefined, + search: searchTerm, }, }); - - const [inProgressData, completedData] = await Promise.all([ - inProgressPromise, - completedPromise, + const countPromise = sendApolloRequest({ + request: countQuery, + variables: { + statuses: + completed_statuses.length > 0 ? completed_statuses : undefined, + cancellation_statuses: + cancellation_statuses.length > 0 + ? cancellation_statuses + : undefined, + search: searchTerm, + }, + }); + const [ordersData, countData] = await Promise.all([ + ordersPromise, + countPromise, ]); - - setInProgressOrders(inProgressData.data.orders); - setCompletedOrders(completedData.data.orders); + setCompletedOrders(ordersData.data.orders); + setCompletedOrdersCount(countData.data.ordersCount); }; + fetchCompletedOrders(); + }, [selectedCompletedStatus, completedPage, searchTerm]); - fetchOrders(); - }, [selectedStatus, selectedCompletedStatus]); + const highestInProgressPage = Math.max( + 0, + Math.ceil(inProgressOrdersCount / QUERY_LIMIT) - 1 + ); + const highestCompletedPage = Math.max( + 0, + Math.ceil(completedOrdersCount / QUERY_LIMIT) - 1 + ); return (

Orders

-
+
setSearchInput(e.target.value)} /> -
+
{ + setSelectedStatus(status); + setInProgressPage(0); + }} + page={inProgressPage} + onPageChange={setInProgressPage} + highestPage={highestInProgressPage} /> { + setSelectedCompletedStatus(status); + setCompletedPage(0); + }} + page={completedPage} + onPageChange={setCompletedPage} + highestPage={highestCompletedPage} />
); diff --git a/app/(pages)/_globals/styles/colors.scss b/app/(pages)/_globals/styles/colors.scss index 72a03f1..a176e35 100644 --- a/app/(pages)/_globals/styles/colors.scss +++ b/app/(pages)/_globals/styles/colors.scss @@ -8,7 +8,7 @@ --error-light: #FFEDED; --error-dark: #DE5D4B; - --background-primary: #F4F2FA; + --background-primary: #ede8fc; --background-secondary: #FFFFFF; --background-tertiary: #DEE2E7; --background-upload: #F9F9F9;