@rovela-ai/sdk 0.19.3 → 0.19.5

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 (211) hide show
  1. package/dist/admin/server/admin-session.d.ts.map +1 -1
  2. package/dist/admin/server/admin-session.js +7 -0
  3. package/dist/admin/server/admin-session.js.map +1 -1
  4. package/dist/auth/server/customer-session.d.ts.map +1 -1
  5. package/dist/auth/server/customer-session.js +5 -0
  6. package/dist/auth/server/customer-session.js.map +1 -1
  7. package/dist/checkout/server/checkout-customer.d.ts.map +1 -1
  8. package/dist/checkout/server/checkout-customer.js +5 -0
  9. package/dist/checkout/server/checkout-customer.js.map +1 -1
  10. package/docs/changelog/README.md +53 -0
  11. package/docs/changelog/epoch-01-chrome-consent.md +63 -0
  12. package/docs/changelog/epoch-02-platform-commerce.md +108 -0
  13. package/docs/changelog/epoch-03-i18n-files.md +72 -0
  14. package/docs/changelog/epoch-04-emails.md +65 -0
  15. package/docs/changelog/epoch-05-admin-experience.md +98 -0
  16. package/docs/changelog/epoch-06-mobile-integrity.md +72 -0
  17. package/docs/design/admin-ledger-visual-universe.html +1961 -0
  18. package/docs/design/admin-sidebar-and-band.html +542 -0
  19. package/package.json +3 -1
  20. package/templates/store-template/.claude/skills/add-section/SKILL.md +438 -0
  21. package/templates/store-template/.claude/skills/build-feature/SKILL.md +201 -0
  22. package/templates/store-template/.claude/skills/customize-theme/SKILL.md +167 -0
  23. package/templates/store-template/.claude/skills/debug-errors/SKILL.md +68 -0
  24. package/templates/store-template/.claude/skills/frontend-design/SKILL.md +176 -0
  25. package/templates/store-template/.claude/skills/generate-images/SKILL.md +115 -0
  26. package/templates/store-template/.claude/skills/manage-products/SKILL.md +187 -0
  27. package/templates/store-template/.claude/skills/override-sdk-component/SKILL.md +154 -0
  28. package/templates/store-template/.env.example +50 -0
  29. package/templates/store-template/.mcp.json +8 -0
  30. package/templates/store-template/.rovela/blueprint.example.json +190 -0
  31. package/templates/store-template/CLAUDE.md +14 -0
  32. package/templates/store-template/app/LayoutContent.tsx +106 -0
  33. package/templates/store-template/app/[...path]/page.tsx +79 -0
  34. package/templates/store-template/app/account/addresses/page.tsx +45 -0
  35. package/templates/store-template/app/account/layout.tsx +17 -0
  36. package/templates/store-template/app/account/orders/[id]/invoice/page.tsx +277 -0
  37. package/templates/store-template/app/account/orders/[id]/page.tsx +725 -0
  38. package/templates/store-template/app/account/orders/page.tsx +153 -0
  39. package/templates/store-template/app/account/page.tsx +184 -0
  40. package/templates/store-template/app/account/profile/page.tsx +434 -0
  41. package/templates/store-template/app/admin/(dashboard)/account/page.tsx +16 -0
  42. package/templates/store-template/app/admin/(dashboard)/analytics/events/page.tsx +14 -0
  43. package/templates/store-template/app/admin/(dashboard)/analytics/layout.tsx +48 -0
  44. package/templates/store-template/app/admin/(dashboard)/analytics/page.tsx +15 -0
  45. package/templates/store-template/app/admin/(dashboard)/analytics/visitors/page.tsx +14 -0
  46. package/templates/store-template/app/admin/(dashboard)/categories/[id]/page.tsx +38 -0
  47. package/templates/store-template/app/admin/(dashboard)/categories/new/page.tsx +35 -0
  48. package/templates/store-template/app/admin/(dashboard)/categories/page.tsx +223 -0
  49. package/templates/store-template/app/admin/(dashboard)/customers/[id]/page.tsx +26 -0
  50. package/templates/store-template/app/admin/(dashboard)/customers/page.tsx +33 -0
  51. package/templates/store-template/app/admin/(dashboard)/discounts/page.tsx +16 -0
  52. package/templates/store-template/app/admin/(dashboard)/emails/page.tsx +16 -0
  53. package/templates/store-template/app/admin/(dashboard)/files/page.tsx +16 -0
  54. package/templates/store-template/app/admin/(dashboard)/layout.tsx +44 -0
  55. package/templates/store-template/app/admin/(dashboard)/loading.tsx +88 -0
  56. package/templates/store-template/app/admin/(dashboard)/locations/page.tsx +16 -0
  57. package/templates/store-template/app/admin/(dashboard)/orders/[id]/page.tsx +66 -0
  58. package/templates/store-template/app/admin/(dashboard)/orders/page.tsx +26 -0
  59. package/templates/store-template/app/admin/(dashboard)/page.tsx +146 -0
  60. package/templates/store-template/app/admin/(dashboard)/payments/page.tsx +15 -0
  61. package/templates/store-template/app/admin/(dashboard)/products/[id]/page.tsx +38 -0
  62. package/templates/store-template/app/admin/(dashboard)/products/new/page.tsx +35 -0
  63. package/templates/store-template/app/admin/(dashboard)/products/page.tsx +31 -0
  64. package/templates/store-template/app/admin/(dashboard)/settings/page.tsx +46 -0
  65. package/templates/store-template/app/admin/(dashboard)/shipping/page.tsx +15 -0
  66. package/templates/store-template/app/admin/(dashboard)/tax/page.tsx +15 -0
  67. package/templates/store-template/app/admin/(dashboard)/users/[id]/page.tsx +114 -0
  68. package/templates/store-template/app/admin/(dashboard)/users/page.tsx +16 -0
  69. package/templates/store-template/app/admin/accept-invite/page.tsx +36 -0
  70. package/templates/store-template/app/admin/forgot-password/page.tsx +13 -0
  71. package/templates/store-template/app/admin/layout.tsx +47 -0
  72. package/templates/store-template/app/admin/login/page.tsx +528 -0
  73. package/templates/store-template/app/admin/reset-password/page.tsx +36 -0
  74. package/templates/store-template/app/api/account/addresses/[id]/route.ts +10 -0
  75. package/templates/store-template/app/api/account/addresses/route.ts +10 -0
  76. package/templates/store-template/app/api/account/change-password/route.ts +107 -0
  77. package/templates/store-template/app/api/account/orders/[id]/refund/route.ts +11 -0
  78. package/templates/store-template/app/api/account/orders/[id]/return/route.ts +14 -0
  79. package/templates/store-template/app/api/account/orders/[id]/route.ts +42 -0
  80. package/templates/store-template/app/api/account/orders/route.ts +29 -0
  81. package/templates/store-template/app/api/account/profile/route.ts +165 -0
  82. package/templates/store-template/app/api/admin/accept-invite/route.ts +11 -0
  83. package/templates/store-template/app/api/admin/analytics/events/route.ts +9 -0
  84. package/templates/store-template/app/api/admin/analytics/route.ts +10 -0
  85. package/templates/store-template/app/api/admin/analytics/visitors/route.ts +9 -0
  86. package/templates/store-template/app/api/admin/branding/favicon/route.ts +9 -0
  87. package/templates/store-template/app/api/admin/branding/logo/route.ts +9 -0
  88. package/templates/store-template/app/api/admin/categories/[id]/route.ts +9 -0
  89. package/templates/store-template/app/api/admin/categories/route.ts +9 -0
  90. package/templates/store-template/app/api/admin/check/route.ts +10 -0
  91. package/templates/store-template/app/api/admin/customer-groups/[id]/route.ts +10 -0
  92. package/templates/store-template/app/api/admin/customer-groups/route.ts +10 -0
  93. package/templates/store-template/app/api/admin/customers/[id]/addresses/[addressId]/route.ts +15 -0
  94. package/templates/store-template/app/api/admin/customers/[id]/addresses/route.ts +15 -0
  95. package/templates/store-template/app/api/admin/customers/[id]/route.ts +11 -0
  96. package/templates/store-template/app/api/admin/customers/bulk/route.ts +8 -0
  97. package/templates/store-template/app/api/admin/customers/route.ts +9 -0
  98. package/templates/store-template/app/api/admin/discounts/[id]/route.ts +11 -0
  99. package/templates/store-template/app/api/admin/discounts/route.ts +10 -0
  100. package/templates/store-template/app/api/admin/emails/preview/route.ts +4 -0
  101. package/templates/store-template/app/api/admin/emails/route.ts +5 -0
  102. package/templates/store-template/app/api/admin/emails/test/route.ts +4 -0
  103. package/templates/store-template/app/api/admin/files/route.ts +8 -0
  104. package/templates/store-template/app/api/admin/forgot-password/route.ts +7 -0
  105. package/templates/store-template/app/api/admin/linked-customer/route.ts +11 -0
  106. package/templates/store-template/app/api/admin/locations/[id]/route.ts +10 -0
  107. package/templates/store-template/app/api/admin/locations/route.ts +10 -0
  108. package/templates/store-template/app/api/admin/me/password/route.ts +13 -0
  109. package/templates/store-template/app/api/admin/me/route.ts +11 -0
  110. package/templates/store-template/app/api/admin/orders/[id]/refund/route.ts +7 -0
  111. package/templates/store-template/app/api/admin/orders/[id]/return/approve/route.ts +7 -0
  112. package/templates/store-template/app/api/admin/orders/[id]/return/reject/route.ts +7 -0
  113. package/templates/store-template/app/api/admin/orders/[id]/route.ts +9 -0
  114. package/templates/store-template/app/api/admin/orders/[id]/ship-with-shippo/route.ts +6 -0
  115. package/templates/store-template/app/api/admin/orders/[id]/shippo-rates/route.ts +6 -0
  116. package/templates/store-template/app/api/admin/orders/bulk/route.ts +8 -0
  117. package/templates/store-template/app/api/admin/orders/route.ts +9 -0
  118. package/templates/store-template/app/api/admin/payment-methods/[id]/route.ts +10 -0
  119. package/templates/store-template/app/api/admin/payment-methods/route.ts +10 -0
  120. package/templates/store-template/app/api/admin/products/[id]/route.ts +9 -0
  121. package/templates/store-template/app/api/admin/products/bulk/route.ts +9 -0
  122. package/templates/store-template/app/api/admin/products/route.ts +9 -0
  123. package/templates/store-template/app/api/admin/products/stats/route.ts +10 -0
  124. package/templates/store-template/app/api/admin/reset-password/route.ts +10 -0
  125. package/templates/store-template/app/api/admin/settings/route.ts +11 -0
  126. package/templates/store-template/app/api/admin/setup/route.ts +10 -0
  127. package/templates/store-template/app/api/admin/setup-guide/manual-mark/route.ts +13 -0
  128. package/templates/store-template/app/api/admin/setup-guide/route.ts +10 -0
  129. package/templates/store-template/app/api/admin/shipping/carriers/[id]/route.ts +9 -0
  130. package/templates/store-template/app/api/admin/shipping/carriers/route.ts +9 -0
  131. package/templates/store-template/app/api/admin/shipping/rates/[id]/route.ts +9 -0
  132. package/templates/store-template/app/api/admin/shipping/rates/route.ts +9 -0
  133. package/templates/store-template/app/api/admin/shipping/shippo/api-key/route.ts +1 -0
  134. package/templates/store-template/app/api/admin/shipping/shippo/validate/route.ts +1 -0
  135. package/templates/store-template/app/api/admin/shipping/zones/[id]/route.ts +9 -0
  136. package/templates/store-template/app/api/admin/shipping/zones/route.ts +9 -0
  137. package/templates/store-template/app/api/admin/stats/route.ts +9 -0
  138. package/templates/store-template/app/api/admin/stripe-status/route.ts +1 -0
  139. package/templates/store-template/app/api/admin/tax-zones/[id]/route.ts +7 -0
  140. package/templates/store-template/app/api/admin/tax-zones/route.ts +7 -0
  141. package/templates/store-template/app/api/admin/users/[id]/route.ts +14 -0
  142. package/templates/store-template/app/api/admin/users/route.ts +12 -0
  143. package/templates/store-template/app/api/admin-auth/[...nextauth]/route.ts +17 -0
  144. package/templates/store-template/app/api/analytics/track/route.ts +12 -0
  145. package/templates/store-template/app/api/auth/[...nextauth]/route.ts +19 -0
  146. package/templates/store-template/app/api/auth/forgot-password/route.ts +9 -0
  147. package/templates/store-template/app/api/auth/register/route.ts +9 -0
  148. package/templates/store-template/app/api/auth/resend-verification/route.ts +9 -0
  149. package/templates/store-template/app/api/auth/reset-password/route.ts +10 -0
  150. package/templates/store-template/app/api/auth/verify-email/route.ts +10 -0
  151. package/templates/store-template/app/api/categories/route.ts +10 -0
  152. package/templates/store-template/app/api/checkout/countries/route.ts +7 -0
  153. package/templates/store-template/app/api/checkout/discount/route.ts +9 -0
  154. package/templates/store-template/app/api/checkout/manual/route.ts +10 -0
  155. package/templates/store-template/app/api/checkout/payment-methods/route.ts +8 -0
  156. package/templates/store-template/app/api/checkout/pickup-locations/route.ts +8 -0
  157. package/templates/store-template/app/api/checkout/route.ts +10 -0
  158. package/templates/store-template/app/api/checkout/shipping/route.ts +21 -0
  159. package/templates/store-template/app/api/checkout/status/route.ts +10 -0
  160. package/templates/store-template/app/api/contact/route.ts +164 -0
  161. package/templates/store-template/app/api/media/presign/route.ts +7 -0
  162. package/templates/store-template/app/api/media/route.ts +7 -0
  163. package/templates/store-template/app/api/products/[slug]/route.ts +10 -0
  164. package/templates/store-template/app/api/products/route.ts +10 -0
  165. package/templates/store-template/app/api/store/settings/route.ts +12 -0
  166. package/templates/store-template/app/api/webhooks/stripe/route.ts +63 -0
  167. package/templates/store-template/app/auth/forgot-password/page.tsx +38 -0
  168. package/templates/store-template/app/auth/layout.tsx +17 -0
  169. package/templates/store-template/app/auth/reset-password/page.tsx +81 -0
  170. package/templates/store-template/app/auth/signin/page.tsx +62 -0
  171. package/templates/store-template/app/auth/signup/page.tsx +38 -0
  172. package/templates/store-template/app/auth/verify-email/page.tsx +185 -0
  173. package/templates/store-template/app/cart/page.tsx +137 -0
  174. package/templates/store-template/app/checkout/page.tsx +88 -0
  175. package/templates/store-template/app/checkout/success/page.tsx +50 -0
  176. package/templates/store-template/app/contact/layout.tsx +24 -0
  177. package/templates/store-template/app/contact/page.tsx +321 -0
  178. package/templates/store-template/app/faq/page.tsx +153 -0
  179. package/templates/store-template/app/globals.css +99 -0
  180. package/templates/store-template/app/layout.tsx +269 -0
  181. package/templates/store-template/app/not-found.tsx +35 -0
  182. package/templates/store-template/app/page.tsx +23 -0
  183. package/templates/store-template/app/privacy/page.tsx +281 -0
  184. package/templates/store-template/app/products/[slug]/ProductDetailClient.tsx +179 -0
  185. package/templates/store-template/app/products/[slug]/page.tsx +149 -0
  186. package/templates/store-template/app/products/layout.tsx +26 -0
  187. package/templates/store-template/app/products/page.tsx +102 -0
  188. package/templates/store-template/app/robots.ts +25 -0
  189. package/templates/store-template/app/sitemap.ts +76 -0
  190. package/templates/store-template/app/terms/page.tsx +313 -0
  191. package/templates/store-template/components/PathSyncBridge.tsx +61 -0
  192. package/templates/store-template/components/Providers.tsx +37 -0
  193. package/templates/store-template/components/layout/Footer.tsx +150 -0
  194. package/templates/store-template/components/layout/Header.tsx +162 -0
  195. package/templates/store-template/components/sections/.gitkeep +7 -0
  196. package/templates/store-template/components/sections/CTA.tsx +51 -0
  197. package/templates/store-template/components/sections/Features.tsx +91 -0
  198. package/templates/store-template/components/sections/Hero.tsx +85 -0
  199. package/templates/store-template/eslint.config.mjs +18 -0
  200. package/templates/store-template/lib/store-config.ts +77 -0
  201. package/templates/store-template/lib/store-policy.ts +68 -0
  202. package/templates/store-template/lib/utils.ts +21 -0
  203. package/templates/store-template/next-env.d.ts +6 -0
  204. package/templates/store-template/next.config.js +105 -0
  205. package/templates/store-template/package.json +32 -0
  206. package/templates/store-template/postcss.config.js +6 -0
  207. package/templates/store-template/proxy.ts +150 -0
  208. package/templates/store-template/public/rovela-logo.png +0 -0
  209. package/templates/store-template/tailwind.config.ts +74 -0
  210. package/templates/store-template/tsconfig.json +41 -0
  211. package/templates/store-template/types/next-auth.d.ts +32 -0
