@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.
- package/dist/admin/server/admin-session.d.ts.map +1 -1
- package/dist/admin/server/admin-session.js +7 -0
- package/dist/admin/server/admin-session.js.map +1 -1
- package/dist/auth/server/customer-session.d.ts.map +1 -1
- package/dist/auth/server/customer-session.js +5 -0
- package/dist/auth/server/customer-session.js.map +1 -1
- package/dist/checkout/server/checkout-customer.d.ts.map +1 -1
- package/dist/checkout/server/checkout-customer.js +5 -0
- package/dist/checkout/server/checkout-customer.js.map +1 -1
- package/docs/changelog/README.md +53 -0
- package/docs/changelog/epoch-01-chrome-consent.md +63 -0
- package/docs/changelog/epoch-02-platform-commerce.md +108 -0
- package/docs/changelog/epoch-03-i18n-files.md +72 -0
- package/docs/changelog/epoch-04-emails.md +65 -0
- package/docs/changelog/epoch-05-admin-experience.md +98 -0
- package/docs/changelog/epoch-06-mobile-integrity.md +72 -0
- package/docs/design/admin-ledger-visual-universe.html +1961 -0
- package/docs/design/admin-sidebar-and-band.html +542 -0
- package/package.json +3 -1
- package/templates/store-template/.claude/skills/add-section/SKILL.md +438 -0
- package/templates/store-template/.claude/skills/build-feature/SKILL.md +201 -0
- package/templates/store-template/.claude/skills/customize-theme/SKILL.md +167 -0
- package/templates/store-template/.claude/skills/debug-errors/SKILL.md +68 -0
- package/templates/store-template/.claude/skills/frontend-design/SKILL.md +176 -0
- package/templates/store-template/.claude/skills/generate-images/SKILL.md +115 -0
- package/templates/store-template/.claude/skills/manage-products/SKILL.md +187 -0
- package/templates/store-template/.claude/skills/override-sdk-component/SKILL.md +154 -0
- package/templates/store-template/.env.example +50 -0
- package/templates/store-template/.mcp.json +8 -0
- package/templates/store-template/.rovela/blueprint.example.json +190 -0
- package/templates/store-template/CLAUDE.md +14 -0
- package/templates/store-template/app/LayoutContent.tsx +106 -0
- package/templates/store-template/app/[...path]/page.tsx +79 -0
- package/templates/store-template/app/account/addresses/page.tsx +45 -0
- package/templates/store-template/app/account/layout.tsx +17 -0
- package/templates/store-template/app/account/orders/[id]/invoice/page.tsx +277 -0
- package/templates/store-template/app/account/orders/[id]/page.tsx +725 -0
- package/templates/store-template/app/account/orders/page.tsx +153 -0
- package/templates/store-template/app/account/page.tsx +184 -0
- package/templates/store-template/app/account/profile/page.tsx +434 -0
- package/templates/store-template/app/admin/(dashboard)/account/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/analytics/events/page.tsx +14 -0
- package/templates/store-template/app/admin/(dashboard)/analytics/layout.tsx +48 -0
- package/templates/store-template/app/admin/(dashboard)/analytics/page.tsx +15 -0
- package/templates/store-template/app/admin/(dashboard)/analytics/visitors/page.tsx +14 -0
- package/templates/store-template/app/admin/(dashboard)/categories/[id]/page.tsx +38 -0
- package/templates/store-template/app/admin/(dashboard)/categories/new/page.tsx +35 -0
- package/templates/store-template/app/admin/(dashboard)/categories/page.tsx +223 -0
- package/templates/store-template/app/admin/(dashboard)/customers/[id]/page.tsx +26 -0
- package/templates/store-template/app/admin/(dashboard)/customers/page.tsx +33 -0
- package/templates/store-template/app/admin/(dashboard)/discounts/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/emails/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/files/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/layout.tsx +44 -0
- package/templates/store-template/app/admin/(dashboard)/loading.tsx +88 -0
- package/templates/store-template/app/admin/(dashboard)/locations/page.tsx +16 -0
- package/templates/store-template/app/admin/(dashboard)/orders/[id]/page.tsx +66 -0
- package/templates/store-template/app/admin/(dashboard)/orders/page.tsx +26 -0
- package/templates/store-template/app/admin/(dashboard)/page.tsx +146 -0
- package/templates/store-template/app/admin/(dashboard)/payments/page.tsx +15 -0
- package/templates/store-template/app/admin/(dashboard)/products/[id]/page.tsx +38 -0
- package/templates/store-template/app/admin/(dashboard)/products/new/page.tsx +35 -0
- package/templates/store-template/app/admin/(dashboard)/products/page.tsx +31 -0
- package/templates/store-template/app/admin/(dashboard)/settings/page.tsx +46 -0
- package/templates/store-template/app/admin/(dashboard)/shipping/page.tsx +15 -0
- package/templates/store-template/app/admin/(dashboard)/tax/page.tsx +15 -0
- package/templates/store-template/app/admin/(dashboard)/users/[id]/page.tsx +114 -0
- package/templates/store-template/app/admin/(dashboard)/users/page.tsx +16 -0
- package/templates/store-template/app/admin/accept-invite/page.tsx +36 -0
- package/templates/store-template/app/admin/forgot-password/page.tsx +13 -0
- package/templates/store-template/app/admin/layout.tsx +47 -0
- package/templates/store-template/app/admin/login/page.tsx +528 -0
- package/templates/store-template/app/admin/reset-password/page.tsx +36 -0
- package/templates/store-template/app/api/account/addresses/[id]/route.ts +10 -0
- package/templates/store-template/app/api/account/addresses/route.ts +10 -0
- package/templates/store-template/app/api/account/change-password/route.ts +107 -0
- package/templates/store-template/app/api/account/orders/[id]/refund/route.ts +11 -0
- package/templates/store-template/app/api/account/orders/[id]/return/route.ts +14 -0
- package/templates/store-template/app/api/account/orders/[id]/route.ts +42 -0
- package/templates/store-template/app/api/account/orders/route.ts +29 -0
- package/templates/store-template/app/api/account/profile/route.ts +165 -0
- package/templates/store-template/app/api/admin/accept-invite/route.ts +11 -0
- package/templates/store-template/app/api/admin/analytics/events/route.ts +9 -0
- package/templates/store-template/app/api/admin/analytics/route.ts +10 -0
- package/templates/store-template/app/api/admin/analytics/visitors/route.ts +9 -0
- package/templates/store-template/app/api/admin/branding/favicon/route.ts +9 -0
- package/templates/store-template/app/api/admin/branding/logo/route.ts +9 -0
- package/templates/store-template/app/api/admin/categories/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/categories/route.ts +9 -0
- package/templates/store-template/app/api/admin/check/route.ts +10 -0
- package/templates/store-template/app/api/admin/customer-groups/[id]/route.ts +10 -0
- package/templates/store-template/app/api/admin/customer-groups/route.ts +10 -0
- package/templates/store-template/app/api/admin/customers/[id]/addresses/[addressId]/route.ts +15 -0
- package/templates/store-template/app/api/admin/customers/[id]/addresses/route.ts +15 -0
- package/templates/store-template/app/api/admin/customers/[id]/route.ts +11 -0
- package/templates/store-template/app/api/admin/customers/bulk/route.ts +8 -0
- package/templates/store-template/app/api/admin/customers/route.ts +9 -0
- package/templates/store-template/app/api/admin/discounts/[id]/route.ts +11 -0
- package/templates/store-template/app/api/admin/discounts/route.ts +10 -0
- package/templates/store-template/app/api/admin/emails/preview/route.ts +4 -0
- package/templates/store-template/app/api/admin/emails/route.ts +5 -0
- package/templates/store-template/app/api/admin/emails/test/route.ts +4 -0
- package/templates/store-template/app/api/admin/files/route.ts +8 -0
- package/templates/store-template/app/api/admin/forgot-password/route.ts +7 -0
- package/templates/store-template/app/api/admin/linked-customer/route.ts +11 -0
- package/templates/store-template/app/api/admin/locations/[id]/route.ts +10 -0
- package/templates/store-template/app/api/admin/locations/route.ts +10 -0
- package/templates/store-template/app/api/admin/me/password/route.ts +13 -0
- package/templates/store-template/app/api/admin/me/route.ts +11 -0
- package/templates/store-template/app/api/admin/orders/[id]/refund/route.ts +7 -0
- package/templates/store-template/app/api/admin/orders/[id]/return/approve/route.ts +7 -0
- package/templates/store-template/app/api/admin/orders/[id]/return/reject/route.ts +7 -0
- package/templates/store-template/app/api/admin/orders/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/orders/[id]/ship-with-shippo/route.ts +6 -0
- package/templates/store-template/app/api/admin/orders/[id]/shippo-rates/route.ts +6 -0
- package/templates/store-template/app/api/admin/orders/bulk/route.ts +8 -0
- package/templates/store-template/app/api/admin/orders/route.ts +9 -0
- package/templates/store-template/app/api/admin/payment-methods/[id]/route.ts +10 -0
- package/templates/store-template/app/api/admin/payment-methods/route.ts +10 -0
- package/templates/store-template/app/api/admin/products/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/products/bulk/route.ts +9 -0
- package/templates/store-template/app/api/admin/products/route.ts +9 -0
- package/templates/store-template/app/api/admin/products/stats/route.ts +10 -0
- package/templates/store-template/app/api/admin/reset-password/route.ts +10 -0
- package/templates/store-template/app/api/admin/settings/route.ts +11 -0
- package/templates/store-template/app/api/admin/setup/route.ts +10 -0
- package/templates/store-template/app/api/admin/setup-guide/manual-mark/route.ts +13 -0
- package/templates/store-template/app/api/admin/setup-guide/route.ts +10 -0
- package/templates/store-template/app/api/admin/shipping/carriers/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/carriers/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/rates/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/rates/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/shippo/api-key/route.ts +1 -0
- package/templates/store-template/app/api/admin/shipping/shippo/validate/route.ts +1 -0
- package/templates/store-template/app/api/admin/shipping/zones/[id]/route.ts +9 -0
- package/templates/store-template/app/api/admin/shipping/zones/route.ts +9 -0
- package/templates/store-template/app/api/admin/stats/route.ts +9 -0
- package/templates/store-template/app/api/admin/stripe-status/route.ts +1 -0
- package/templates/store-template/app/api/admin/tax-zones/[id]/route.ts +7 -0
- package/templates/store-template/app/api/admin/tax-zones/route.ts +7 -0
- package/templates/store-template/app/api/admin/users/[id]/route.ts +14 -0
- package/templates/store-template/app/api/admin/users/route.ts +12 -0
- package/templates/store-template/app/api/admin-auth/[...nextauth]/route.ts +17 -0
- package/templates/store-template/app/api/analytics/track/route.ts +12 -0
- package/templates/store-template/app/api/auth/[...nextauth]/route.ts +19 -0
- package/templates/store-template/app/api/auth/forgot-password/route.ts +9 -0
- package/templates/store-template/app/api/auth/register/route.ts +9 -0
- package/templates/store-template/app/api/auth/resend-verification/route.ts +9 -0
- package/templates/store-template/app/api/auth/reset-password/route.ts +10 -0
- package/templates/store-template/app/api/auth/verify-email/route.ts +10 -0
- package/templates/store-template/app/api/categories/route.ts +10 -0
- package/templates/store-template/app/api/checkout/countries/route.ts +7 -0
- package/templates/store-template/app/api/checkout/discount/route.ts +9 -0
- package/templates/store-template/app/api/checkout/manual/route.ts +10 -0
- package/templates/store-template/app/api/checkout/payment-methods/route.ts +8 -0
- package/templates/store-template/app/api/checkout/pickup-locations/route.ts +8 -0
- package/templates/store-template/app/api/checkout/route.ts +10 -0
- package/templates/store-template/app/api/checkout/shipping/route.ts +21 -0
- package/templates/store-template/app/api/checkout/status/route.ts +10 -0
- package/templates/store-template/app/api/contact/route.ts +164 -0
- package/templates/store-template/app/api/media/presign/route.ts +7 -0
- package/templates/store-template/app/api/media/route.ts +7 -0
- package/templates/store-template/app/api/products/[slug]/route.ts +10 -0
- package/templates/store-template/app/api/products/route.ts +10 -0
- package/templates/store-template/app/api/store/settings/route.ts +12 -0
- package/templates/store-template/app/api/webhooks/stripe/route.ts +63 -0
- package/templates/store-template/app/auth/forgot-password/page.tsx +38 -0
- package/templates/store-template/app/auth/layout.tsx +17 -0
- package/templates/store-template/app/auth/reset-password/page.tsx +81 -0
- package/templates/store-template/app/auth/signin/page.tsx +62 -0
- package/templates/store-template/app/auth/signup/page.tsx +38 -0
- package/templates/store-template/app/auth/verify-email/page.tsx +185 -0
- package/templates/store-template/app/cart/page.tsx +137 -0
- package/templates/store-template/app/checkout/page.tsx +88 -0
- package/templates/store-template/app/checkout/success/page.tsx +50 -0
- package/templates/store-template/app/contact/layout.tsx +24 -0
- package/templates/store-template/app/contact/page.tsx +321 -0
- package/templates/store-template/app/faq/page.tsx +153 -0
- package/templates/store-template/app/globals.css +99 -0
- package/templates/store-template/app/layout.tsx +269 -0
- package/templates/store-template/app/not-found.tsx +35 -0
- package/templates/store-template/app/page.tsx +23 -0
- package/templates/store-template/app/privacy/page.tsx +281 -0
- package/templates/store-template/app/products/[slug]/ProductDetailClient.tsx +179 -0
- package/templates/store-template/app/products/[slug]/page.tsx +149 -0
- package/templates/store-template/app/products/layout.tsx +26 -0
- package/templates/store-template/app/products/page.tsx +102 -0
- package/templates/store-template/app/robots.ts +25 -0
- package/templates/store-template/app/sitemap.ts +76 -0
- package/templates/store-template/app/terms/page.tsx +313 -0
- package/templates/store-template/components/PathSyncBridge.tsx +61 -0
- package/templates/store-template/components/Providers.tsx +37 -0
- package/templates/store-template/components/layout/Footer.tsx +150 -0
- package/templates/store-template/components/layout/Header.tsx +162 -0
- package/templates/store-template/components/sections/.gitkeep +7 -0
- package/templates/store-template/components/sections/CTA.tsx +51 -0
- package/templates/store-template/components/sections/Features.tsx +91 -0
- package/templates/store-template/components/sections/Hero.tsx +85 -0
- package/templates/store-template/eslint.config.mjs +18 -0
- package/templates/store-template/lib/store-config.ts +77 -0
- package/templates/store-template/lib/store-policy.ts +68 -0
- package/templates/store-template/lib/utils.ts +21 -0
- package/templates/store-template/next-env.d.ts +6 -0
- package/templates/store-template/next.config.js +105 -0
- package/templates/store-template/package.json +32 -0
- package/templates/store-template/postcss.config.js +6 -0
- package/templates/store-template/proxy.ts +150 -0
- package/templates/store-template/public/rovela-logo.png +0 -0
- package/templates/store-template/tailwind.config.ts +74 -0
- package/templates/store-template/tsconfig.json +41 -0
- 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
|
+
}
|