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.
Files changed (76) hide show
  1. package/dist/index.d.mts +1451 -1303
  2. package/dist/index.d.ts +1451 -1303
  3. package/dist/index.js +10502 -5728
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +7817 -3059
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +4 -3
  8. package/src/components/AccountReviewsTab.tsx +97 -0
  9. package/src/components/CouponCodeInput.tsx +190 -0
  10. package/src/components/Header.tsx +5 -1
  11. package/src/components/Notification.tsx +1 -1
  12. package/src/components/NotificationBell.tsx +33 -0
  13. package/src/components/NotificationCard.tsx +211 -0
  14. package/src/components/NotificationDrawer.tsx +195 -0
  15. package/src/components/OrderCard.tsx +164 -99
  16. package/src/components/ProductReviewsSection.tsx +30 -0
  17. package/src/components/RatingDistribution.tsx +86 -0
  18. package/src/components/ReviewCard.tsx +59 -0
  19. package/src/components/ReviewForm.tsx +207 -0
  20. package/src/components/ReviewPromptBanner.tsx +98 -0
  21. package/src/components/ReviewsList.tsx +151 -0
  22. package/src/components/StarRating.tsx +98 -0
  23. package/src/hooks/useDiscounts.ts +7 -0
  24. package/src/hooks/useOrders.ts +15 -0
  25. package/src/hooks/useReviews.ts +230 -0
  26. package/src/index.ts +25 -0
  27. package/src/lib/Apis/apis/discounts-api.ts +23 -72
  28. package/src/lib/Apis/apis/notifications-api.ts +196 -231
  29. package/src/lib/Apis/apis/products-api.ts +84 -0
  30. package/src/lib/Apis/apis/review-api.ts +283 -4
  31. package/src/lib/Apis/apis/stores-api.ts +180 -0
  32. package/src/lib/Apis/models/bulk-channel-toggle-dto.ts +52 -0
  33. package/src/lib/Apis/models/cart-body-populated.ts +3 -3
  34. package/src/lib/Apis/models/channel-settings-dto.ts +39 -0
  35. package/src/lib/Apis/models/{discount-paginated-response.ts → completed-order-dto.ts} +21 -16
  36. package/src/lib/Apis/models/create-discount-dto.ts +31 -92
  37. package/src/lib/Apis/models/create-review-dto.ts +4 -4
  38. package/src/lib/Apis/models/create-shippo-account-dto.ts +45 -0
  39. package/src/lib/Apis/models/create-store-dto.ts +6 -0
  40. package/src/lib/Apis/models/discount.ts +37 -98
  41. package/src/lib/Apis/models/discounts-insights-dto.ts +12 -0
  42. package/src/lib/Apis/models/index.ts +13 -7
  43. package/src/lib/Apis/models/{manual-discount.ts → manual-discount-dto.ts} +10 -10
  44. package/src/lib/Apis/models/manual-order-dto.ts +3 -3
  45. package/src/lib/Apis/models/populated-discount.ts +41 -101
  46. package/src/lib/Apis/models/preference-update-item.ts +59 -0
  47. package/src/lib/Apis/models/product-light-dto.ts +40 -0
  48. package/src/lib/Apis/models/{check-notifications-response-dto.ts → review-status-dto.ts} +8 -7
  49. package/src/lib/Apis/models/review.ts +9 -3
  50. package/src/lib/Apis/models/reviewable-order-dto.ts +58 -0
  51. package/src/lib/Apis/models/reviewable-product-dto.ts +81 -0
  52. package/src/lib/Apis/models/shippo-account-response-dto.ts +51 -0
  53. package/src/lib/Apis/models/store-entity.ts +6 -0
  54. package/src/lib/Apis/models/store.ts +6 -0
  55. package/src/lib/Apis/models/update-discount-dto.ts +31 -92
  56. package/src/lib/Apis/models/update-notification-settings-dto.ts +28 -0
  57. package/src/lib/Apis/models/update-review-dto.ts +4 -4
  58. package/src/lib/Apis/models/update-store-dto.ts +6 -0
  59. package/src/lib/Apis/models/{pick-type-class.ts → variant-light-dto.ts} +20 -14
  60. package/src/lib/utils/discount.ts +155 -0
  61. package/src/lib/validations/discount.ts +11 -0
  62. package/src/providers/CartProvider.tsx +2 -2
  63. package/src/providers/DiscountProvider.tsx +97 -0
  64. package/src/providers/EcommerceProvider.tsx +13 -5
  65. package/src/providers/NotificationCenterProvider.tsx +436 -0
  66. package/src/screens/CartScreen.tsx +1 -1
  67. package/src/screens/CheckoutScreen.tsx +39 -12
  68. package/src/screens/NotificationSettingsScreen.tsx +413 -0
  69. package/src/screens/OrderDetailScreen.tsx +283 -0
  70. package/src/screens/OrderReviewsScreen.tsx +308 -0
  71. package/src/screens/OrdersScreen.tsx +31 -7
  72. package/src/screens/ProductDetailScreen.tsx +24 -11
  73. package/src/screens/ProfileScreen.tsx +5 -0
  74. package/src/lib/Apis/models/create-notification-dto.ts +0 -75
  75. package/src/lib/Apis/models/notification.ts +0 -93
  76. 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-white">
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-6">
59
- <h1 className="text-2xl font-bold text-slate-900">Orders</h1>
60
- <p className="text-sm text-gray-500 mt-1">
61
- {filteredOrders.length} {filteredOrders.length === 1 ? 'order' : 'orders'}
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) => <OrderCard key={order.id} order={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}