This commit is contained in:
shafi54 2026-07-19 09:54:14 +05:30
parent 8c9df8151a
commit 306297ed09
5 changed files with 642 additions and 376 deletions

View file

@ -292,7 +292,7 @@ const CompactProductCard = ({
onChange={handleQuantityChange} onChange={handleQuantityChange}
step={item.incrementStep} step={item.incrementStep}
showUnits={true} showUnits={true}
unit={item.unitNotation} // unit={item.unitNotation}
/> />
) : ( ) : (
<MyTouchableOpacity <MyTouchableOpacity

View file

@ -0,0 +1,173 @@
import { useState, useRef } from 'react'
import { X, Upload, ImageIcon, Loader2 } from 'lucide-react'
import { trpc } from '../lib/trpc-client'
import { useUploadToObjectStorage } from '../hooks/useUploadToObjectStorage'
interface ComplaintFormProps {
open: boolean
onClose: () => void
orderId: number
}
interface ComplaintImage {
imgUrl: string
mimeType: string
file: File
}
export default function ComplaintForm({ open, onClose, orderId }: ComplaintFormProps) {
const [complaintBody, setComplaintBody] = useState('')
const [complaintImages, setComplaintImages] = useState<ComplaintImage[]>([])
const fileInputRef = useRef<HTMLInputElement>(null)
const raiseComplaintMutation = trpc.user.complaint.raise.useMutation()
const { refetch: refetchComplaints } = trpc.user.complaint.getAll.useQuery(undefined, {
enabled: false,
})
const { upload, isUploading } = useUploadToObjectStorage()
const handleAddImages = (files: FileList | null) => {
if (!files || files.length === 0) return
const newImages: ComplaintImage[] = []
Array.from(files).forEach((file) => {
if (!file.type.startsWith('image/')) return
newImages.push({
imgUrl: URL.createObjectURL(file),
mimeType: file.type || 'image/jpeg',
file,
})
})
setComplaintImages((prev) => [...prev, ...newImages])
}
const handleRemoveImage = (image: ComplaintImage) => {
setComplaintImages((prev) => {
URL.revokeObjectURL(image.imgUrl)
return prev.filter((item) => item.imgUrl !== image.imgUrl)
})
}
const handleSubmit = async () => {
if (!complaintBody.trim()) {
alert('Please enter complaint details')
return
}
try {
let imageUrls: string[] = []
if (complaintImages.length > 0) {
const uploadImages = complaintImages.map((image) => ({
blob: image.file,
mimeType: image.mimeType || 'image/jpeg',
}))
const { keys } = await upload({
images: uploadImages,
contextString: 'complaint',
})
imageUrls = keys
}
await raiseComplaintMutation.mutateAsync({
orderId: orderId.toString(),
complaintBody: complaintBody.trim(),
imageUrls,
})
await refetchComplaints()
alert('Complaint raised successfully')
setComplaintBody('')
setComplaintImages([])
onClose()
} catch (error: any) {
alert(error.message || 'Failed to raise complaint')
}
}
if (!open) return null
return (
<div className="p-6">
<div className="mb-4 flex flex-row items-center justify-between">
<p className="text-xl font-bold text-gray-900">Raise Complaint</p>
<button onClick={onClose}>
<X className="h-6 w-6 text-gray-400" />
</button>
</div>
<p className="mb-2 font-medium text-gray-700">Describe the issue</p>
<textarea
className="mb-4 min-h-32 w-full rounded-xl border border-gray-200 bg-gray-50 p-4 text-base text-gray-800"
value={complaintBody}
onChange={(e) => setComplaintBody(e.target.value)}
placeholder="Tell us what went wrong..."
rows={4}
/>
{/* Image uploader */}
<div className="mb-4">
<p className="mb-2 font-medium text-gray-700">Add images (optional)</p>
<div className="flex flex-row flex-wrap gap-3">
{complaintImages.map((image) => (
<div key={image.imgUrl} className="relative h-20 w-20 overflow-hidden rounded-xl border border-gray-200">
<img src={image.imgUrl} alt="complaint" className="h-full w-full object-cover" />
<button
onClick={() => handleRemoveImage(image)}
className="absolute right-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-black/60"
>
<X className="h-4 w-4 text-white" />
</button>
</div>
))}
<button
onClick={() => fileInputRef.current?.click()}
className="flex h-20 w-20 flex-col items-center justify-center rounded-xl border-2 border-dashed border-gray-300 hover:border-brand-500 hover:bg-gray-50"
>
<Upload className="h-5 w-5 text-gray-400" />
<span className="mt-1 text-[10px] font-medium text-gray-500">Add</span>
</button>
<input
ref={fileInputRef}
type="file"
accept="image/*"
multiple
className="hidden"
onChange={(e) => {
handleAddImages(e.target.files)
e.target.value = ''
}}
/>
</div>
{complaintImages.length === 0 && (
<div className="mt-2 flex flex-row items-center">
<ImageIcon className="h-3.5 w-3.5 text-gray-400" />
<p className="ml-1 text-xs text-gray-400">No images selected</p>
</div>
)}
</div>
<button
className={`mt-2 flex w-full items-center justify-center gap-2 rounded-xl py-4 font-bold text-white shadow-sm transition-colors ${
raiseComplaintMutation.isPending || isUploading ? 'bg-yellow-400 opacity-70' : 'bg-yellow-500 hover:bg-yellow-600'
}`}
onClick={handleSubmit}
disabled={raiseComplaintMutation.isPending || isUploading}
>
{raiseComplaintMutation.isPending || isUploading ? (
<>
<Loader2 className="h-5 w-5 animate-spin" />
{isUploading ? 'Uploading...' : 'Submitting...'}
</>
) : (
'Submit Complaint'
)}
</button>
</div>
)
}

View file

@ -0,0 +1,119 @@
import { useState } from 'react'
import { trpc } from '../lib/trpc-client'
type ContextString = 'review' | 'product_info' | 'notification' | 'store' | 'complaint' | 'profile'
interface UploadInput {
blob: Blob
mimeType: string
}
interface UploadBatchInput {
images: UploadInput[]
contextString: ContextString
}
interface UploadResult {
keys: string[]
presignedUrls: string[]
}
export function useUploadToObjectStorage() {
const [isUploading, setIsUploading] = useState(false)
const [error, setError] = useState<Error | null>(null)
const [progress, setProgress] = useState<{ completed: number; total: number } | null>(null)
const generateUploadUrls = trpc.common.generateUploadUrls.useMutation()
const upload = async (input: UploadBatchInput): Promise<UploadResult> => {
setIsUploading(true)
setError(null)
setProgress({ completed: 0, total: input.images.length })
try {
const { images, contextString } = input
if (images.length === 0) {
return { keys: [], presignedUrls: [] }
}
const mimeTypes = images.map((img) => img.mimeType)
const { uploadUrls } = await generateUploadUrls.mutateAsync({
contextString: contextString as any,
mimeTypes,
})
if (uploadUrls.length !== images.length) {
throw new Error(`Expected ${images.length} URLs, got ${uploadUrls.length}`)
}
const uploadPromises = images.map(async (image, index) => {
const presignedUrl = uploadUrls[index]
const { blob, mimeType } = image
const response = await fetch(presignedUrl, {
method: 'PUT',
body: blob,
headers: { 'Content-Type': mimeType },
})
if (!response.ok) {
throw new Error(`Upload ${index + 1} failed with status ${response.status}`)
}
setProgress((prev) => (prev ? { ...prev, completed: prev.completed + 1 } : null))
return {
key: extractKeyFromPresignedUrl(presignedUrl),
presignedUrl,
}
})
const results = await Promise.all(uploadPromises)
return {
keys: results.map((result) => result.key),
presignedUrls: results.map((result) => result.presignedUrl),
}
} catch (err) {
const uploadError = err instanceof Error ? err : new Error('Upload failed')
setError(uploadError)
throw uploadError
} finally {
setIsUploading(false)
setProgress(null)
}
}
const uploadSingle = async (
blob: Blob,
mimeType: string,
contextString: ContextString
): Promise<{ key: string; presignedUrl: string }> => {
const result = await upload({
images: [{ blob, mimeType }],
contextString,
})
return {
key: result.keys[0],
presignedUrl: result.presignedUrls[0],
}
}
return {
upload,
uploadSingle,
isUploading,
error,
progress,
isPending: generateUploadUrls.isPending,
}
}
function extractKeyFromPresignedUrl(url: string): string {
const parsedUrl = new URL(url)
let rawKey = parsedUrl.pathname.replace(/^\/+/, '')
rawKey = rawKey.split('/').slice(1).join('/')
return decodeURIComponent(rawKey)
}

View file

@ -0,0 +1,6 @@
export const orderStatusManipulator = (status: string): string => {
if (status.toLowerCase() === 'pending') {
return 'Confirmed'
}
return status
}

View file

@ -1,9 +1,10 @@
import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router' import { createFileRoute, useNavigate, useParams } from '@tanstack/react-router'
import { trpc } from '../lib/trpc-client' import { trpc } from '../lib/trpc-client'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { p, AppContainer, div } from 'web-components' import { ChevronLeft, CreditCard, CheckCircle, Zap, Edit2, Tag, XCircle, X, AlertCircle, Loader2 } from 'lucide-react'
import { ChevronLeft, CreditCard, Package, CheckCircle, XCircle, Clock, Zap, Calendar, Edit2, Tag, AlertCircle, X } from 'lucide-react'
import { Dialog } from '../components/Dialog' import { Dialog } from '../components/Dialog'
import ComplaintForm from '../components/ComplaintForm'
import { orderStatusManipulator } from '../lib/string-manipulators'
import dayjs from 'dayjs' import dayjs from 'dayjs'
export const Route = createFileRoute('/me/orders/$id')({ export const Route = createFileRoute('/me/orders/$id')({
@ -13,11 +14,15 @@ export const Route = createFileRoute('/me/orders/$id')({
function OrderDetailPage() { function OrderDetailPage() {
const { id } = useParams({ from: '/me/orders/$id' }) const { id } = useParams({ from: '/me/orders/$id' })
const navigate = useNavigate() const navigate = useNavigate()
const orderId = Number(id)
const { data: orderData, isLoading, error, refetch } = trpc.user.order.getOrderById.useQuery( const {
{ orderId: orderId+'' }, data: orderData,
{ enabled: !!orderId } isLoading,
error,
refetch,
} = trpc.user.order.getOrderById.useQuery(
{ orderId: id },
{ enabled: !!id }
) )
const [isEditingNotes, setIsEditingNotes] = useState(false) const [isEditingNotes, setIsEditingNotes] = useState(false)
@ -26,15 +31,16 @@ function OrderDetailPage() {
const [cancelDialogOpen, setCancelDialogOpen] = useState(false) const [cancelDialogOpen, setCancelDialogOpen] = useState(false)
const [cancelReason, setCancelReason] = useState('') const [cancelReason, setCancelReason] = useState('')
const [complaintDialogOpen, setComplaintDialogOpen] = useState(false) const [complaintDialogOpen, setComplaintDialogOpen] = useState(false)
const [complaintBody, setComplaintBody] = useState('')
// const order = orderData?.data
const updateNotesMutation = trpc.user.order.updateUserNotes.useMutation({ const updateNotesMutation = trpc.user.order.updateUserNotes.useMutation({
onSuccess: () => { onSuccess: () => {
setNotesText(notesInput) setNotesText(notesInput)
setIsEditingNotes(false) setIsEditingNotes(false)
refetch() refetch()
alert('Notes updated successfully')
},
onError: (error: any) => {
alert(error.message || 'Failed to update notes')
}, },
}) })
@ -43,16 +49,21 @@ function OrderDetailPage() {
setCancelDialogOpen(false) setCancelDialogOpen(false)
setCancelReason('') setCancelReason('')
refetch() refetch()
alert('Order cancelled successfully')
},
onError: (error: any) => {
alert(error.message || 'Failed to cancel order')
}, },
}) })
const raiseComplaintMutation = trpc.user.complaint.raise.useMutation({ const handleCancelOrder = () => {
onSuccess: () => { if (!order) return
setComplaintDialogOpen(false) if (!cancelReason.trim()) {
setComplaintBody('') alert('Please enter a reason for cancellation')
refetch() return
}, }
}) cancelOrderMutation.mutate({ id: order.id, reason: cancelReason })
}
useEffect(() => { useEffect(() => {
if (orderData?.userNotes) { if (orderData?.userNotes) {
@ -61,36 +72,17 @@ function OrderDetailPage() {
} }
}, [orderData]) }, [orderData])
const handleCancelOrder = () => {
if (!cancelReason.trim()) {
alert('Please enter a reason for cancellation')
return
}
cancelOrderMutation.mutate({ id: orderId, reason: cancelReason })
}
const handleRaiseComplaint = () => {
if (!complaintBody.trim()) {
alert('Please describe your complaint')
return
}
raiseComplaintMutation.mutate({ orderId: orderId+'', complaintBody: complaintBody.trim() })
}
if (isLoading) { if (isLoading) {
return ( return (
<AppContainer> <div className="flex min-h-screen flex-1 items-center justify-center bg-white">
<div className="flex min-h-full flex-1 items-center justify-center bg-white">
<p className="font-medium text-slate-400">Loading details...</p> <p className="font-medium text-slate-400">Loading details...</p>
</div> </div>
</AppContainer>
) )
} }
if (error || !orderData) { if (error || !orderData) {
return ( return (
<AppContainer> <div className="flex min-h-screen flex-1 flex-col items-center justify-center bg-white p-8">
<div className="flex min-h-full flex-1 flex-col items-center justify-center bg-white p-8">
<AlertCircle className="mb-4 h-12 w-12 text-red-500" /> <AlertCircle className="mb-4 h-12 w-12 text-red-500" />
<p className="mt-4 text-lg font-bold text-slate-900">Failed to load</p> <p className="mt-4 text-lg font-bold text-slate-900">Failed to load</p>
<button <button
@ -100,37 +92,36 @@ function OrderDetailPage() {
Go Back Go Back
</button> </button>
</div> </div>
</AppContainer>
) )
} }
const order = orderData
const orderAny = order as any
const getStatusConfig = (status: string) => { const getStatusConfig = (status: string) => {
const s = status.toLowerCase() const s = status.toLowerCase()
switch (s) { switch (s) {
case 'delivered': case 'delivered':
case 'success': case 'success':
return { label: 'Delivered', color: '#10B981', bgColor: 'bg-green-50', textColor: 'text-green-700' } return { label: 'Delivered', color: '#10B981' }
case 'cancelled': case 'cancelled':
case 'failed': case 'failed':
return { label: 'Cancelled', color: '#EF4444', bgColor: 'bg-red-50', textColor: 'text-red-700' } return { label: 'Cancelled', color: '#EF4444' }
case 'pending': case 'pending':
case 'processing': case 'processing':
return { label: 'Pending', color: '#F59E0B', bgColor: 'bg-yellow-50', textColor: 'text-yellow-700' } return { label: 'Pending', color: '#F59E0B' }
default: default:
return { label: status, color: '#3B82F6', bgColor: 'bg-blue-50', textColor: 'text-blue-700' } return { label: status, color: '#1570EF' }
} }
} }
const orderAny = orderData as any const subtotal = order.items.reduce((sum: number, item: any) => sum + item.amount, 0)
const subtotal = orderData.items?.reduce((sum: number, item: any) => sum + (item.amount || item.price * item.quantity), 0) || 0 const discountAmount = order.discountAmount || 0
const discountAmount = orderData.discountAmount || 0 const deliveryCharge = orderAny.deliveryCharge || 0
const deliveryCharge = orderData.deliveryCharge || 0 const totalAmount = order.orderAmount
const totalAmount = orderData.orderAmount || subtotal - discountAmount + deliveryCharge const statusConfig = getStatusConfig(order.deliveryStatus || 'pending')
const statusConfig = getStatusConfig(orderData.deliveryStatus || 'pending')
return ( return (
<AppContainer> <div className="flex min-h-screen flex-1 flex-col bg-slate-50">
<div className="flex min-h-full flex-1 flex-col bg-slate-50">
{/* Header */} {/* Header */}
<div className="flex flex-row items-center justify-between border-b border-slate-100 bg-white px-6 pb-4 pt-4"> <div className="flex flex-row items-center justify-between border-b border-slate-100 bg-white px-6 pb-4 pt-4">
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
@ -141,25 +132,28 @@ function OrderDetailPage() {
<ChevronLeft className="h-6 w-6 text-slate-800" /> <ChevronLeft className="h-6 w-6 text-slate-800" />
</button> </button>
<div> <div>
<p className="text-lg font-bold text-slate-900">Order #{orderData.orderId || orderData.id}</p> <p className="text-lg font-bold text-slate-900">Order #{order.orderId || order.id}</p>
<p className="text-xs text-slate-400"> <p className="text-xs text-slate-400">
{dayjs(orderData.orderDate || orderData.createdAt).format('DD MMM, h:mm A')} {dayjs(order.orderDate || order.createdAt).format('DD MMM, h:mm A')}
</p> </p>
{orderData.isFlashDelivery && ( {order.isFlashDelivery && (
<p className="mt-1 text-xs font-bold text-amber-600"> 1 Hr Delivery</p> <p className="mt-1 text-xs font-bold text-amber-600"> 1 Hr Delivery</p>
)} )}
</div> </div>
</div> </div>
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
<div <div
className={`rounded-full px-3 py-1 ${statusConfig.bgColor}`} className="rounded-full px-3 py-1"
style={{ backgroundColor: statusConfig.color + '10' }} style={{ backgroundColor: statusConfig.color + '10' }}
> >
<p className="text-[10px] font-bold uppercase" style={{ color: statusConfig.color }}> <p
{statusConfig.label} className="text-[10px] font-bold uppercase"
style={{ color: statusConfig.color }}
>
{orderStatusManipulator(statusConfig.label)}
</p> </p>
</div> </div>
{orderData.isFlashDelivery && ( {order.isFlashDelivery && (
<div className="rounded-full border border-amber-200 bg-amber-100 px-2 py-1"> <div className="rounded-full border border-amber-200 bg-amber-100 px-2 py-1">
<p className="text-[10px] font-black uppercase text-amber-700"></p> <p className="text-[10px] font-black uppercase text-amber-700"></p>
</div> </div>
@ -169,7 +163,7 @@ function OrderDetailPage() {
<div className="flex-1 overflow-y-auto p-4 pb-12"> <div className="flex-1 overflow-y-auto p-4 pb-12">
{/* 1 Hr Delivery Banner */} {/* 1 Hr Delivery Banner */}
{orderData.isFlashDelivery && ( {order.isFlashDelivery && (
<div className="mb-4 rounded-2xl border border-amber-200 bg-linear-to-r from-amber-50 to-yellow-50 p-4"> <div className="mb-4 rounded-2xl border border-amber-200 bg-linear-to-r from-amber-50 to-yellow-50 p-4">
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
<Zap className="h-6 w-6 text-amber-600" /> <Zap className="h-6 w-6 text-amber-600" />
@ -193,40 +187,38 @@ function OrderDetailPage() {
<div> <div>
<p className="text-[10px] font-bold uppercase text-slate-400">Payment Method</p> <p className="text-[10px] font-bold uppercase text-slate-400">Payment Method</p>
<p className="font-bold text-slate-900"> <p className="font-bold text-slate-900">
{orderData.paymentMode?.toUpperCase() === 'COD' ? 'Cash on Delivery' : orderData.paymentMode} {order.paymentMode?.toUpperCase() === 'COD' ? 'Cash on Delivery' : order.paymentMode}
</p> </p>
</div> </div>
</div> </div>
<div className="items-end text-right"> <div className="items-end text-right">
<p className="text-[10px] font-bold uppercase text-slate-400">Status</p> <p className="text-[10px] font-bold uppercase text-slate-400">Status</p>
<p className="font-bold capitalize text-slate-900">{orderData.paymentStatus}</p> <p className="font-bold capitalize text-slate-900">{order.paymentStatus}</p>
</div> </div>
</div> </div>
{/* Delivery Date Info */} {(order.deliveryDate || order.isFlashDelivery) &&
{(orderData.deliveryDate || orderData.isFlashDelivery) && ['delivered', 'success'].includes((order.deliveryStatus || '').toLowerCase()) && (
['delivered', 'success'].includes((orderData.deliveryStatus || '').toLowerCase()) && (
<div className="flex flex-row items-center border-t border-slate-50 pt-4"> <div className="flex flex-row items-center border-t border-slate-50 pt-4">
{orderData.isFlashDelivery ? ( {order.isFlashDelivery ? (
<Zap className="h-4 w-4 text-amber-600" /> <Zap className="h-4 w-4 text-amber-600" />
) : ( ) : (
<CheckCircle className="h-4 w-4 text-green-600" /> <CheckCircle className="h-4 w-4 text-green-600" />
)} )}
<p className={`ml-2 text-xs font-medium ${orderData.isFlashDelivery ? 'text-amber-700' : 'text-slate-600'}`}> <p className={`ml-2 text-xs font-medium ${order.isFlashDelivery ? 'text-amber-700' : 'text-slate-600'}`}>
{orderData.isFlashDelivery {order.isFlashDelivery
? `Flash Delivered on ${dayjs(orderData.createdAt || orderData.orderDate).add(30, 'minutes').format('DD MMM YYYY, h:mm A')}` ? `Flash Delivered on ${dayjs(order.createdAt || order.orderDate).add(30, 'minutes').format('DD MMM YYYY, h:mm A')}`
: `Delivered on ${dayjs(orderData.deliveryDate).format('DD MMM YYYY, h:mm A')}`} : `Delivered on ${dayjs(order.deliveryDate).format('DD MMM YYYY, h:mm A')}`}
</p> </p>
</div> </div>
)} )}
{/* Pending 1 Hr Delivery Info */} {order.isFlashDelivery &&
{orderData.isFlashDelivery && !['delivered', 'success'].includes((order.deliveryStatus || '').toLowerCase()) && (
!['delivered', 'success'].includes((orderData.deliveryStatus || '').toLowerCase()) && (
<div className="flex flex-row items-center border-t border-slate-50 pt-4"> <div className="flex flex-row items-center border-t border-slate-50 pt-4">
<Zap className="h-4 w-4 text-amber-600" /> <Zap className="h-4 w-4 text-amber-600" />
<p className="ml-2 text-xs font-medium text-amber-700"> <p className="ml-2 text-xs font-medium text-amber-700">
1 Hr Delivery: {dayjs(orderData.createdAt || orderData.orderDate).add(30, 'minutes').format('DD MMM YYYY, h:mm A')} 1 Hr Delivery: {dayjs(order.createdAt || order.orderDate).add(30, 'minutes').format('DD MMM YYYY, h:mm A')}
</p> </p>
</div> </div>
)} )}
@ -249,10 +241,10 @@ function OrderDetailPage() {
</button> </button>
<button <button
onClick={() => { onClick={() => {
updateNotesMutation.mutate({ id: orderId, userNotes: notesInput }) updateNotesMutation.mutate({ id: order.id, userNotes: notesInput })
}} }}
disabled={updateNotesMutation.isPending} disabled={updateNotesMutation.isPending}
className="rounded-lg bg-blue-500 px-3 py-1" className="rounded-lg bg-brand-500 px-3 py-1"
> >
<p className="text-xs font-bold text-white"> <p className="text-xs font-bold text-white">
{updateNotesMutation.isPending ? 'Saving...' : 'Save'} {updateNotesMutation.isPending ? 'Saving...' : 'Save'}
@ -288,14 +280,14 @@ function OrderDetailPage() {
</div> </div>
{/* Cancellation Detail */} {/* Cancellation Detail */}
{orderData.cancelReason && ( {order.cancelReason && (
<div className="mb-4 rounded-2xl border border-rose-100 bg-rose-50 p-5"> <div className="mb-4 rounded-2xl border border-rose-100 bg-rose-50 p-5">
<p className="mb-2 text-[10px] font-bold uppercase text-rose-700">Cancellation Reason</p> <p className="mb-2 text-[10px] font-bold uppercase text-rose-700">Cancellation Reason</p>
<p className="text-sm font-medium text-rose-900">{orderData.cancelReason}</p> <p className="text-sm font-medium text-rose-900">{order.cancelReason}</p>
{orderData.refundAmount && ( {order.refundAmount && (
<div className="mt-3 flex flex-row items-center justify-between border-t border-rose-200/50 pt-3"> <div className="mt-3 flex flex-row items-center justify-between border-t border-rose-200/50 pt-3">
<p className="text-xs text-rose-700">Refund Amount</p> <p className="text-xs text-rose-700">Refund Amount</p>
<p className="text-base font-bold text-rose-900">{orderData.refundAmount}</p> <p className="text-base font-bold text-rose-900">{order.refundAmount}</p>
</div> </div>
)} )}
</div> </div>
@ -306,43 +298,41 @@ function OrderDetailPage() {
<p className="mb-3 text-base font-bold text-slate-900">Order Items</p> <p className="mb-3 text-base font-bold text-slate-900">Order Items</p>
</div> </div>
{orderData.items?.map((item: any, index: number) => ( {order.items.map((item: any, index: number) => (
<div <div
key={index} key={index}
className="mb-3 flex flex-row items-center rounded-2xl border border-slate-100 bg-white p-3 shadow-sm" className="mb-3 flex flex-row items-center rounded-2xl border border-slate-100 bg-white p-3 shadow-sm"
> >
<div className="mr-4 flex h-14 w-14 items-center justify-center rounded-xl border border-slate-100 bg-slate-50 p-1"> <div className="mr-4 flex h-14 w-14 items-center justify-center overflow-hidden rounded-xl border border-slate-100 bg-slate-50 p-1">
{item.image ? ( {item.image ? (
<img <img
src={item.image} src={item.image}
alt="" alt={item.productName}
className="h-full w-full rounded-lg object-cover" className="h-full w-full rounded-lg object-cover"
/> />
) : ( ) : (
<Package className="h-6 w-6 text-slate-400" /> <div className="text-[10px] font-bold text-slate-400">No img</div>
)} )}
</div> </div>
<div className="flex-1"> <div className="flex-1">
<p className="text-sm font-bold text-slate-900">{item.productName || item.product?.name || `Product #${item.productId}`}</p> <p className="line-clamp-1 text-sm font-bold text-slate-900">{item.productName}</p>
<p className="mt-1 text-xs text-slate-400"> <p className="mt-1 text-xs text-slate-400">{item.quantity} × {item.price}</p>
{item.quantity} × {item.price}
</p>
</div> </div>
<p className="ml-2 text-base font-bold text-slate-900">{item.amount || item.price * item.quantity}</p> <p className="ml-2 text-base font-bold text-slate-900">{item.amount}</p>
</div> </div>
))} ))}
{/* Coupon */} {/* Coupon */}
{orderData.couponCode && ( {order.couponCode && (
<div className="mb-4 mt-2 flex flex-row items-center justify-between rounded-2xl border border-emerald-100 bg-emerald-50 p-4"> <div className="mb-4 mt-2 flex flex-row items-center justify-between rounded-2xl border border-emerald-100 bg-emerald-50 p-4">
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
<Tag className="h-5 w-5 text-emerald-600" /> <Tag className="h-5 w-5 text-emerald-600" />
<div className="ml-3"> <div className="ml-3">
<p className="text-sm font-bold text-emerald-900">{orderData.couponCode}</p> <p className="text-sm font-bold text-emerald-900">{order.couponCode}</p>
<p className="text-[10px] text-emerald-600">Coupon Applied</p> <p className="text-[10px] text-emerald-600">Coupon Applied</p>
</div> </div>
</div> </div>
<p className="font-bold text-emerald-700">-{orderData.discountAmount}</p> <p className="font-bold text-emerald-700">-{order.discountAmount}</p>
</div> </div>
)} )}
@ -387,7 +377,7 @@ function OrderDetailPage() {
</div> </div>
{/* Cancel Order Button */} {/* Cancel Order Button */}
{!['success', 'delivered', 'cancelled'].includes((orderData.deliveryStatus || '').toLowerCase()) && ( {order.deliveryStatus !== 'success' && order.deliveryStatus !== 'cancelled' && (
<div className="mt-4"> <div className="mt-4">
<button <button
onClick={() => setCancelDialogOpen(true)} onClick={() => setCancelDialogOpen(true)}
@ -426,54 +416,32 @@ function OrderDetailPage() {
/> />
<button <button
className={`w-full rounded-xl py-4 text-center font-bold text-white shadow-sm ${ className={`flex w-full items-center justify-center rounded-xl py-4 text-center font-bold text-white shadow-sm ${
cancelOrderMutation.isPending ? 'bg-red-400 opacity-70' : 'bg-red-600' cancelOrderMutation.isPending ? 'bg-red-400 opacity-70' : 'bg-red-600'
}`} }`}
onClick={handleCancelOrder} onClick={handleCancelOrder}
disabled={cancelOrderMutation.isPending} disabled={cancelOrderMutation.isPending}
> >
{cancelOrderMutation.isPending ? 'Cancelling...' : 'Confirm Cancellation'} {cancelOrderMutation.isPending ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
'Confirm Cancellation'
)}
</button> </button>
</div> </div>
</Dialog> </Dialog>
{/* Raise Complaint Dialog */} {/* Raise Complaint Dialog */}
<Dialog open={complaintDialogOpen} onClose={() => setComplaintDialogOpen(false)}> <Dialog open={complaintDialogOpen} onClose={() => setComplaintDialogOpen(false)}>
<div className="p-6"> <ComplaintForm
<div className="mb-4 flex flex-row items-center justify-between"> open={complaintDialogOpen}
<p className="text-xl font-bold text-gray-900">Raise Complaint</p> onClose={() => {
<button onClick={() => setComplaintDialogOpen(false)}> setComplaintDialogOpen(false)
<X className="h-6 w-6 text-gray-400" /> refetch()
</button> }}
</div> orderId={order.id}
<div className="mb-4 rounded-xl border border-amber-100 bg-amber-50 p-4">
<p className="text-sm text-amber-800">
Please describe your issue with this order. Our support team will get back to you within 24 hours.
</p>
</div>
<p className="mb-2 font-medium text-gray-700">Complaint Details</p>
<textarea
className="mb-6 min-h-32 w-full rounded-xl border border-gray-200 bg-gray-50 p-4 text-base text-gray-800"
value={complaintBody}
onChange={(e) => setComplaintBody(e.target.value)}
placeholder="Describe your issue in detail..."
rows={4}
/> />
<button
className={`w-full rounded-xl py-4 text-center font-bold text-white shadow-sm ${
raiseComplaintMutation.isPending ? 'bg-blue-400 opacity-70' : 'bg-blue-600'
}`}
onClick={handleRaiseComplaint}
disabled={raiseComplaintMutation.isPending}
>
{raiseComplaintMutation.isPending ? 'Submitting...' : 'Submit Complaint'}
</button>
</div>
</Dialog> </Dialog>
</div> </div>
</AppContainer>
) )
} }