hey-pharmacist-ecommerce 1.1.30 → 1.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +1451 -1303
- package/dist/index.d.ts +1451 -1303
- package/dist/index.js +10502 -5728
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +7817 -3059
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/src/components/AccountReviewsTab.tsx +97 -0
- package/src/components/CouponCodeInput.tsx +190 -0
- package/src/components/Header.tsx +5 -1
- package/src/components/Notification.tsx +1 -1
- package/src/components/NotificationBell.tsx +33 -0
- package/src/components/NotificationCard.tsx +211 -0
- package/src/components/NotificationDrawer.tsx +195 -0
- package/src/components/OrderCard.tsx +164 -99
- package/src/components/ProductReviewsSection.tsx +30 -0
- package/src/components/RatingDistribution.tsx +86 -0
- package/src/components/ReviewCard.tsx +59 -0
- package/src/components/ReviewForm.tsx +207 -0
- package/src/components/ReviewPromptBanner.tsx +98 -0
- package/src/components/ReviewsList.tsx +151 -0
- package/src/components/StarRating.tsx +98 -0
- package/src/hooks/useDiscounts.ts +7 -0
- package/src/hooks/useOrders.ts +15 -0
- package/src/hooks/useReviews.ts +230 -0
- package/src/index.ts +25 -0
- package/src/lib/Apis/apis/discounts-api.ts +23 -72
- package/src/lib/Apis/apis/notifications-api.ts +196 -231
- package/src/lib/Apis/apis/products-api.ts +84 -0
- package/src/lib/Apis/apis/review-api.ts +283 -4
- package/src/lib/Apis/apis/stores-api.ts +180 -0
- package/src/lib/Apis/models/bulk-channel-toggle-dto.ts +52 -0
- package/src/lib/Apis/models/cart-body-populated.ts +3 -3
- package/src/lib/Apis/models/channel-settings-dto.ts +39 -0
- package/src/lib/Apis/models/{discount-paginated-response.ts → completed-order-dto.ts} +21 -16
- package/src/lib/Apis/models/create-discount-dto.ts +31 -92
- package/src/lib/Apis/models/create-review-dto.ts +4 -4
- package/src/lib/Apis/models/create-shippo-account-dto.ts +45 -0
- package/src/lib/Apis/models/create-store-dto.ts +6 -0
- package/src/lib/Apis/models/discount.ts +37 -98
- package/src/lib/Apis/models/discounts-insights-dto.ts +12 -0
- package/src/lib/Apis/models/index.ts +13 -7
- package/src/lib/Apis/models/{manual-discount.ts → manual-discount-dto.ts} +10 -10
- package/src/lib/Apis/models/manual-order-dto.ts +3 -3
- package/src/lib/Apis/models/populated-discount.ts +41 -101
- package/src/lib/Apis/models/preference-update-item.ts +59 -0
- package/src/lib/Apis/models/product-light-dto.ts +40 -0
- package/src/lib/Apis/models/{check-notifications-response-dto.ts → review-status-dto.ts} +8 -7
- package/src/lib/Apis/models/review.ts +9 -3
- package/src/lib/Apis/models/reviewable-order-dto.ts +58 -0
- package/src/lib/Apis/models/reviewable-product-dto.ts +81 -0
- package/src/lib/Apis/models/shippo-account-response-dto.ts +51 -0
- package/src/lib/Apis/models/store-entity.ts +6 -0
- package/src/lib/Apis/models/store.ts +6 -0
- package/src/lib/Apis/models/update-discount-dto.ts +31 -92
- package/src/lib/Apis/models/update-notification-settings-dto.ts +28 -0
- package/src/lib/Apis/models/update-review-dto.ts +4 -4
- package/src/lib/Apis/models/update-store-dto.ts +6 -0
- package/src/lib/Apis/models/{pick-type-class.ts → variant-light-dto.ts} +20 -14
- package/src/lib/utils/discount.ts +155 -0
- package/src/lib/validations/discount.ts +11 -0
- package/src/providers/CartProvider.tsx +2 -2
- package/src/providers/DiscountProvider.tsx +97 -0
- package/src/providers/EcommerceProvider.tsx +13 -5
- package/src/providers/NotificationCenterProvider.tsx +436 -0
- package/src/screens/CartScreen.tsx +1 -1
- package/src/screens/CheckoutScreen.tsx +39 -12
- package/src/screens/NotificationSettingsScreen.tsx +413 -0
- package/src/screens/OrderDetailScreen.tsx +283 -0
- package/src/screens/OrderReviewsScreen.tsx +308 -0
- package/src/screens/OrdersScreen.tsx +31 -7
- package/src/screens/ProductDetailScreen.tsx +24 -11
- package/src/screens/ProfileScreen.tsx +5 -0
- package/src/lib/Apis/models/create-notification-dto.ts +0 -75
- package/src/lib/Apis/models/notification.ts +0 -93
- package/src/lib/Apis/models/single-notification-dto.ts +0 -99
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { motion } from 'framer-motion';
|
|
5
|
+
import {
|
|
6
|
+
ChevronLeft,
|
|
7
|
+
Package,
|
|
8
|
+
Truck,
|
|
9
|
+
MapPin,
|
|
10
|
+
CreditCard,
|
|
11
|
+
Calendar,
|
|
12
|
+
ExternalLink,
|
|
13
|
+
Printer,
|
|
14
|
+
CheckCircle2,
|
|
15
|
+
Clock,
|
|
16
|
+
AlertCircle,
|
|
17
|
+
Info
|
|
18
|
+
} from 'lucide-react';
|
|
19
|
+
import { useRouter } from 'next/navigation';
|
|
20
|
+
import { useOrder } from '@/hooks/useOrders';
|
|
21
|
+
import { useBasePath } from '@/providers/BasePathProvider';
|
|
22
|
+
import { formatPrice, formatDate } from '@/lib/utils/format';
|
|
23
|
+
import { Badge } from '@/components/ui/Badge';
|
|
24
|
+
import { Button } from '@/components/ui/Button';
|
|
25
|
+
import Image from 'next/image';
|
|
26
|
+
|
|
27
|
+
interface OrderDetailScreenProps {
|
|
28
|
+
id: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function OrderDetailScreen({ id }: OrderDetailScreenProps) {
|
|
32
|
+
const router = useRouter();
|
|
33
|
+
const { buildPath } = useBasePath();
|
|
34
|
+
const { order, isLoading, error } = useOrder(id);
|
|
35
|
+
|
|
36
|
+
if (isLoading) {
|
|
37
|
+
return (
|
|
38
|
+
<div className="min-h-screen bg-slate-50 flex flex-col items-center justify-center p-4">
|
|
39
|
+
<div className="w-16 h-16 border-4 border-primary/20 border-t-primary rounded-full animate-spin mb-4" />
|
|
40
|
+
<p className="text-muted font-medium animate-pulse">Retrieving order details...</p>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (error || !order) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="min-h-screen bg-slate-50 flex flex-col items-center justify-center p-4">
|
|
48
|
+
<div className="p-4 bg-red-50 rounded-full mb-4">
|
|
49
|
+
<AlertCircle className="w-10 h-10 text-red-500" />
|
|
50
|
+
</div>
|
|
51
|
+
<h1 className="text-xl font-bold text-secondary mb-2">Order Not Found</h1>
|
|
52
|
+
<p className="text-muted mb-6">We couldn't find the order you're looking for.</p>
|
|
53
|
+
<Button onClick={() => router.push(buildPath('/account'))}>
|
|
54
|
+
Back to Account
|
|
55
|
+
</Button>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const items = order.items || [];
|
|
61
|
+
const status = order.orderStatus || 'Pending';
|
|
62
|
+
const isDelivery = order.orderType === 'Delivery';
|
|
63
|
+
|
|
64
|
+
const getStatusVariant = (status: string): 'success' | 'warning' | 'primary' | 'danger' | 'gray' => {
|
|
65
|
+
switch (status.toLowerCase()) {
|
|
66
|
+
case 'pending': return 'warning';
|
|
67
|
+
case 'delivered':
|
|
68
|
+
case 'fulfilled':
|
|
69
|
+
case 'picked up': return 'success';
|
|
70
|
+
case 'shipped': return 'primary';
|
|
71
|
+
case 'cancelled': return 'danger';
|
|
72
|
+
default: return 'gray';
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const shippingAddress = order.shippingInfo?.addressTo;
|
|
77
|
+
const pickupAddress = order.pickUpAddress;
|
|
78
|
+
const activeAddress = isDelivery ? shippingAddress : pickupAddress;
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className="min-h-screen bg-linear-to-b from-[#F8FAFC] to-[#EBF4FB] pb-20">
|
|
82
|
+
{/* Header / Navigation */}
|
|
83
|
+
<div className="container mx-auto px-4 pt-8 max-w-6xl">
|
|
84
|
+
<button
|
|
85
|
+
onClick={() => router.back()}
|
|
86
|
+
className="group flex items-center gap-2 text-muted hover:text-secondary transition-colors mb-6"
|
|
87
|
+
>
|
|
88
|
+
<div className="p-1.5 rounded-full bg-white shadow-xs group-hover:shadow-md transition-all">
|
|
89
|
+
<ChevronLeft className="w-4 h-4" />
|
|
90
|
+
</div>
|
|
91
|
+
<span className="text-sm font-semibold">Back to History</span>
|
|
92
|
+
</button>
|
|
93
|
+
|
|
94
|
+
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6 mb-8">
|
|
95
|
+
<div>
|
|
96
|
+
<div className="flex items-center gap-3 mb-2">
|
|
97
|
+
<h1 className="text-3xl font-bold text-secondary">Order Details</h1>
|
|
98
|
+
<Badge variant={getStatusVariant(status)}>{status}</Badge>
|
|
99
|
+
<Badge variant="primary" className="bg-primary-100 text-primary-700 border-primary-200">
|
|
100
|
+
{order.orderType || 'Pickup'}
|
|
101
|
+
</Badge>
|
|
102
|
+
</div>
|
|
103
|
+
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-muted">
|
|
104
|
+
<span className="flex items-center gap-1.5 font-medium">
|
|
105
|
+
<span className="opacity-60 text-xs uppercase tracking-widest font-bold">ID:</span>
|
|
106
|
+
<span className="text-secondary font-mono tracking-tight">#{id.toUpperCase()}</span>
|
|
107
|
+
</span>
|
|
108
|
+
<span className="flex items-center gap-1.5 font-medium">
|
|
109
|
+
<Calendar className="w-4 h-4 opacity-60" />
|
|
110
|
+
{formatDate(order.createdAt || new Date(), 'long')}
|
|
111
|
+
</span>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
<div className="flex gap-3">
|
|
115
|
+
{order.payment?.hostedInvoiceUrl && (
|
|
116
|
+
<Button size="sm" onClick={() => window.open(order.payment?.hostedInvoiceUrl, '_blank')} className="bg-accent hover:bg-accent-dark border-none">
|
|
117
|
+
<ExternalLink className="w-4 h-4" />
|
|
118
|
+
Payment Details
|
|
119
|
+
</Button>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div className="container mx-auto px-4 max-w-6xl grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
126
|
+
{/* Left Column: Order Tracking & Items */}
|
|
127
|
+
<div className="lg:col-span-2 space-y-6">
|
|
128
|
+
{/* Items Section */}
|
|
129
|
+
<motion.div
|
|
130
|
+
initial={{ opacity: 0, y: 20 }}
|
|
131
|
+
animate={{ opacity: 1, y: 0 }}
|
|
132
|
+
className="bg-white rounded-3xl border border-slate-200 shadow-xs overflow-hidden"
|
|
133
|
+
>
|
|
134
|
+
<div className="p-6 border-b border-slate-100 bg-slate-50/50 flex items-center justify-between">
|
|
135
|
+
<div className="flex items-center gap-2">
|
|
136
|
+
<Package className="w-5 h-5 text-secondary" />
|
|
137
|
+
<h2 className="font-bold text-secondary">Order Items</h2>
|
|
138
|
+
</div>
|
|
139
|
+
<span className="text-xs font-bold bg-slate-200 text-muted px-2.5 py-1 rounded-full">
|
|
140
|
+
{items.length} {items.length === 1 ? 'Product' : 'Products'}
|
|
141
|
+
</span>
|
|
142
|
+
</div>
|
|
143
|
+
<div className="divide-y divide-slate-100">
|
|
144
|
+
{items.map((item, idx) => (
|
|
145
|
+
<div key={item._id || idx} className="p-6 flex gap-6 group hover:bg-slate-50/50 transition-colors">
|
|
146
|
+
<div className="relative w-20 h-20 bg-slate-100 rounded-2xl overflow-hidden shrink-0 border border-slate-100 group-hover:scale-105 transition-transform duration-300">
|
|
147
|
+
<Image
|
|
148
|
+
src={item.productVariantData?.media?.[0]?.file || '/placeholder-product.jpg'}
|
|
149
|
+
alt={item.productVariantData?.name || 'Item'}
|
|
150
|
+
fill
|
|
151
|
+
className="object-cover"
|
|
152
|
+
sizes="80px"
|
|
153
|
+
/>
|
|
154
|
+
</div>
|
|
155
|
+
<div className="flex-1 flex flex-col justify-between py-1">
|
|
156
|
+
<div>
|
|
157
|
+
<h3 className="font-bold text-secondary text-lg group-hover:text-primary transition-colors leading-snug mb-1">
|
|
158
|
+
{item.productVariantData?.name}
|
|
159
|
+
</h3>
|
|
160
|
+
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-muted">
|
|
161
|
+
<span className="flex items-center gap-1.5">
|
|
162
|
+
<span className="w-1.5 h-1.5 rounded-full bg-slate-300" />
|
|
163
|
+
Quantity: <span className="text-secondary font-bold">{item.quantity}</span>
|
|
164
|
+
</span>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
<div className="flex items-center justify-between">
|
|
168
|
+
<span className="text-muted text-sm">{formatPrice(item.productVariantData?.finalPrice || 0)} per unit</span>
|
|
169
|
+
<span className="font-black text-secondary">{formatPrice((item.productVariantData?.finalPrice || 0) * item.quantity)}</span>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
))}
|
|
174
|
+
</div>
|
|
175
|
+
</motion.div>
|
|
176
|
+
|
|
177
|
+
{/* Order Meta / Info */}
|
|
178
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
179
|
+
<div className="bg-white p-6 rounded-3xl border border-slate-200 shadow-xs">
|
|
180
|
+
<div className="flex items-center gap-2 mb-4">
|
|
181
|
+
<MapPin className="w-5 h-5 text-accent" />
|
|
182
|
+
<h3 className="font-bold text-secondary">{isDelivery ? 'Shipping Address' : 'Pickup Location'}</h3>
|
|
183
|
+
</div>
|
|
184
|
+
{activeAddress ? (
|
|
185
|
+
<div className="text-sm text-muted leading-relaxed">
|
|
186
|
+
<p className="font-bold text-secondary text-base mb-1">
|
|
187
|
+
{activeAddress.name}
|
|
188
|
+
</p>
|
|
189
|
+
<p>{activeAddress.street1}</p>
|
|
190
|
+
{activeAddress.street2 && <p>{activeAddress.street2}</p>}
|
|
191
|
+
<p>{activeAddress.city}, {activeAddress.state} {activeAddress.zip}</p>
|
|
192
|
+
<p>{activeAddress.country}</p>
|
|
193
|
+
</div>
|
|
194
|
+
) : (
|
|
195
|
+
<p className="text-sm text-muted italic">No address recorded</p>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
<div className="bg-white p-6 rounded-3xl border border-slate-200 shadow-xs">
|
|
199
|
+
<div className="flex items-center gap-2 mb-4">
|
|
200
|
+
<CreditCard className="w-5 h-5 text-accent" />
|
|
201
|
+
<h3 className="font-bold text-secondary">Payment Method</h3>
|
|
202
|
+
</div>
|
|
203
|
+
<div className="text-sm text-muted leading-relaxed">
|
|
204
|
+
<p className="font-bold text-secondary text-base mb-1">
|
|
205
|
+
{order.payment?.paymentMethod ? order.payment.paymentMethod.replace('_', ' ').toUpperCase() : 'N/A'}
|
|
206
|
+
</p>
|
|
207
|
+
<p className="flex items-center gap-2 mt-2">
|
|
208
|
+
<span className="opacity-60">Status:</span>
|
|
209
|
+
<Badge variant={order.payment?.paymentStatus === 'Paid' ? 'success' : 'warning'}>
|
|
210
|
+
{order.payment?.paymentStatus || 'Processing'}
|
|
211
|
+
</Badge>
|
|
212
|
+
</p>
|
|
213
|
+
{order.payment?.transactionId && (
|
|
214
|
+
<p className="mt-3 text-[10px] font-bold text-slate-400 uppercase tracking-tighter">
|
|
215
|
+
Ref: {order.payment.transactionId}
|
|
216
|
+
</p>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
{/* Right Column: Order Summary */}
|
|
224
|
+
<div className="space-y-6">
|
|
225
|
+
<motion.div
|
|
226
|
+
initial={{ opacity: 0, x: 20 }}
|
|
227
|
+
animate={{ opacity: 1, x: 0 }}
|
|
228
|
+
className="bg-secondary p-8 rounded-[2rem] text-white shadow-xl shadow-secondary/20 sticky top-8"
|
|
229
|
+
>
|
|
230
|
+
<h2 className="text-xl font-bold mb-6 flex items-center gap-2">
|
|
231
|
+
Summary View
|
|
232
|
+
</h2>
|
|
233
|
+
|
|
234
|
+
<div className="space-y-4 mb-8">
|
|
235
|
+
<div className="flex justify-between items-center text-white/70">
|
|
236
|
+
<span className="text-sm font-medium">Subtotal</span>
|
|
237
|
+
<span className="font-bold text-white">{formatPrice(order.subTotal || 0)}</span>
|
|
238
|
+
</div>
|
|
239
|
+
<div className="flex justify-between items-center text-white/70">
|
|
240
|
+
<span className="text-sm font-medium">Shipping</span>
|
|
241
|
+
<span className="font-bold text-white">{formatPrice(order.shippingCost || 0)}</span>
|
|
242
|
+
</div>
|
|
243
|
+
<div className="flex justify-between items-center text-white/70">
|
|
244
|
+
<span className="text-sm font-medium">Tax</span>
|
|
245
|
+
<span className="font-bold text-white">{formatPrice(order.tax || 0)}</span>
|
|
246
|
+
</div>
|
|
247
|
+
{order.discountedAmount !== undefined && order.discountedAmount > 0 && (
|
|
248
|
+
<div className="flex justify-between items-center text-primary">
|
|
249
|
+
<span className="text-sm font-medium">Discount</span>
|
|
250
|
+
<span className="font-bold">-{formatPrice(order.discountedAmount)}</span>
|
|
251
|
+
</div>
|
|
252
|
+
)}
|
|
253
|
+
|
|
254
|
+
<div className="pt-4 border-t border-white/10 mt-4">
|
|
255
|
+
<div className="flex justify-between items-end">
|
|
256
|
+
<div>
|
|
257
|
+
<p className="text-xs font-black uppercase tracking-[0.2em] text-white/40 mb-1">Grand Total</p>
|
|
258
|
+
<p className="text-3xl font-black">{formatPrice(order.grandTotal || 0)}</p>
|
|
259
|
+
</div>
|
|
260
|
+
<div className="px-3 py-1 bg-white/10 rounded-lg text-[10px] font-black uppercase tracking-wider text-white/60">
|
|
261
|
+
USD
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
{order.orderStatus === 'Pending' && (
|
|
268
|
+
<div className="p-4 bg-white/5 rounded-2xl border border-white/10 mb-8">
|
|
269
|
+
<div className="flex items-start gap-3">
|
|
270
|
+
<Info className="w-5 h-5 text-primary shrink-0 mt-0.5" />
|
|
271
|
+
<div>
|
|
272
|
+
<p className="text-xs font-bold mb-1">Order is processing</p>
|
|
273
|
+
<p className="text-[11px] text-white/60 leading-relaxed">We've received your request and our pharmacists are reviewing it for safety and accuracy.</p>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
</motion.div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { PopulatedOrder } from '@/lib/Apis/models';
|
|
6
|
+
import { ReviewForm } from '@/components/ReviewForm';
|
|
7
|
+
import { ArrowLeft, Package } from 'lucide-react';
|
|
8
|
+
import { formatDistanceToNow } from 'date-fns';
|
|
9
|
+
import Image from 'next/image';
|
|
10
|
+
import { useCurrentOrders } from '@/hooks/useOrders';
|
|
11
|
+
import { useAuth } from '@/providers/AuthProvider';
|
|
12
|
+
|
|
13
|
+
interface SelectedProduct {
|
|
14
|
+
productId: string;
|
|
15
|
+
productName: string;
|
|
16
|
+
productVariantId: string;
|
|
17
|
+
variantName: string;
|
|
18
|
+
variantImage?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function OrderReviewsScreen() {
|
|
22
|
+
const router = useRouter();
|
|
23
|
+
const { isAuthenticated } = useAuth();
|
|
24
|
+
const { orders, isLoading, error, refetch } = useCurrentOrders();
|
|
25
|
+
const [selectedOrder, setSelectedOrder] = useState<PopulatedOrder | null>(null);
|
|
26
|
+
const [selectedProduct, setSelectedProduct] = useState<SelectedProduct | null>(null);
|
|
27
|
+
|
|
28
|
+
// If not authenticated, redirect to login
|
|
29
|
+
React.useEffect(() => {
|
|
30
|
+
if (!isLoading && !isAuthenticated) {
|
|
31
|
+
router.push('/login');
|
|
32
|
+
}
|
|
33
|
+
}, [isAuthenticated, isLoading, router]);
|
|
34
|
+
|
|
35
|
+
const handleOrderClick = (order: PopulatedOrder) => {
|
|
36
|
+
console.log('Selected order:', order.id || order._id, 'Status:', order.orderStatus);
|
|
37
|
+
console.log('Full order data:', JSON.stringify(order, null, 2));
|
|
38
|
+
setSelectedOrder(order);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleProductClick = (product: SelectedProduct) => {
|
|
42
|
+
console.log('Selected product:', product.productId, 'Variant:', product.productVariantId);
|
|
43
|
+
setSelectedProduct(product);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const handleReviewSuccess = () => {
|
|
47
|
+
console.log('Review submitted successfully!');
|
|
48
|
+
setSelectedProduct(null);
|
|
49
|
+
setSelectedOrder(null);
|
|
50
|
+
refetch();
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (isLoading) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="container mx-auto px-4 py-8">
|
|
56
|
+
<div className="max-w-4xl mx-auto">
|
|
57
|
+
<div className="animate-pulse space-y-4">
|
|
58
|
+
<div className="h-8 bg-gray-200 rounded w-1/3" />
|
|
59
|
+
<div className="space-y-3">
|
|
60
|
+
{[1, 2, 3].map((i) => (
|
|
61
|
+
<div key={i} className="h-24 bg-gray-200 rounded" />
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (error) {
|
|
71
|
+
return (
|
|
72
|
+
<div className="container mx-auto px-4 py-8">
|
|
73
|
+
<div className="max-w-4xl mx-auto">
|
|
74
|
+
<div className="text-center py-12">
|
|
75
|
+
<p className="text-red-600 mb-4">{error.message}</p>
|
|
76
|
+
<button
|
|
77
|
+
onClick={() => router.back()}
|
|
78
|
+
className="text-[#E67E50] hover:underline"
|
|
79
|
+
>
|
|
80
|
+
Go Back
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Show review form if a product is selected
|
|
89
|
+
if (selectedProduct && selectedOrder) {
|
|
90
|
+
return (
|
|
91
|
+
<div className="container mx-auto px-4 py-8">
|
|
92
|
+
<div className="max-w-2xl mx-auto">
|
|
93
|
+
<button
|
|
94
|
+
onClick={() => setSelectedProduct(null)}
|
|
95
|
+
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-6"
|
|
96
|
+
>
|
|
97
|
+
<ArrowLeft className="size-5" />
|
|
98
|
+
Back to Products
|
|
99
|
+
</button>
|
|
100
|
+
|
|
101
|
+
<div className="bg-white rounded-lg border border-gray-200 p-6 mb-6">
|
|
102
|
+
<div className="flex items-center gap-4 mb-6">
|
|
103
|
+
{selectedProduct.variantImage && (
|
|
104
|
+
<div className="relative w-20 h-20 rounded-lg overflow-hidden bg-gray-100">
|
|
105
|
+
<Image
|
|
106
|
+
src={selectedProduct.variantImage}
|
|
107
|
+
alt={selectedProduct.productName}
|
|
108
|
+
fill
|
|
109
|
+
className="object-cover"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
<div>
|
|
114
|
+
<h3 className="font-semibold text-lg text-gray-900">
|
|
115
|
+
{selectedProduct.productName}
|
|
116
|
+
</h3>
|
|
117
|
+
<p className="text-sm text-gray-600">{selectedProduct.variantName}</p>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<ReviewForm
|
|
122
|
+
productId={selectedProduct.productId}
|
|
123
|
+
productVariantId={selectedProduct.productVariantId}
|
|
124
|
+
orderId={(() => {
|
|
125
|
+
const resolvedOrderId = selectedOrder.id || selectedOrder._id || '';
|
|
126
|
+
console.log('Passing orderId to ReviewForm:', resolvedOrderId);
|
|
127
|
+
console.log('Order userId:', selectedOrder.userId);
|
|
128
|
+
return resolvedOrderId;
|
|
129
|
+
})()}
|
|
130
|
+
onSuccess={handleReviewSuccess}
|
|
131
|
+
onCancel={() => setSelectedProduct(null)}
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Show order products if an order is selected
|
|
140
|
+
if (selectedOrder) {
|
|
141
|
+
return (
|
|
142
|
+
<div className="container mx-auto px-4 py-8">
|
|
143
|
+
<div className="max-w-4xl mx-auto">
|
|
144
|
+
<button
|
|
145
|
+
onClick={() => setSelectedOrder(null)}
|
|
146
|
+
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-6"
|
|
147
|
+
>
|
|
148
|
+
<ArrowLeft className="size-5" />
|
|
149
|
+
Back to Orders
|
|
150
|
+
</button>
|
|
151
|
+
|
|
152
|
+
<div className="bg-white rounded-lg border border-gray-200 p-6 mb-6">
|
|
153
|
+
<div className="flex items-center justify-between mb-4">
|
|
154
|
+
<div>
|
|
155
|
+
<h2 className="text-xl font-semibold text-gray-900">
|
|
156
|
+
Order #{(selectedOrder.id || selectedOrder._id || '').slice(-8)}
|
|
157
|
+
</h2>
|
|
158
|
+
<p className="text-sm text-gray-600">Status: {selectedOrder.orderStatus}</p>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div className="space-y-4">
|
|
164
|
+
{selectedOrder.items?.map((item: any, index: number) => {
|
|
165
|
+
// Debug: Log the full item structure
|
|
166
|
+
console.log('Order item structure:', JSON.stringify(item, null, 2));
|
|
167
|
+
|
|
168
|
+
// CartItemPopulated structure:
|
|
169
|
+
// - productVariantId: string (the ID we need)
|
|
170
|
+
// - productVariantData: ProductVariant object
|
|
171
|
+
// - quantity: number
|
|
172
|
+
const variantData = item.productVariantData || item.variant;
|
|
173
|
+
const variantId = item.productVariantId || item._id || item.id;
|
|
174
|
+
|
|
175
|
+
// Extract images from media array (ProductVariant uses 'media' not 'images')
|
|
176
|
+
const variantImages = variantData?.media?.map((m: any) => m.url || m.src || m) || variantData?.images || [];
|
|
177
|
+
const firstImage = variantImages[0];
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div
|
|
181
|
+
key={`${variantId}-${index}`}
|
|
182
|
+
className="bg-white rounded-lg border border-gray-200 p-4 cursor-pointer hover:border-[#E67E50] hover:shadow-md transition-all"
|
|
183
|
+
onClick={() => {
|
|
184
|
+
// Try to extract productId from various possible locations
|
|
185
|
+
const possibleProductId =
|
|
186
|
+
item.productId ||
|
|
187
|
+
item.product?._id ||
|
|
188
|
+
item.product?.id ||
|
|
189
|
+
variantData?.productId ||
|
|
190
|
+
variantData?.product?._id ||
|
|
191
|
+
variantData?.product?.id ||
|
|
192
|
+
variantId; // Fallback to variantId if no productId found
|
|
193
|
+
|
|
194
|
+
const productData = {
|
|
195
|
+
productId: possibleProductId,
|
|
196
|
+
productName: variantData?.name || item.product?.name || 'Product',
|
|
197
|
+
productVariantId: variantId,
|
|
198
|
+
variantName: variantData?.name || 'Default',
|
|
199
|
+
variantImage: firstImage,
|
|
200
|
+
};
|
|
201
|
+
console.log('Extracted product data:', productData);
|
|
202
|
+
handleProductClick(productData);
|
|
203
|
+
}}
|
|
204
|
+
>
|
|
205
|
+
<div className="flex items-center gap-4">
|
|
206
|
+
{firstImage && (
|
|
207
|
+
<div className="relative w-16 h-16 rounded-lg overflow-hidden bg-gray-100 flex-shrink-0">
|
|
208
|
+
<Image
|
|
209
|
+
src={firstImage}
|
|
210
|
+
alt={variantData?.name || 'Product'}
|
|
211
|
+
fill
|
|
212
|
+
className="object-cover"
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
<div className="flex-1">
|
|
217
|
+
<h3 className="font-medium text-gray-900">{variantData?.name || 'Product'}</h3>
|
|
218
|
+
<p className="text-sm text-gray-600">{variantData?.description || ''}</p>
|
|
219
|
+
<p className="text-xs text-gray-500 mt-1">Quantity: {item.quantity}</p>
|
|
220
|
+
</div>
|
|
221
|
+
<div className="flex items-center gap-2 text-[#E67E50]">
|
|
222
|
+
<span className="text-sm font-medium">Review Now</span>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
})}
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Show completed orders list
|
|
235
|
+
const completedOrders = orders.filter(order =>
|
|
236
|
+
order.orderStatus && ['Delivered', 'Picked Up', 'Fulfilled'].includes(order.orderStatus)
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div className="container mx-auto px-4 py-8">
|
|
241
|
+
<div className="max-w-4xl mx-auto">
|
|
242
|
+
<div className="mb-8">
|
|
243
|
+
<h1 className="text-3xl font-bold text-gray-900 mb-2">Review Your Orders</h1>
|
|
244
|
+
<p className="text-gray-600">
|
|
245
|
+
Share your experience with products you've purchased
|
|
246
|
+
</p>
|
|
247
|
+
<p className="text-xs text-gray-500 mt-2">
|
|
248
|
+
You can only review orders that have been delivered or completed. Reviews help other customers make informed decisions!
|
|
249
|
+
</p>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
{completedOrders.length === 0 ? (
|
|
253
|
+
<div className="text-center py-12 bg-white rounded-lg border border-gray-200">
|
|
254
|
+
<Package className="size-16 text-gray-300 mx-auto mb-4" />
|
|
255
|
+
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
|
256
|
+
No completed orders
|
|
257
|
+
</h3>
|
|
258
|
+
<p className="text-gray-600 mb-6">
|
|
259
|
+
Complete an order to leave a review
|
|
260
|
+
</p>
|
|
261
|
+
<button
|
|
262
|
+
onClick={() => router.push('/shop')}
|
|
263
|
+
className="px-6 py-3 bg-[#E67E50] text-white rounded-lg hover:bg-[#d66f40] transition-colors"
|
|
264
|
+
>
|
|
265
|
+
Start Shopping
|
|
266
|
+
</button>
|
|
267
|
+
</div>
|
|
268
|
+
) : (
|
|
269
|
+
<div className="space-y-4">
|
|
270
|
+
{completedOrders.map((order) => (
|
|
271
|
+
<div
|
|
272
|
+
key={order.id || order._id}
|
|
273
|
+
className="bg-white rounded-lg border border-gray-200 p-6 cursor-pointer hover:border-[#E67E50] hover:shadow-md transition-all"
|
|
274
|
+
onClick={() => handleOrderClick(order)}
|
|
275
|
+
>
|
|
276
|
+
<div className="flex items-center justify-between">
|
|
277
|
+
<div className="flex-1">
|
|
278
|
+
<div className="flex items-center gap-3 mb-2">
|
|
279
|
+
<h3 className="font-semibold text-lg text-gray-900">
|
|
280
|
+
Order #{(order.id || order._id || '').slice(-8)}
|
|
281
|
+
</h3>
|
|
282
|
+
<span className="px-2 py-1 text-xs bg-green-100 text-green-700 rounded">
|
|
283
|
+
{order.orderStatus}
|
|
284
|
+
</span>
|
|
285
|
+
</div>
|
|
286
|
+
<p className="text-sm text-gray-600 mb-1">
|
|
287
|
+
{order.items?.length || 0} {order.items?.length === 1 ? 'item' : 'items'}
|
|
288
|
+
</p>
|
|
289
|
+
{order.createdAt && (
|
|
290
|
+
<p className="text-xs text-gray-500">
|
|
291
|
+
{formatDistanceToNow(new Date(order.createdAt), { addSuffix: true })}
|
|
292
|
+
</p>
|
|
293
|
+
)}
|
|
294
|
+
</div>
|
|
295
|
+
<div className="text-right">
|
|
296
|
+
<div className="flex items-center gap-2 text-[#E67E50]">
|
|
297
|
+
<span className="text-sm font-medium">Review Products</span>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
))}
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
@@ -12,6 +12,7 @@ import { FilterChips } from '@/components/FilterChips';
|
|
|
12
12
|
import { useRouter } from 'next/navigation';
|
|
13
13
|
import { ManualOrderDTOOrderStatusEnum, PaymentPaymentStatusEnum } from '@/lib/Apis';
|
|
14
14
|
import { useBasePath } from '@/providers/BasePathProvider';
|
|
15
|
+
import { ReviewPromptBanner } from '@/components/ReviewPromptBanner';
|
|
15
16
|
|
|
16
17
|
const STATUS_FILTERS = ['All', ...Object.values(ManualOrderDTOOrderStatusEnum)];
|
|
17
18
|
const PAYMENT_FILTERS = ['All', ...Object.values(PaymentPaymentStatusEnum)];
|
|
@@ -24,7 +25,7 @@ export function OrdersScreen() {
|
|
|
24
25
|
const [page, setPage] = useState(1);
|
|
25
26
|
const [selectedFilter, setSelectedFilter] = useState<StatusFilterType>('All');
|
|
26
27
|
const [selectedPaymentFilter, setSelectedPaymentFilter] = useState<PaymentFilterType>('All');
|
|
27
|
-
const { orders, isLoading, pagination } = useOrders(
|
|
28
|
+
const { orders, isLoading, pagination, deleteOrder } = useOrders(
|
|
28
29
|
page,
|
|
29
30
|
10,
|
|
30
31
|
selectedFilter,
|
|
@@ -44,24 +45,41 @@ export function OrdersScreen() {
|
|
|
44
45
|
});
|
|
45
46
|
}, [orders, selectedFilter, selectedPaymentFilter]);
|
|
46
47
|
|
|
48
|
+
// Find the first completed order that hasn't been dismissed
|
|
49
|
+
const completedOrderForPrompt = useMemo(() => {
|
|
50
|
+
return filteredOrders.find(order =>
|
|
51
|
+
order.orderStatus && ['Delivered', 'Picked Up', 'Fulfilled'].includes(order.orderStatus)
|
|
52
|
+
);
|
|
53
|
+
}, [filteredOrders]);
|
|
54
|
+
|
|
47
55
|
const hasOrders = filteredOrders.length > 0;
|
|
48
56
|
const MAX_VISIBLE_FILTERS = 4;
|
|
49
57
|
|
|
50
58
|
return (
|
|
51
|
-
<div className="min-h-screen bg-
|
|
59
|
+
<div className="min-h-screen bg-linear-to-b from-[#F8FAFC] to-[#EBF4FB]">
|
|
52
60
|
<div className="container mx-auto px-4 py-8 max-w-6xl">
|
|
53
61
|
<motion.div
|
|
54
62
|
initial={{ opacity: 0, y: 24 }}
|
|
55
63
|
animate={{ opacity: 1, y: 0 }}
|
|
56
64
|
className="space-y-6"
|
|
57
65
|
>
|
|
58
|
-
<div className="mb-
|
|
59
|
-
<h1 className="text-
|
|
60
|
-
<p className="text-sm text-
|
|
61
|
-
|
|
66
|
+
<div className="mb-8">
|
|
67
|
+
<h1 className="text-3xl font-black text-secondary tracking-tight">Order History</h1>
|
|
68
|
+
<p className="text-sm font-medium text-muted mt-1 flex items-center gap-2">
|
|
69
|
+
<span className="w-1.5 h-1.5 rounded-full bg-accent animate-pulse" />
|
|
70
|
+
{filteredOrders.length} {filteredOrders.length === 1 ? 'record found' : 'records found'} for your account
|
|
62
71
|
</p>
|
|
63
72
|
</div>
|
|
64
73
|
|
|
74
|
+
{/* Review Prompt Banner */}
|
|
75
|
+
{!isLoading && completedOrderForPrompt && (
|
|
76
|
+
<ReviewPromptBanner
|
|
77
|
+
orderId={completedOrderForPrompt.id || completedOrderForPrompt._id || ''}
|
|
78
|
+
orderNumber={(completedOrderForPrompt.id || completedOrderForPrompt._id || '').slice(-8)}
|
|
79
|
+
itemCount={completedOrderForPrompt.items?.length || 0}
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
82
|
+
|
|
65
83
|
{/* Filters */}
|
|
66
84
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
67
85
|
<FilterChips
|
|
@@ -96,7 +114,13 @@ export function OrdersScreen() {
|
|
|
96
114
|
{isLoading ? (
|
|
97
115
|
Array.from({ length: 3 }).map((_, index) => <OrderCardSkeleton key={index} />)
|
|
98
116
|
) : hasOrders ? (
|
|
99
|
-
filteredOrders.map((order) =>
|
|
117
|
+
filteredOrders.map((order) => (
|
|
118
|
+
<OrderCard
|
|
119
|
+
key={order.id || order._id}
|
|
120
|
+
order={order}
|
|
121
|
+
onDelete={deleteOrder}
|
|
122
|
+
/>
|
|
123
|
+
))
|
|
100
124
|
) : (
|
|
101
125
|
<EmptyState
|
|
102
126
|
icon={Package}
|