enh
This commit is contained in:
parent
8c9df8151a
commit
306297ed09
5 changed files with 642 additions and 376 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
173
apps/web-ui/src/components/ComplaintForm.tsx
Normal file
173
apps/web-ui/src/components/ComplaintForm.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
119
apps/web-ui/src/hooks/useUploadToObjectStorage.ts
Normal file
119
apps/web-ui/src/hooks/useUploadToObjectStorage.ts
Normal 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)
|
||||||
|
}
|
||||||
6
apps/web-ui/src/lib/string-manipulators.ts
Normal file
6
apps/web-ui/src/lib/string-manipulators.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const orderStatusManipulator = (status: string): string => {
|
||||||
|
if (status.toLowerCase() === 'pending') {
|
||||||
|
return 'Confirmed'
|
||||||
|
}
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
@ -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,419 +72,376 @@ 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
|
onClick={() => navigate({ to: '/me/orders' })}
|
||||||
onClick={() => navigate({ to: '/me/orders' })}
|
className="mt-6 rounded-xl bg-slate-900 px-6 py-2 font-bold text-white"
|
||||||
className="mt-6 rounded-xl bg-slate-900 px-6 py-2 font-bold text-white"
|
>
|
||||||
>
|
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">
|
<button
|
||||||
<button
|
onClick={() => navigate({ to: '/me/orders' })}
|
||||||
onClick={() => navigate({ to: '/me/orders' })}
|
className="-ml-2 mr-3 p-2"
|
||||||
className="-ml-2 mr-3 p-2"
|
>
|
||||||
>
|
<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 #{order.orderId || order.id}</p>
|
||||||
<p className="text-lg font-bold text-slate-900">Order #{orderData.orderId || orderData.id}</p>
|
<p className="text-xs text-slate-400">
|
||||||
<p className="text-xs text-slate-400">
|
{dayjs(order.orderDate || order.createdAt).format('DD MMM, h:mm A')}
|
||||||
{dayjs(orderData.orderDate || orderData.createdAt).format('DD MMM, h:mm A')}
|
</p>
|
||||||
</p>
|
{order.isFlashDelivery && (
|
||||||
{orderData.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 className="flex flex-row items-center gap-2">
|
|
||||||
<div
|
|
||||||
className={`rounded-full px-3 py-1 ${statusConfig.bgColor}`}
|
|
||||||
style={{ backgroundColor: statusConfig.color + '10' }}
|
|
||||||
>
|
|
||||||
<p className="text-[10px] font-bold uppercase" style={{ color: statusConfig.color }}>
|
|
||||||
{statusConfig.label}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{orderData.isFlashDelivery && (
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
<div className="flex-1 overflow-y-auto p-4 pb-12">
|
<div
|
||||||
{/* 1 Hr Delivery Banner */}
|
className="rounded-full px-3 py-1"
|
||||||
{orderData.isFlashDelivery && (
|
style={{ backgroundColor: statusConfig.color + '10' }}
|
||||||
<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">
|
<p
|
||||||
<Zap className="h-6 w-6 text-amber-600" />
|
className="text-[10px] font-bold uppercase"
|
||||||
<div className="ml-3 flex-1">
|
style={{ color: statusConfig.color }}
|
||||||
<p className="text-sm font-bold text-amber-900">1 Hr Delivery Order</p>
|
|
||||||
<p className="mt-1 text-xs text-amber-700">
|
|
||||||
Your order will be delivered within 1 hr of placement
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Main Info Card */}
|
|
||||||
<div className="mb-4 rounded-2xl border border-slate-100 bg-white p-5">
|
|
||||||
<div className="mb-4 flex flex-row items-center justify-between">
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
<div className="mr-3 flex h-10 w-10 items-center justify-center rounded-full bg-slate-50">
|
|
||||||
<CreditCard className="h-5 w-5 text-slate-500" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-[10px] font-bold uppercase text-slate-400">Payment Method</p>
|
|
||||||
<p className="font-bold text-slate-900">
|
|
||||||
{orderData.paymentMode?.toUpperCase() === 'COD' ? 'Cash on Delivery' : orderData.paymentMode}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="items-end text-right">
|
|
||||||
<p className="text-[10px] font-bold uppercase text-slate-400">Status</p>
|
|
||||||
<p className="font-bold capitalize text-slate-900">{orderData.paymentStatus}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Delivery Date Info */}
|
|
||||||
{(orderData.deliveryDate || orderData.isFlashDelivery) &&
|
|
||||||
['delivered', 'success'].includes((orderData.deliveryStatus || '').toLowerCase()) && (
|
|
||||||
<div className="flex flex-row items-center border-t border-slate-50 pt-4">
|
|
||||||
{orderData.isFlashDelivery ? (
|
|
||||||
<Zap className="h-4 w-4 text-amber-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'}`}>
|
|
||||||
{orderData.isFlashDelivery
|
|
||||||
? `Flash Delivered on ${dayjs(orderData.createdAt || orderData.orderDate).add(30, 'minutes').format('DD MMM YYYY, h:mm A')}`
|
|
||||||
: `Delivered on ${dayjs(orderData.deliveryDate).format('DD MMM YYYY, h:mm A')}`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Pending 1 Hr Delivery Info */}
|
|
||||||
{orderData.isFlashDelivery &&
|
|
||||||
!['delivered', 'success'].includes((orderData.deliveryStatus || '').toLowerCase()) && (
|
|
||||||
<div className="flex flex-row items-center border-t border-slate-50 pt-4">
|
|
||||||
<Zap className="h-4 w-4 text-amber-600" />
|
|
||||||
<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')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Special Instructions */}
|
|
||||||
<div className="mb-4 rounded-2xl border border-slate-100 bg-white p-5">
|
|
||||||
<div className="mb-3 flex flex-row items-center justify-between">
|
|
||||||
<p className="text-[10px] font-bold uppercase text-slate-400">Special Instructions</p>
|
|
||||||
{isEditingNotes ? (
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setIsEditingNotes(false)
|
|
||||||
setNotesInput(notesText)
|
|
||||||
}}
|
|
||||||
className="rounded-lg bg-slate-100 px-3 py-1"
|
|
||||||
>
|
|
||||||
<p className="text-xs font-bold text-slate-600">Cancel</p>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
updateNotesMutation.mutate({ id: orderId, userNotes: notesInput })
|
|
||||||
}}
|
|
||||||
disabled={updateNotesMutation.isPending}
|
|
||||||
className="rounded-lg bg-blue-500 px-3 py-1"
|
|
||||||
>
|
|
||||||
<p className="text-xs font-bold text-white">
|
|
||||||
{updateNotesMutation.isPending ? 'Saving...' : 'Save'}
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setNotesInput(notesText || '')
|
|
||||||
setIsEditingNotes(true)
|
|
||||||
}}
|
|
||||||
className="flex flex-row items-center rounded-lg bg-slate-50 px-2 py-1"
|
|
||||||
>
|
|
||||||
<Edit2 className="mr-1 h-3 w-3 text-slate-500" />
|
|
||||||
<p className="text-xs font-medium text-slate-500">{notesText ? 'Edit' : 'Add'}</p>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{isEditingNotes ? (
|
|
||||||
<textarea
|
|
||||||
className="min-h-20 rounded-xl border border-slate-200 bg-slate-50 p-3 text-sm text-slate-700"
|
|
||||||
value={notesInput}
|
|
||||||
onChange={(e) => setNotesInput(e.target.value)}
|
|
||||||
placeholder="Add special delivery instructions..."
|
|
||||||
rows={4}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm leading-5 text-slate-700">
|
|
||||||
{notesText || 'No instructions added'}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Cancellation Detail */}
|
|
||||||
{orderData.cancelReason && (
|
|
||||||
<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="text-sm font-medium text-rose-900">{orderData.cancelReason}</p>
|
|
||||||
{orderData.refundAmount && (
|
|
||||||
<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-base font-bold text-rose-900">₹{orderData.refundAmount}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Items Section */}
|
|
||||||
<div className="mb-2 px-1">
|
|
||||||
<p className="mb-3 text-base font-bold text-slate-900">Order Items</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{orderData.items?.map((item: any, index: number) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
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">
|
{orderStatusManipulator(statusConfig.label)}
|
||||||
{item.image ? (
|
</p>
|
||||||
<img
|
</div>
|
||||||
src={item.image}
|
{order.isFlashDelivery && (
|
||||||
alt=""
|
<div className="rounded-full border border-amber-200 bg-amber-100 px-2 py-1">
|
||||||
className="h-full w-full rounded-lg object-cover"
|
<p className="text-[10px] font-black uppercase text-amber-700">⚡</p>
|
||||||
/>
|
</div>
|
||||||
) : (
|
)}
|
||||||
<Package className="h-6 w-6 text-slate-400" />
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
<div className="flex-1 overflow-y-auto p-4 pb-12">
|
||||||
<p className="text-sm font-bold text-slate-900">{item.productName || item.product?.name || `Product #${item.productId}`}</p>
|
{/* 1 Hr Delivery Banner */}
|
||||||
<p className="mt-1 text-xs text-slate-400">
|
{order.isFlashDelivery && (
|
||||||
{item.quantity} × ₹{item.price}
|
<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">
|
||||||
|
<Zap className="h-6 w-6 text-amber-600" />
|
||||||
|
<div className="ml-3 flex-1">
|
||||||
|
<p className="text-sm font-bold text-amber-900">1 Hr Delivery Order</p>
|
||||||
|
<p className="mt-1 text-xs text-amber-700">
|
||||||
|
Your order will be delivered within 1 hr of placement
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="ml-2 text-base font-bold text-slate-900">₹{item.amount || item.price * item.quantity}</p>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Coupon */}
|
{/* Main Info Card */}
|
||||||
{orderData.couponCode && (
|
<div className="mb-4 rounded-2xl border border-slate-100 bg-white p-5">
|
||||||
<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 flex flex-row items-center justify-between">
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<Tag className="h-5 w-5 text-emerald-600" />
|
<div className="mr-3 flex h-10 w-10 items-center justify-center rounded-full bg-slate-50">
|
||||||
<div className="ml-3">
|
<CreditCard className="h-5 w-5 text-slate-500" />
|
||||||
<p className="text-sm font-bold text-emerald-900">{orderData.couponCode}</p>
|
</div>
|
||||||
<p className="text-[10px] text-emerald-600">Coupon Applied</p>
|
<div>
|
||||||
</div>
|
<p className="text-[10px] font-bold uppercase text-slate-400">Payment Method</p>
|
||||||
|
<p className="font-bold text-slate-900">
|
||||||
|
{order.paymentMode?.toUpperCase() === 'COD' ? 'Cash on Delivery' : order.paymentMode}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-bold text-emerald-700">-₹{orderData.discountAmount}</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="items-end text-right">
|
||||||
|
<p className="text-[10px] font-bold uppercase text-slate-400">Status</p>
|
||||||
|
<p className="font-bold capitalize text-slate-900">{order.paymentStatus}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Summary Section */}
|
{(order.deliveryDate || order.isFlashDelivery) &&
|
||||||
<div className="mb-6 rounded-2xl border border-slate-100 bg-white p-5">
|
['delivered', 'success'].includes((order.deliveryStatus || '').toLowerCase()) && (
|
||||||
<div className="mb-3 flex flex-row justify-between">
|
<div className="flex flex-row items-center border-t border-slate-50 pt-4">
|
||||||
<p className="text-sm text-slate-500">Subtotal</p>
|
{order.isFlashDelivery ? (
|
||||||
<p className="font-medium text-slate-900">₹{subtotal}</p>
|
<Zap className="h-4 w-4 text-amber-600" />
|
||||||
</div>
|
) : (
|
||||||
{discountAmount > 0 && (
|
<CheckCircle className="h-4 w-4 text-green-600" />
|
||||||
<div className="mb-3 flex flex-row justify-between">
|
)}
|
||||||
<p className="text-sm text-emerald-600">Discount</p>
|
<p className={`ml-2 text-xs font-medium ${order.isFlashDelivery ? 'text-amber-700' : 'text-slate-600'}`}>
|
||||||
<p className="font-medium text-emerald-600">-₹{discountAmount}</p>
|
{order.isFlashDelivery
|
||||||
|
? `Flash Delivered on ${dayjs(order.createdAt || order.orderDate).add(30, 'minutes').format('DD MMM YYYY, h:mm A')}`
|
||||||
|
: `Delivered on ${dayjs(order.deliveryDate).format('DD MMM YYYY, h:mm A')}`}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mb-3 flex flex-row justify-between">
|
|
||||||
<p className="text-sm text-slate-500">Delivery</p>
|
|
||||||
<p className="font-medium text-slate-900">
|
|
||||||
{deliveryCharge > 0 ? `₹${deliveryCharge}` : 'Free'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row items-center justify-between border-t border-slate-50 pt-4">
|
|
||||||
<p className="text-base font-bold text-slate-900">Total Amount</p>
|
|
||||||
<p className="text-xl font-bold text-slate-900">₹{totalAmount}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer Actions */}
|
{order.isFlashDelivery &&
|
||||||
<div className="flex flex-row gap-3">
|
!['delivered', 'success'].includes((order.deliveryStatus || '').toLowerCase()) && (
|
||||||
<button
|
<div className="flex flex-row items-center border-t border-slate-50 pt-4">
|
||||||
onClick={() => navigate({ to: '/me' })}
|
<Zap className="h-4 w-4 text-amber-600" />
|
||||||
className="flex-1 rounded-xl bg-slate-100 py-3.5 text-center font-bold text-slate-600"
|
<p className="ml-2 text-xs font-medium text-amber-700">
|
||||||
>
|
1 Hr Delivery: {dayjs(order.createdAt || order.orderDate).add(30, 'minutes').format('DD MMM YYYY, h:mm A')}
|
||||||
Back
|
</p>
|
||||||
</button>
|
</div>
|
||||||
<button
|
)}
|
||||||
onClick={() => setComplaintDialogOpen(true)}
|
</div>
|
||||||
className="flex-1 rounded-xl bg-blue-600 py-3.5 text-center font-bold text-white"
|
|
||||||
>
|
|
||||||
Raise Complaint
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Cancel Order Button */}
|
{/* Special Instructions */}
|
||||||
{!['success', 'delivered', 'cancelled'].includes((orderData.deliveryStatus || '').toLowerCase()) && (
|
<div className="mb-4 rounded-2xl border border-slate-100 bg-white p-5">
|
||||||
<div className="mt-4">
|
<div className="mb-3 flex flex-row items-center justify-between">
|
||||||
|
<p className="text-[10px] font-bold uppercase text-slate-400">Special Instructions</p>
|
||||||
|
{isEditingNotes ? (
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsEditingNotes(false)
|
||||||
|
setNotesInput(notesText)
|
||||||
|
}}
|
||||||
|
className="rounded-lg bg-slate-100 px-3 py-1"
|
||||||
|
>
|
||||||
|
<p className="text-xs font-bold text-slate-600">Cancel</p>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
updateNotesMutation.mutate({ id: order.id, userNotes: notesInput })
|
||||||
|
}}
|
||||||
|
disabled={updateNotesMutation.isPending}
|
||||||
|
className="rounded-lg bg-brand-500 px-3 py-1"
|
||||||
|
>
|
||||||
|
<p className="text-xs font-bold text-white">
|
||||||
|
{updateNotesMutation.isPending ? 'Saving...' : 'Save'}
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => setCancelDialogOpen(true)}
|
onClick={() => {
|
||||||
className="flex w-full flex-row items-center justify-center rounded-xl border border-red-100 bg-red-50 py-3.5"
|
setNotesInput(notesText || '')
|
||||||
|
setIsEditingNotes(true)
|
||||||
|
}}
|
||||||
|
className="flex flex-row items-center rounded-lg bg-slate-50 px-2 py-1"
|
||||||
>
|
>
|
||||||
<XCircle className="mr-2 h-5 w-5 text-red-600" />
|
<Edit2 className="mr-1 h-3 w-3 text-slate-500" />
|
||||||
<p className="font-bold text-red-600">Cancel Order</p>
|
<p className="text-xs font-medium text-slate-500">{notesText ? 'Edit' : 'Add'}</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
|
{isEditingNotes ? (
|
||||||
|
<textarea
|
||||||
|
className="min-h-20 rounded-xl border border-slate-200 bg-slate-50 p-3 text-sm text-slate-700"
|
||||||
|
value={notesInput}
|
||||||
|
onChange={(e) => setNotesInput(e.target.value)}
|
||||||
|
placeholder="Add special delivery instructions..."
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm leading-5 text-slate-700">
|
||||||
|
{notesText || 'No instructions added'}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cancel Order Dialog */}
|
{/* Cancellation Detail */}
|
||||||
<Dialog open={cancelDialogOpen} onClose={() => setCancelDialogOpen(false)}>
|
{order.cancelReason && (
|
||||||
<div className="p-6">
|
<div className="mb-4 rounded-2xl border border-rose-100 bg-rose-50 p-5">
|
||||||
<div className="mb-4 flex flex-row items-center justify-between">
|
<p className="mb-2 text-[10px] font-bold uppercase text-rose-700">Cancellation Reason</p>
|
||||||
<p className="text-xl font-bold text-gray-900">Cancel Order</p>
|
<p className="text-sm font-medium text-rose-900">{order.cancelReason}</p>
|
||||||
<button onClick={() => setCancelDialogOpen(false)}>
|
{order.refundAmount && (
|
||||||
<X className="h-6 w-6 text-gray-400" />
|
<div className="mt-3 flex flex-row items-center justify-between border-t border-rose-200/50 pt-3">
|
||||||
</button>
|
<p className="text-xs text-rose-700">Refund Amount</p>
|
||||||
|
<p className="text-base font-bold text-rose-900">₹{order.refundAmount}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Items Section */}
|
||||||
|
<div className="mb-2 px-1">
|
||||||
|
<p className="mb-3 text-base font-bold text-slate-900">Order Items</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{order.items.map((item: any, index: number) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
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 overflow-hidden rounded-xl border border-slate-100 bg-slate-50 p-1">
|
||||||
|
{item.image ? (
|
||||||
|
<img
|
||||||
|
src={item.image}
|
||||||
|
alt={item.productName}
|
||||||
|
className="h-full w-full rounded-lg object-cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="text-[10px] font-bold text-slate-400">No img</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
<div className="mb-4 rounded-xl border border-red-100 bg-red-50 p-4">
|
<p className="line-clamp-1 text-sm font-bold text-slate-900">{item.productName}</p>
|
||||||
<p className="text-sm text-red-800">
|
<p className="mt-1 text-xs text-slate-400">{item.quantity} × ₹{item.price}</p>
|
||||||
Are you sure you want to cancel this order? This action cannot be undone.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p className="ml-2 text-base font-bold text-slate-900">₹{item.amount}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
<p className="mb-2 font-medium text-gray-700">Reason for cancellation</p>
|
{/* Coupon */}
|
||||||
<textarea
|
{order.couponCode && (
|
||||||
className="mb-6 min-h-24 w-full rounded-xl border border-gray-200 bg-gray-50 p-4 text-base text-gray-800"
|
<div className="mb-4 mt-2 flex flex-row items-center justify-between rounded-2xl border border-emerald-100 bg-emerald-50 p-4">
|
||||||
value={cancelReason}
|
<div className="flex flex-row items-center">
|
||||||
onChange={(e) => setCancelReason(e.target.value)}
|
<Tag className="h-5 w-5 text-emerald-600" />
|
||||||
placeholder="Please tell us why you are cancelling..."
|
<div className="ml-3">
|
||||||
rows={3}
|
<p className="text-sm font-bold text-emerald-900">{order.couponCode}</p>
|
||||||
/>
|
<p className="text-[10px] text-emerald-600">Coupon Applied</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="font-bold text-emerald-700">-₹{order.discountAmount}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Summary Section */}
|
||||||
|
<div className="mb-6 rounded-2xl border border-slate-100 bg-white p-5">
|
||||||
|
<div className="mb-3 flex flex-row justify-between">
|
||||||
|
<p className="text-sm text-slate-500">Subtotal</p>
|
||||||
|
<p className="font-medium text-slate-900">₹{subtotal}</p>
|
||||||
|
</div>
|
||||||
|
{discountAmount > 0 && (
|
||||||
|
<div className="mb-3 flex flex-row justify-between">
|
||||||
|
<p className="text-sm text-emerald-600">Discount</p>
|
||||||
|
<p className="font-medium text-emerald-600">-₹{discountAmount}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="mb-3 flex flex-row justify-between">
|
||||||
|
<p className="text-sm text-slate-500">Delivery</p>
|
||||||
|
<p className="font-medium text-slate-900">
|
||||||
|
{deliveryCharge > 0 ? `₹${deliveryCharge}` : 'Free'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row items-center justify-between border-t border-slate-50 pt-4">
|
||||||
|
<p className="text-base font-bold text-slate-900">Total Amount</p>
|
||||||
|
<p className="text-xl font-bold text-slate-900">₹{totalAmount}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer Actions */}
|
||||||
|
<div className="flex flex-row gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate({ to: '/me' })}
|
||||||
|
className="flex-1 rounded-xl bg-slate-100 py-3.5 text-center font-bold text-slate-600"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setComplaintDialogOpen(true)}
|
||||||
|
className="flex-1 rounded-xl bg-blue-600 py-3.5 text-center font-bold text-white"
|
||||||
|
>
|
||||||
|
Raise Complaint
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cancel Order Button */}
|
||||||
|
{order.deliveryStatus !== 'success' && order.deliveryStatus !== 'cancelled' && (
|
||||||
|
<div className="mt-4">
|
||||||
<button
|
<button
|
||||||
className={`w-full rounded-xl py-4 text-center font-bold text-white shadow-sm ${
|
onClick={() => setCancelDialogOpen(true)}
|
||||||
cancelOrderMutation.isPending ? 'bg-red-400 opacity-70' : 'bg-red-600'
|
className="flex w-full flex-row items-center justify-center rounded-xl border border-red-100 bg-red-50 py-3.5"
|
||||||
}`}
|
|
||||||
onClick={handleCancelOrder}
|
|
||||||
disabled={cancelOrderMutation.isPending}
|
|
||||||
>
|
>
|
||||||
{cancelOrderMutation.isPending ? 'Cancelling...' : 'Confirm Cancellation'}
|
<XCircle className="mr-2 h-5 w-5 text-red-600" />
|
||||||
|
<p className="font-bold text-red-600">Cancel Order</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
)}
|
||||||
|
|
||||||
{/* Raise Complaint Dialog */}
|
|
||||||
<Dialog open={complaintDialogOpen} onClose={() => setComplaintDialogOpen(false)}>
|
|
||||||
<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={() => setComplaintDialogOpen(false)}>
|
|
||||||
<X className="h-6 w-6 text-gray-400" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</AppContainer>
|
|
||||||
|
{/* Cancel Order Dialog */}
|
||||||
|
<Dialog open={cancelDialogOpen} onClose={() => setCancelDialogOpen(false)}>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="mb-4 flex flex-row items-center justify-between">
|
||||||
|
<p className="text-xl font-bold text-gray-900">Cancel Order</p>
|
||||||
|
<button onClick={() => setCancelDialogOpen(false)}>
|
||||||
|
<X className="h-6 w-6 text-gray-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4 rounded-xl border border-red-100 bg-red-50 p-4">
|
||||||
|
<p className="text-sm text-red-800">
|
||||||
|
Are you sure you want to cancel this order? This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mb-2 font-medium text-gray-700">Reason for cancellation</p>
|
||||||
|
<textarea
|
||||||
|
className="mb-6 min-h-24 w-full rounded-xl border border-gray-200 bg-gray-50 p-4 text-base text-gray-800"
|
||||||
|
value={cancelReason}
|
||||||
|
onChange={(e) => setCancelReason(e.target.value)}
|
||||||
|
placeholder="Please tell us why you are cancelling..."
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
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'
|
||||||
|
}`}
|
||||||
|
onClick={handleCancelOrder}
|
||||||
|
disabled={cancelOrderMutation.isPending}
|
||||||
|
>
|
||||||
|
{cancelOrderMutation.isPending ? (
|
||||||
|
<Loader2 className="h-5 w-5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
'Confirm Cancellation'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* Raise Complaint Dialog */}
|
||||||
|
<Dialog open={complaintDialogOpen} onClose={() => setComplaintDialogOpen(false)}>
|
||||||
|
<ComplaintForm
|
||||||
|
open={complaintDialogOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setComplaintDialogOpen(false)
|
||||||
|
refetch()
|
||||||
|
}}
|
||||||
|
orderId={order.id}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Loading…
Add table
Reference in a new issue