Skip to content

Commit 05a4468

Browse files
committed
feat: add order page for admin, chnage deliverystatus
1 parent b57aa11 commit 05a4468

File tree

9 files changed

+453
-3
lines changed

9 files changed

+453
-3
lines changed

package-lock.json

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"@tailwindcss/aspect-ratio": "^0.4.2",
4646
"@tailwindcss/forms": "^0.5.3",
4747
"@tailwindcss/typography": "^0.5.8",
48+
"@types/nodemailer": "^6.4.7",
4849
"@typescript-eslint/eslint-plugin": "^5.45.0",
4950
"@typescript-eslint/parser": "^5.45.0",
5051
"autoprefixer": "^10.4.13",

src/appdata/list.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const STATUSES = ['PENDING', 'SHIPPING', 'SUCCESS', 'CANCELED']

src/pages/dashboard/order/[id].tsx

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { zodResolver } from '@hookform/resolvers/zod'
2+
import Image from 'next/image'
3+
import Link from 'next/link'
4+
import { useRouter } from 'next/router'
5+
import { useForm } from 'react-hook-form'
6+
import { toast } from 'react-hot-toast'
7+
import { z } from 'zod'
8+
9+
import { STATUSES } from '@/appdata/list'
10+
import { AdminLayout } from '@/components/common/layout/AdminLayout'
11+
import { SelectInput } from '@/components/common/Select'
12+
import { SubmitButton } from '@/components/dashboard/common/Buttons'
13+
import { trpc } from '@/utils/trpc'
14+
15+
const schema = z.object({
16+
deliveryStatus: z
17+
.string({ invalid_type_error: 'Please select valid status' })
18+
.refine((val) => STATUSES.map((c) => c).includes(val as any), {
19+
message: 'Please select valid status',
20+
}),
21+
})
22+
type SchemaType = z.infer<typeof schema>
23+
24+
export default function Example() {
25+
const router = useRouter()
26+
const utils = trpc.useContext()
27+
const orderDetails = trpc.order.getOrderDetails.useQuery({
28+
id: router.query.id as string,
29+
})
30+
const updateDeliveryStatus = trpc.order.updateDeliveryStatus.useMutation()
31+
const {
32+
register,
33+
handleSubmit,
34+
formState: { errors, isSubmitting },
35+
} = useForm<SchemaType>({
36+
resolver: zodResolver(schema),
37+
defaultValues: {
38+
deliveryStatus: orderDetails.data?.deliveryStatus || '',
39+
},
40+
})
41+
async function submit(val: SchemaType) {
42+
if (val.deliveryStatus === orderDetails.data?.deliveryStatus) {
43+
toast.error('Delivery status is not changed')
44+
return
45+
}
46+
await updateDeliveryStatus.mutate(
47+
{
48+
id: router.query.id as string,
49+
...val,
50+
},
51+
{
52+
onSuccess() {
53+
toast.success('Changed delivery status')
54+
utils.order.getOrderDetails.invalidate({
55+
id: router.query.id as string,
56+
})
57+
},
58+
}
59+
)
60+
}
61+
return (
62+
<AdminLayout>
63+
<section className="section" aria-labelledby="page-title">
64+
<h1 id="page-title" className="heading1">
65+
Order: {router.query.id}
66+
</h1>
67+
{orderDetails.isLoading && <p>Loading . . .</p>}
68+
{orderDetails.error && (
69+
<p className="text-red-500">{orderDetails.error.message}</p>
70+
)}
71+
{orderDetails.data && (
72+
<div className="mx-auto my-4 max-w-7xl rounded border border-gray-300 dark:border-gray-700">
73+
<article>
74+
<div className="my-4 px-6">
75+
<p className="font-medium tracking-widest">User Details</p>
76+
<dl className="grid grid-cols-2 text-sm tracking-wider sm:text-base md:grid-cols-4">
77+
<dt>Name</dt>
78+
<dd>{orderDetails.data.name}</dd>
79+
{orderDetails.data.email && (
80+
<>
81+
{' '}
82+
<dt>Email</dt>
83+
<dd title={orderDetails.data.email} className="truncate">
84+
{orderDetails.data.email}
85+
</dd>
86+
</>
87+
)}
88+
<dt>Country</dt>
89+
<dd>{orderDetails.data.country}</dd>
90+
<dt>Address line1</dt>
91+
<dd>{orderDetails.data.addressLine1}</dd>
92+
<dt>Address line2</dt>
93+
<dd>{orderDetails.data.addressLine2}</dd>
94+
<dt>City</dt>
95+
<dd>{orderDetails.data.city}</dd>
96+
<dt>Postal code</dt>
97+
<dd>{orderDetails.data.postalCode}</dd>
98+
<dt>Phone</dt>
99+
<dd>{orderDetails.data.phone}</dd>
100+
</dl>
101+
</div>
102+
<div className="border-t border-gray-300 dark:border-gray-700">
103+
<p className="px-6 font-medium tracking-widest">
104+
Ordered Items
105+
</p>
106+
{orderDetails.data.items.map((item: any) => {
107+
if (!item) return null
108+
return (
109+
<div className="my-4 px-6" key={item.id}>
110+
<div className="grid grid-cols-2 items-center gap-2">
111+
<Image
112+
src={item.image}
113+
alt={item.name}
114+
width="100"
115+
height="100"
116+
/>
117+
<div>
118+
<Link
119+
href={`/products/${item.slug}`}
120+
className="text-blue-700 transition hover:text-blue-500 dark:text-blue-500 hover:dark:text-blue-700"
121+
>
122+
<p className="text-sm font-medium md:text-base">
123+
{item.name}
124+
</p>
125+
</Link>
126+
<p className="text-xs font-medium md:text-base">
127+
Quantity: {item.quantity}
128+
</p>
129+
<p className="text-sm font-medium md:text-base">
130+
${item.price}
131+
</p>
132+
</div>
133+
</div>
134+
</div>
135+
)
136+
})}
137+
</div>
138+
<form
139+
onSubmit={handleSubmit((v) => submit(v))}
140+
className="space-y-3 border-t border-gray-300 p-2 font-medium tracking-wider dark:border-gray-700"
141+
>
142+
<SelectInput
143+
error={errors.deliveryStatus}
144+
label="Select Status"
145+
options={['PENDING', 'SHIPPING', 'SUCCESS', 'CANCELED']}
146+
{...register('deliveryStatus')}
147+
/>
148+
<SubmitButton disabled={isSubmitting} text="Update" />
149+
</form>
150+
<div className="flex justify-end border-t border-gray-300 font-medium tracking-wider dark:border-gray-700">
151+
<dl>
152+
<div className="flex gap-2">
153+
<dt>Shipping charge: </dt>
154+
<dd>$ {orderDetails.data.shippingCharge}</dd>
155+
</div>
156+
<div className="flex gap-2">
157+
<dt>Total amount: </dt>
158+
<dd className="font-medium">
159+
$ {orderDetails.data.totalAmount}
160+
</dd>
161+
</div>
162+
</dl>
163+
</div>
164+
</article>
165+
</div>
166+
)}
167+
</section>
168+
</AdminLayout>
169+
)
170+
}