@@ -0,0 +1,725 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * Order Detail Page
5
+ *
6
+ * Shows details of a specific customer order including:
7
+ * - Order summary (number, date, status)
8
+ * - Items list with images
9
+ * - Totals (subtotal, shipping, tax, total)
10
+ * - Shipping address
11
+ * - Receipt download button
12
+ * - Tracking info if shipped
13
+ * - Request Refund button (for orders not yet shipped)
14
+ * - Request Return button (for delivered orders)
15
+ * - Refund status display
16
+ * - Return request status display
17
+ */
18
+
19
+ import { useState, useEffect, useCallback } from 'react'
20
+ import { LocaleLink as Link, useStoreFormatting } from '@rovela-ai/sdk/core'
21
+ import { use } from 'react'
22
+ import { ChevronLeft, Package, Download, FileText, AlertTriangle, X, Loader2, RotateCcw } from 'lucide-react'
23
+ import { AuthGuard } from '@rovela-ai/sdk/auth'
24
+ import { useStoreSettings } from '@rovela-ai/sdk/core'
25
+ import { OrderTimeline } from '@rovela-ai/sdk/checkout'
26
+ import { useT, translateDynamic } from '@rovela-ai/sdk/core'
27
+
28
+ interface OrderItem {
29
+ id: string
30
+ name: string
31
+ quantity: number
32
+ price: number
33
+ attributes?: Record<string, string>
34
+ }
35
+
36
+ interface ShippingAddress {
37
+ firstName?: string
38
+ lastName?: string
39
+ name?: string
40
+ line1: string
41
+ line2?: string
42
+ city: string
43
+ state: string
44
+ postalCode: string
45
+ country: string
46
+ }
47
+
48
+ interface Order {
49
+ id: string
50
+ status: string
51
+ total: number
52
+ subtotal: number
53
+ tax: number
54
+ shipping: number
55
+ discountAmount?: number | string
56
+ discountCode?: string | null
57
+ refundAmount?: number
58
+ createdAt: string
59
+ items: OrderItem[]
60
+ shippingAddress: ShippingAddress
61
+ pickupLocationId?: string | null
62
+ trackingNumber?: string
63
+ trackingUrl?: string
64
+ carrier?: string
65
+ receiptUrl?: string
66
+ returnReason?: string
67
+ }
68
+
69
+ function OrderDetailContent({ orderId }: { orderId: string }) {
70
+ const t = useT()
71
+ // Locale-aware price and date shapes (active i18n locale + store currency).
72
+ const { formatPrice, formatDate } = useStoreFormatting()
73
+ const [order, setOrder] = useState<Order | null>(null)
74
+ // Per-order snapshot (orders.currency): the order renders in the currency
75
+ // it was PLACED in, even after the store's currency changes.
76
+ const orderCurrency = (order as { currency?: string | null } | null)?.currency
77
+ const [isLoading, setIsLoading] = useState(true)
78
+ const [error, setError] = useState<string | null>(null)
79
+
80
+ // Refund state
81
+ const [showRefundDialog, setShowRefundDialog] = useState(false)
82
+ const [isProcessingRefund, setIsProcessingRefund] = useState(false)
83
+ const [refundError, setRefundError] = useState<string | null>(null)
84
+ const [refundSuccess, setRefundSuccess] = useState<string | null>(null)
85
+
86
+ // Return state
87
+ const [showReturnDialog, setShowReturnDialog] = useState(false)
88
+ const [returnReason, setReturnReason] = useState('')
89
+ const [isProcessingReturn, setIsProcessingReturn] = useState(false)
90
+ const [returnError, setReturnError] = useState<string | null>(null)
91
+ const [returnSuccess, setReturnSuccess] = useState<string | null>(null)
92
+
93
+ const fetchOrder = useCallback(async () => {
94
+ try {
95
+ const res = await fetch(`/api/account/orders/${orderId}`)
96
+ if (res.ok) {
97
+ const data = await res.json()
98
+ setOrder(data.order)
99
+ } else if (res.status === 404) {
100
+ setError(t('account.orderNotFound'))
101
+ } else if (res.status === 403) {
102
+ setError(t('account.orderForbidden'))
103
+ } else {
104
+ setError(t('account.orderLoadFailed'))
105
+ }
106
+ } catch (err) {
107
+ console.error('Failed to fetch order:', err)
108
+ setError(t('account.orderLoadFailed'))
109
+ } finally {
110
+ setIsLoading(false)
111
+ }
112
+ }, [orderId])
113
+
114
+ useEffect(() => {
115
+ fetchOrder()
116
+ }, [fetchOrder])
117
+
118
+ // Handle refund request
119
+ const handleRefundRequest = useCallback(async () => {
120
+ setIsProcessingRefund(true)
121
+ setRefundError(null)
122
+
123
+ try {
124
+ const res = await fetch(`/api/account/orders/${orderId}/refund`, {
125
+ method: 'POST',
126
+ headers: { 'Content-Type': 'application/json' },
127
+ })
128
+
129
+ const data = await res.json()
130
+
131
+ if (res.ok && data.success) {
132
+ setRefundSuccess(data.message || t('account.refundProcessedMsg'))
133
+ setShowRefundDialog(false)
134
+ // Refresh order to show updated status
135
+ await fetchOrder()
136
+ } else {
137
+ setRefundError(data.error || t('account.refundFailed'))
138
+ }
139
+ } catch (err) {
140
+ console.error('Refund request failed:', err)
141
+ setRefundError(t('account.refundFailed'))
142
+ } finally {
143
+ setIsProcessingRefund(false)
144
+ }
145
+ }, [orderId, fetchOrder])
146
+
147
+ // Handle return request
148
+ const handleReturnRequest = useCallback(async () => {
149
+ if (!returnReason.trim()) {
150
+ setReturnError(t('account.returnReasonRequired'))
151
+ return
152
+ }
153
+
154
+ setIsProcessingReturn(true)
155
+ setReturnError(null)
156
+
157
+ try {
158
+ const res = await fetch(`/api/account/orders/${orderId}/return`, {
159
+ method: 'POST',
160
+ headers: { 'Content-Type': 'application/json' },
161
+ body: JSON.stringify({ reason: returnReason.trim() }),
162
+ })
163
+
164
+ const data = await res.json()
165
+
166
+ if (res.ok && data.success) {
167
+ setReturnSuccess(data.message || t('account.returnSubmitted'))
168
+ setShowReturnDialog(false)
169
+ setReturnReason('')
170
+ // Refresh order to show updated status
171
+ await fetchOrder()
172
+ } else {
173
+ setReturnError(data.error || t('account.returnFailed'))
174
+ }
175
+ } catch (err) {
176
+ console.error('Return request failed:', err)
177
+ setReturnError(t('account.returnFailed'))
178
+ } finally {
179
+ setIsProcessingReturn(false)
180
+ }
181
+ }, [orderId, returnReason, fetchOrder])
182
+
183
+ const getStatusStyles = (status: string) => {
184
+ switch (status) {
185
+ case 'delivered':
186
+ return 'bg-green-100 text-green-800'
187
+ case 'shipped':
188
+ return 'bg-blue-100 text-blue-800'
189
+ case 'paid':
190
+ return 'bg-yellow-100 text-yellow-800'
191
+ case 'pending':
192
+ return 'bg-gray-100 text-gray-800'
193
+ case 'cancelled':
194
+ case 'refunded':
195
+ return 'bg-red-100 text-red-800'
196
+ case 'return_requested':
197
+ return 'bg-amber-100 text-amber-800'
198
+ default:
199
+ return 'bg-gray-100 text-gray-800'
200
+ }
201
+ }
202
+
203
+ const getStatusDisplay = (status: string) => {
204
+ if (status === 'return_requested') {
205
+ return t('status.returnPending')
206
+ }
207
+ const l = translateDynamic(t, 'status', status)
208
+ return l === status ? status.charAt(0).toUpperCase() + status.slice(1) : l
209
+ }
210
+
211
+ const getAddressName = (address: ShippingAddress) => {
212
+ if (address.name) return address.name
213
+ return [address.firstName, address.lastName].filter(Boolean).join(' ') || t('account.customer')
214
+ }
215
+
216
+ // Check if order is eligible for customer-initiated refund
217
+ const canRequestRefund = order?.status === 'paid'
218
+
219
+ // Check if order is eligible for customer-initiated return
220
+ const canRequestReturn = order?.status === 'delivered'
221
+
222
+ // Check if order has a pending return request
223
+ const hasReturnRequest = order?.status === 'return_requested'
224
+
225
+ // Check if order has been refunded (partially or fully)
226
+ const hasRefund = order && order.refundAmount && order.refundAmount > 0
227
+
228
+ if (isLoading) {
229
+ return (
230
+ <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
231
+ <div className="animate-pulse space-y-6">
232
+ <div className="h-8 w-48 bg-muted rounded" />
233
+ <div className="h-32 bg-muted rounded" />
234
+ <div className="h-64 bg-muted rounded" />
235
+ </div>
236
+ </div>
237
+ )
238
+ }
239
+
240
+ if (error || !order) {
241
+ return (
242
+ <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
243
+ <nav className="mb-8">
244
+ <Link
245
+ href="/account/orders"
246
+ className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors"
247
+ >
248
+ <ChevronLeft className="h-4 w-4 mr-1" />
249
+ {t('account.backToOrders')}
250
+ </Link>
251
+ </nav>
252
+ <div className="text-center py-12">
253
+ <Package className="h-16 w-16 mx-auto text-muted-foreground" />
254
+ <h2 className="mt-6 text-xl font-semibold">{error || t('account.orderNotFound')}</h2>
255
+ <Link
256
+ href="/account/orders"
257
+ className="mt-8 inline-block rounded-md bg-primary px-8 py-3 text-sm font-semibold text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
258
+ >
259
+ {t('account.viewAllOrders')}
260
+ </Link>
261
+ </div>
262
+ </div>
263
+ )
264
+ }
265
+
266
+ return (
267
+ <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
268
+ {/* Navigation */}
269
+ <nav className="mb-8">
270
+ <Link
271
+ href="/account/orders"
272
+ className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground transition-colors"
273
+ >
274
+ <ChevronLeft className="h-4 w-4 mr-1" />
275
+ {t('account.backToOrders')}
276
+ </Link>
277
+ </nav>
278
+
279
+ {/* Refund Success Message */}
280
+ {refundSuccess && (
281
+ <div className="mb-6 rounded-lg border border-green-200 bg-green-50 p-4">
282
+ <div className="flex items-start gap-3">
283
+ <RotateCcw className="h-5 w-5 text-green-600 mt-0.5" />
284
+ <div className="flex-1">
285
+ <p className="text-sm font-medium text-green-800">{refundSuccess}</p>
286
+ </div>
287
+ <button
288
+ onClick={() => setRefundSuccess(null)}
289
+ className="text-green-600 hover:text-green-800"
290
+ >
291
+ <X className="h-4 w-4" />
292
+ </button>
293
+ </div>
294
+ </div>
295
+ )}
296
+
297
+ {/* Return Success Message */}
298
+ {returnSuccess && (
299
+ <div className="mb-6 rounded-lg border border-green-200 bg-green-50 p-4">
300
+ <div className="flex items-start gap-3">
301
+ <RotateCcw className="h-5 w-5 text-green-600 mt-0.5" />
302
+ <div className="flex-1">
303
+ <p className="text-sm font-medium text-green-800">{returnSuccess}</p>
304
+ </div>
305
+ <button
306
+ onClick={() => setReturnSuccess(null)}
307
+ className="text-green-600 hover:text-green-800"
308
+ >
309
+ <X className="h-4 w-4" />
310
+ </button>
311
+ </div>
312
+ </div>
313
+ )}
314
+
315
+ {/* Return Request Pending Banner */}
316
+ {hasReturnRequest && (
317
+ <div className="mb-6 rounded-lg border border-amber-200 bg-amber-50 p-4">
318
+ <div className="flex items-start gap-3">
319
+ <RotateCcw className="h-5 w-5 text-amber-600 mt-0.5" />
320
+ <div className="flex-1">
321
+ <p className="text-sm font-medium text-amber-800">
322
+ {t('account.returnRequestPending')}
323
+ </p>
324
+ <p className="text-xs text-amber-600 mt-1">
325
+ {t('account.returnReviewNote')}
326
+ </p>
327
+ {order.returnReason && (
328
+ <div className="mt-3 p-2 bg-white/50 rounded border border-amber-200">
329
+ <p className="text-xs text-amber-700 font-medium">{t('account.yourReason')}</p>
330
+ <p className="text-sm text-amber-800 mt-0.5">{order.returnReason}</p>
331
+ </div>
332
+ )}
333
+ </div>
334
+ </div>
335
+ </div>
336
+ )}
337
+
338
+ {/* Header */}
339
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-8">
340
+ <div>
341
+ <h1 className="text-3xl font-bold font-heading">
342
+ {t('account.orderRefNo', { ref: order.id.slice(0, 8).toUpperCase() })}
343
+ </h1>
344
+ <p className="text-muted-foreground mt-1">
345
+ {t('account.placedOn', { date: formatDate(new Date(order.createdAt)) })}
346
+ </p>
347
+ </div>
348
+ <div className="flex items-center gap-3">
349
+ <span className={`inline-flex items-center rounded-full px-4 py-2 text-sm font-medium ${getStatusStyles(order.status)}`}>
350
+ {getStatusDisplay(order.status)}
351
+ </span>
352
+ {canRequestRefund && (
353
+ <button
354
+ onClick={() => {
355
+ setRefundError(null)
356
+ setShowRefundDialog(true)
357
+ }}
358
+ className="inline-flex items-center gap-2 rounded-md border border-red-200 bg-white px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-50 transition-colors"
359
+ >
360
+ <RotateCcw className="h-4 w-4" />
361
+ {t('account.requestRefund')}
362
+ </button>
363
+ )}
364
+ {canRequestReturn && (
365
+ <button
366
+ onClick={() => {
367
+ setReturnError(null)
368
+ setReturnReason('')
369
+ setShowReturnDialog(true)
370
+ }}
371
+ className="inline-flex items-center gap-2 rounded-md border border-amber-200 bg-white px-4 py-2 text-sm font-medium text-amber-600 hover:bg-amber-50 transition-colors"
372
+ >
373
+ <RotateCcw className="h-4 w-4" />
374
+ {t('account.requestReturn')}
375
+ </button>
376
+ )}
377
+ <Link
378
+ href={`/account/orders/${order.id}/invoice`}
379
+ className="inline-flex items-center gap-2 rounded-md border bg-background px-4 py-2 text-sm font-medium hover:bg-muted transition-colors"
380
+ >
381
+ <FileText className="h-4 w-4" />
382
+ {t('account.viewInvoice')}
383
+ </Link>
384
+ </div>
385
+ </div>
386
+
387
+ {/* Refund Status Banner */}
388
+ {hasRefund && (
389
+ <div className="mb-6 rounded-lg border border-amber-200 bg-amber-50 p-4">
390
+ <div className="flex items-center gap-3">
391
+ <RotateCcw className="h-5 w-5 text-amber-600" />
392
+ <div>
393
+ <p className="text-sm font-medium text-amber-800">
394
+ {t('account.refundProcessedAmount', { amount: formatPrice(order.refundAmount!, orderCurrency) })}
395
+ </p>
396
+ <p className="text-xs text-amber-600 mt-0.5">
397
+ {t('account.refundTiming')}
398
+ </p>
399
+ </div>
400
+ </div>
401
+ </div>
402
+ )}
403
+
404
+ <div className="grid gap-8 lg:grid-cols-3">
405
+ {/* Main Content */}
406
+ <div className="lg:col-span-2 space-y-8">
407
+ {/* Order status timeline */}
408
+ <div className="rounded-lg border bg-card p-6">
409
+ <h2 className="font-semibold mb-6">{t('account.orderStatus')}</h2>
410
+ <OrderTimeline
411
+ status={order.status}
412
+ trackingNumber={order.trackingNumber}
413
+ trackingUrl={order.trackingUrl}
414
+ carrier={order.carrier}
415
+ />
416
+ </div>
417
+
418
+ {/* Order Items */}
419
+ <div className="rounded-lg border bg-card overflow-hidden">
420
+ <div className="px-6 py-4 border-b bg-muted/30">
421
+ <h2 className="font-semibold">{t('account.orderItems')}</h2>
422
+ </div>
423
+ <div className="divide-y">
424
+ {order.items.map((item, index) => (
425
+ <div key={item.id || index} className="flex items-center gap-4 px-6 py-4">
426
+ <div className="h-16 w-16 rounded-lg bg-muted flex items-center justify-center flex-shrink-0">
427
+ <Package className="h-6 w-6 text-muted-foreground" />
428
+ </div>
429
+ <div className="flex-1 min-w-0">
430
+ <p className="font-medium truncate">{item.name}</p>
431
+ {item.attributes && Object.keys(item.attributes).length > 0 && (
432
+ <p className="text-sm text-muted-foreground">
433
+ {Object.entries(item.attributes).map(([k, v]) => `${k}: ${v}`).join(', ')}
434
+ </p>
435
+ )}
436
+ <p className="text-sm text-muted-foreground">{t('account.qty', { count: item.quantity })}</p>
437
+ </div>
438
+ <div className="text-right">
439
+ <p className="font-medium">{formatPrice(item.price, orderCurrency)}</p>
440
+ </div>
441
+ </div>
442
+ ))}
443
+ </div>
444
+ </div>
445
+
446
+ </div>
447
+
448
+ {/* Sidebar */}
449
+ <div className="space-y-6">
450
+ {/* Order Summary */}
451
+ <div className="rounded-lg border bg-card p-6">
452
+ <h2 className="font-semibold mb-4">{t('account.orderSummary')}</h2>
453
+ <div className="space-y-2 text-sm">
454
+ <div className="flex justify-between">
455
+ <span className="text-muted-foreground">{t('common.subtotal')}</span>
456
+ <span>{formatPrice(order.subtotal, orderCurrency)}</span>
457
+ </div>
458
+ {Number(order.discountAmount ?? 0) > 0 && (
459
+ <div className="flex justify-between text-emerald-600">
460
+ <span>{t('common.discount')}{order.discountCode ? ` (${order.discountCode})` : ''}</span>
461
+ <span>-{formatPrice(order.discountAmount!, orderCurrency)}</span>
462
+ </div>
463
+ )}
464
+ {order.tax > 0 && (
465
+ <div className="flex justify-between">
466
+ <span className="text-muted-foreground">{t('common.tax')}</span>
467
+ <span>{formatPrice(order.tax, orderCurrency)}</span>
468
+ </div>
469
+ )}
470
+ {order.shipping > 0 && (
471
+ <div className="flex justify-between">
472
+ <span className="text-muted-foreground">{t('common.shipping')}</span>
473
+ <span>{formatPrice(order.shipping, orderCurrency)}</span>
474
+ </div>
475
+ )}
476
+ <div className="pt-2 border-t">
477
+ <div className="flex justify-between font-medium">
478
+ <span>Total</span>
479
+ <span>{formatPrice(order.total, orderCurrency)}</span>
480
+ </div>
481
+ </div>
482
+ {hasRefund && (
483
+ <div className="pt-2 border-t">
484
+ <div className="flex justify-between text-green-600 font-medium">
485
+ <span>{t('account.refunded')}</span>
486
+ <span>-{formatPrice(order.refundAmount!, orderCurrency)}</span>
487
+ </div>
488
+ </div>
489
+ )}
490
+ </div>
491
+ </div>
492
+
493
+ {/* Shipping Address */}
494
+ {order.shippingAddress && (
495
+ <div className="rounded-lg border bg-card p-6">
496
+ <h2 className="font-semibold mb-4">
497
+ {order.pickupLocationId ? t('account.pickupLocation') : t('account.shippingAddress')}
498
+ </h2>
499
+ <div className="text-sm text-muted-foreground space-y-1">
500
+ <p className="text-foreground font-medium">{getAddressName(order.shippingAddress)}</p>
501
+ <p>{order.shippingAddress.line1}</p>
502
+ {order.shippingAddress.line2 && <p>{order.shippingAddress.line2}</p>}
503
+ <p>
504
+ {order.shippingAddress.city}, {order.shippingAddress.state} {order.shippingAddress.postalCode}
505
+ </p>
506
+ <p>{order.shippingAddress.country}</p>
507
+ </div>
508
+ </div>
509
+ )}
510
+
511
+ {/* Receipt Download */}
512
+ {order.receiptUrl && (
513
+ <div className="rounded-lg border bg-card p-6">
514
+ <h2 className="font-semibold mb-4">{t('account.receipt')}</h2>
515
+ <a
516
+ href={order.receiptUrl}
517
+ target="_blank"
518
+ rel="noopener noreferrer"
519
+ className="w-full inline-flex items-center justify-center gap-2 rounded-md bg-primary px-4 py-3 text-sm font-semibold text-primary-foreground shadow-sm hover:bg-primary/90 transition-colors"
520
+ >
521
+ <Download className="h-4 w-4" />
522
+ {t('account.downloadReceipt')}
523
+ </a>
524
+ <p className="mt-2 text-xs text-muted-foreground text-center">
525
+ {t('account.receiptHint')}
526
+ </p>
527
+ </div>
528
+ )}
529
+ </div>
530
+ </div>
531
+
532
+ {/* Refund Confirmation Dialog */}
533
+ {showRefundDialog && (
534
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
535
+ {/* Backdrop */}
536
+ <div
537
+ className="absolute inset-0 bg-black/50"
538
+ onClick={() => !isProcessingRefund && setShowRefundDialog(false)}
539
+ />
540
+
541
+ {/* Dialog */}
542
+ <div className="relative w-full max-w-md mx-4 bg-card rounded-lg shadow-xl">
543
+ {/* Header */}
544
+ <div className="flex items-start gap-4 p-6 border-b">
545
+ <div className="flex-shrink-0 w-10 h-10 rounded-full bg-amber-100 flex items-center justify-center">
546
+ <AlertTriangle className="h-5 w-5 text-amber-600" />
547
+ </div>
548
+ <div className="flex-1">
549
+ <h2 className="text-lg font-semibold">{t('account.requestRefund')}</h2>
550
+ <p className="text-sm text-muted-foreground mt-1">
551
+ {t('account.refundConfirmQ')}
552
+ </p>
553
+ </div>
554
+ <button
555
+ onClick={() => setShowRefundDialog(false)}
556
+ disabled={isProcessingRefund}
557
+ className="text-muted-foreground hover:text-foreground"
558
+ >
559
+ <X className="h-5 w-5" />
560
+ </button>
561
+ </div>
562
+
563
+ {/* Content */}
564
+ <div className="p-6 space-y-4">
565
+ {refundError && (
566
+ <div className="rounded-md bg-red-50 border border-red-200 p-3">
567
+ <p className="text-sm text-red-700">{refundError}</p>
568
+ </div>
569
+ )}
570
+
571
+ <div className="rounded-md bg-muted/50 p-4">
572
+ <p className="text-sm text-muted-foreground mb-1">{t('account.refundAmount')}</p>
573
+ <p className="text-2xl font-bold">{formatPrice(order.total, orderCurrency)}</p>
574
+ <p className="text-xs text-muted-foreground mt-1">
575
+ {t('account.refundMethodNote')}
576
+ </p>
577
+ </div>
578
+
579
+ <p className="text-sm text-muted-foreground">
580
+ {t('account.refundProcessingNote')}
581
+ </p>
582
+ </div>
583
+
584
+ {/* Footer */}
585
+ <div className="flex gap-3 p-6 border-t bg-muted/30">
586
+ <button
587
+ onClick={() => setShowRefundDialog(false)}
588
+ disabled={isProcessingRefund}
589
+ className="flex-1 rounded-md border bg-background px-4 py-2.5 text-sm font-medium hover:bg-muted transition-colors disabled:opacity-50"
590
+ >
591
+ {t('account.cancel')}
592
+ </button>
593
+ <button
594
+ onClick={handleRefundRequest}
595
+ disabled={isProcessingRefund}
596
+ className="flex-1 rounded-md bg-red-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-red-700 transition-colors disabled:opacity-50 inline-flex items-center justify-center gap-2"
597
+ >
598
+ {isProcessingRefund ? (
599
+ <>
600
+ <Loader2 className="h-4 w-4 animate-spin" />
601
+ {t('account.processing')}
602
+ </>
603
+ ) : (
604
+ t('account.confirmRefund')
605
+ )}
606
+ </button>
607
+ </div>
608
+ </div>
609
+ </div>
610
+ )}
611
+
612
+ {/* Return Request Confirmation Dialog */}
613
+ {showReturnDialog && (
614
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
615
+ {/* Backdrop */}
616
+ <div
617
+ className="absolute inset-0 bg-black/50"
618
+ onClick={() => !isProcessingReturn && setShowReturnDialog(false)}
619
+ />
620
+
621
+ {/* Dialog */}
622
+ <div className="relative w-full max-w-md mx-4 bg-card rounded-lg shadow-xl">
623
+ {/* Header */}
624
+ <div className="flex items-start gap-4 p-6 border-b">
625
+ <div className="flex-shrink-0 w-10 h-10 rounded-full bg-amber-100 flex items-center justify-center">
626
+ <RotateCcw className="h-5 w-5 text-amber-600" />
627
+ </div>
628
+ <div className="flex-1">
629
+ <h2 className="text-lg font-semibold">{t('account.requestReturn')}</h2>
630
+ <p className="text-sm text-muted-foreground mt-1">
631
+ {t('account.returnSubtitle')}
632
+ </p>
633
+ </div>
634
+ <button
635
+ onClick={() => setShowReturnDialog(false)}
636
+ disabled={isProcessingReturn}
637
+ className="text-muted-foreground hover:text-foreground"
638
+ >
639
+ <X className="h-5 w-5" />
640
+ </button>
641
+ </div>
642
+
643
+ {/* Content */}
644
+ <div className="p-6 space-y-4">
645
+ {returnError && (
646
+ <div className="rounded-md bg-red-50 border border-red-200 p-3">
647
+ <p className="text-sm text-red-700">{returnError}</p>
648
+ </div>
649
+ )}
650
+
651
+ <div className="rounded-md bg-muted/50 p-4">
652
+ <p className="text-sm text-muted-foreground mb-1">{t('account.orderTotal')}</p>
653
+ <p className="text-2xl font-bold">{formatPrice(order.total, orderCurrency)}</p>
654
+ <p className="text-xs text-muted-foreground mt-1">
655
+ {t('account.returnApprovedNote')}
656
+ </p>
657
+ </div>
658
+
659
+ <div>
660
+ <label htmlFor="returnReason" className="block text-sm font-medium mb-2">
661
+ {t('account.reasonForReturn')} <span className="text-red-500">*</span>
662
+ </label>
663
+ <textarea
664
+ id="returnReason"
665
+ value={returnReason}
666
+ onChange={(e) => setReturnReason(e.target.value)}
667
+ placeholder={t('account.returnReasonPlaceholder')}
668
+ rows={3}
669
+ maxLength={500}
670
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent resize-none"
671
+ />
672
+ <p className="text-xs text-muted-foreground mt-1">
673
+ {t('account.charCount', { count: returnReason.length })}
674
+ </p>
675
+ </div>
676
+
677
+ <p className="text-sm text-muted-foreground">
678
+ {t('account.returnReviewEmailNote')}
679
+ </p>
680
+ </div>
681
+
682
+ {/* Footer */}
683
+ <div className="flex gap-3 p-6 border-t bg-muted/30">
684
+ <button
685
+ onClick={() => setShowReturnDialog(false)}
686
+ disabled={isProcessingReturn}
687
+ className="flex-1 rounded-md border bg-background px-4 py-2.5 text-sm font-medium hover:bg-muted transition-colors disabled:opacity-50"
688
+ >
689
+ {t('account.cancel')}
690
+ </button>
691
+ <button
692
+ onClick={handleReturnRequest}
693
+ disabled={isProcessingReturn || !returnReason.trim()}
694
+ className="flex-1 rounded-md bg-amber-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-amber-700 transition-colors disabled:opacity-50 inline-flex items-center justify-center gap-2"
695
+ >
696
+ {isProcessingReturn ? (
697
+ <>
698
+ <Loader2 className="h-4 w-4 animate-spin" />
699
+ {t('account.submitting')}
700
+ </>
701
+ ) : (
702
+ t('account.submitRequest')
703
+ )}
704
+ </button>
705
+ </div>
706
+ </div>
707
+ </div>
708
+ )}
709
+ </div>
710
+ )
711
+ }
712
+
713
+ export default function OrderDetailPage({
714
+ params,
715
+ }: {
716
+ params: Promise<{ id: string }>
717
+ }) {
718
+ const { id } = use(params)
719
+
720
+ return (
721
+ <AuthGuard redirectTo="/auth/signin">
722
+ <OrderDetailContent orderId={id} />
723
+ </AuthGuard>
724
+ )
725
+ }