src/pages/dashboard/order/index.tsx

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import type { ColumnDef } from '@tanstack/react-table'
2+
import Link from 'next/link'
3+
import { useMemo } from 'react'
4+
import { AiFillEdit } from 'react-icons/ai'
5+
6+
import { AdminLayout } from '@/components/common/layout/AdminLayout'
7+
import { Table } from '@/components/dashboard/common/Table'
8+
import type { Order } from '@/types/order'
9+
import { trpc } from '@/utils/trpc'
10+
11+
const getBgColor = (status: string) => {
12+
let str = ''
13+
switch (status.toUpperCase()) {
14+
case 'PENDING':
15+
str = 'bg-blue-700'
16+
break
17+
case 'SHIPPING':
18+
str = 'bg-teal-700'
19+
break
20+
case 'SUCCESS':
21+
str = 'bg-green-600'
22+
break
23+
case 'CANCELED':
24+
str = 'bg-red-700'
25+
break
26+
27+
default:
28+
break
29+
}
30+
return str
31+
}
32+
export default function Example() {
33+
const orderList = trpc.order.getAdminOrder.useQuery()
34+
const columns = useMemo<ColumnDef<Order>[]>(
35+
() => [
36+
{
37+
header: 'Id',
38+
accessorKey: 'id',
39+
cell: (props) => <p>{props.row.original.id}</p>,
40+
},
41+
{
42+
header: 'Order by',
43+
accessorKey: 'name',
44+
cell: (props) => <p>{props.row.original.name}</p>,
45+
},
46+
{
47+
header: 'No. of Items',
48+
accessorKey: 'items',
49+
enableSorting: false,
50+
enableGlobalFilter: false,
51+
cell: (props) => props.row.original.items.length,
52+
},
53+
{
54+
header: 'Shipping',
55+
accessorKey: 'shippingCharge',
56+
enableSorting: false,
57+
enableGlobalFilter: false,
58+
cell: (props) => (
59+
<p>
60+
{props.row.original.shippingCharge === 0
61+
? 'FREE'
62+
: `$${props.row.original.shippingCharge}`}
63+
</p>
64+
),
65+
},
66+
{
67+
header: 'Status',
68+
accessorKey: 'deliveryStatus',
69+
cell: (props) => (
70+
<p
71+
className={`max-w-max whitespace-nowrap rounded-2xl py-1 px-3 text-white ${getBgColor(
72+
props.row.original.deliveryStatus
73+
)}`}
74+
>
75+
{props.row.original.deliveryStatus}
76+
</p>
77+
),
78+
},
79+
{
80+
header: 'Total',
81+
enableSorting: false,
82+
enableGlobalFilter: false,
83+
accessorKey: 'totalAmount',
84+
cell: (props) => `$${props.renderValue()}`,
85+
},
86+
{
87+
header: 'Details',
88+
accessorKey: 'details',
89+
enableSorting: false,
90+
enableGlobalFilter: false,
91+
cell: (props) => (
92+
<Link
93+
href={`/dashboard/order/${props.row.original.id}`}
94+
className="flex max-w-max items-center justify-center rounded-full bg-indigo-600 p-1 transition hover:scale-105 dark:bg-indigo-500"
95+
>
96+
<AiFillEdit aria-hidden="true" className="h-5 w-5 text-white" />
97+
<p className="sr-only">Order Details {props.row.original.id}</p>
98+
</Link>
99+
),
100+
},
101+
],
102+
[]
103+
)
104+
return (
105+
<AdminLayout>
106+
<section className="section" aria-labelledby="page-title">
107+
<h1 id="page-title" className="heading1">
108+
Order list
109+
</h1>
110+
111+
<div className="w-full p-4">
112+
{orderList.isLoading && <p>Loading . . .</p>}
113+
{orderList.error && <p>{orderList.error.message}</p>}
114+
{!orderList.isError && !orderList.isLoading && (
115+
<Table columns={columns} data={(orderList.data as any) || []} />
116+
)}
117+
</div>
118+
</section>
119+
</AdminLayout>
120+
)
121+
}

src/pages/dashboard/product/index.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,12 +169,14 @@ export default function Index() {
169169
),
170170
},
171171
],
172-
[router]
172+
[deleteProduct, router, utils.product.dashboardList]
173173
)
174174
return (
175175
<AdminLayout>
176176
<section className="section" aria-labelledby="page-title">
177-
<h1 className="heading1">Product list</h1>
177+
<h1 id="page-title" className="heading1">
178+
Product list
179+
</h1>
178180

179181
<div className="w-full p-4">
180182
{productList.isLoading && <p>Loading . . .</p>}

0 commit comments

Comments
 (0)