Compare commits
2 commits
8c9df8151a
...
5338505bfc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5338505bfc | ||
|
|
306297ed09 |
6 changed files with 659 additions and 381 deletions
|
|
@ -292,7 +292,7 @@ const CompactProductCard = ({
|
|||
onChange={handleQuantityChange}
|
||||
step={item.incrementStep}
|
||||
showUnits={true}
|
||||
unit={item.unitNotation}
|
||||
// unit={item.unitNotation}
|
||||
/>
|
||||
) : (
|
||||
<MyTouchableOpacity
|
||||
|
|
|
|||
|
|
@ -29,6 +29,13 @@ import { LinearGradient } from "expo-linear-gradient";
|
|||
|
||||
const { height: screenHeight } = Dimensions.get("window");
|
||||
|
||||
const formatQuantity = (quantity: number, unit: string): { value: string; display: string } => {
|
||||
if (unit?.toLowerCase() === 'kg' && quantity < 1) {
|
||||
return { value: `${Math.round(quantity * 1000)} g`, display: `${Math.round(quantity * 1000)}g` };
|
||||
}
|
||||
return { value: `${quantity} ${unit}(s)`, display: `${quantity}${unit}` };
|
||||
};
|
||||
|
||||
interface FloatingCartBarProps {
|
||||
isFlashDelivery?: boolean;
|
||||
isExpanded?: boolean;
|
||||
|
|
@ -259,10 +266,15 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
|
|||
<React.Fragment key={item.id}>
|
||||
<View style={tw`py-4`}>
|
||||
<View style={tw`flex-row items-center`}>
|
||||
<View style={tw`items-center`}>
|
||||
<Image
|
||||
source={{ uri: productsById[item.productId]?.images?.[0] }}
|
||||
style={tw`w-8 h-8 rounded-lg bg-slate-50 border border-slate-100`}
|
||||
/>
|
||||
<MyText style={tw`text-gray-500 text-[9px] font-medium mt-1`}>
|
||||
<MyText style={tw`text-[#f81260] font-semibold`}>{formatQuantity(productsById[item.productId]?.productQuantity || 1, productsById[item.productId]?.unitNotation || '').display}</MyText>
|
||||
</MyText>
|
||||
</View>
|
||||
|
||||
<View style={tw`flex-1 ml-4`}>
|
||||
<View style={tw`flex-row items-center justify-between mb-1`}>
|
||||
|
|
@ -283,7 +295,7 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
|
|||
}}
|
||||
step={productsById[item.productId]?.incrementStep || 1}
|
||||
showUnits={true}
|
||||
unit={productsById[item.productId]?.unitNotation}
|
||||
// unit={productsById[item.productId]?.unitNotation}
|
||||
/>
|
||||
</View>
|
||||
<View style={tw`flex-row items-center justify-between`}>
|
||||
|
|
|
|||
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 { trpc } from '../lib/trpc-client'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { p, AppContainer, div } from 'web-components'
|
||||
import { ChevronLeft, CreditCard, Package, CheckCircle, XCircle, Clock, Zap, Calendar, Edit2, Tag, AlertCircle, X } from 'lucide-react'
|
||||
import { ChevronLeft, CreditCard, CheckCircle, Zap, Edit2, Tag, XCircle, X, AlertCircle, Loader2 } from 'lucide-react'
|
||||
import { Dialog } from '../components/Dialog'
|
||||
import ComplaintForm from '../components/ComplaintForm'
|
||||
import { orderStatusManipulator } from '../lib/string-manipulators'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export const Route = createFileRoute('/me/orders/$id')({
|
||||
|
|
@ -13,11 +14,15 @@ export const Route = createFileRoute('/me/orders/$id')({
|
|||
function OrderDetailPage() {
|
||||
const { id } = useParams({ from: '/me/orders/$id' })
|
||||
const navigate = useNavigate()
|
||||
const orderId = Number(id)
|
||||
|
||||
const { data: orderData, isLoading, error, refetch } = trpc.user.order.getOrderById.useQuery(
|
||||
{ orderId: orderId+'' },
|
||||
{ enabled: !!orderId }
|
||||
const {
|
||||
data: orderData,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
} = trpc.user.order.getOrderById.useQuery(
|
||||
{ orderId: id },
|
||||
{ enabled: !!id }
|
||||
)
|
||||
|
||||
const [isEditingNotes, setIsEditingNotes] = useState(false)
|
||||
|
|
@ -26,15 +31,16 @@ function OrderDetailPage() {
|
|||
const [cancelDialogOpen, setCancelDialogOpen] = useState(false)
|
||||
const [cancelReason, setCancelReason] = useState('')
|
||||
const [complaintDialogOpen, setComplaintDialogOpen] = useState(false)
|
||||
const [complaintBody, setComplaintBody] = useState('')
|
||||
|
||||
// const order = orderData?.data
|
||||
|
||||
const updateNotesMutation = trpc.user.order.updateUserNotes.useMutation({
|
||||
onSuccess: () => {
|
||||
setNotesText(notesInput)
|
||||
setIsEditingNotes(false)
|
||||
refetch()
|
||||
alert('Notes updated successfully')
|
||||
},
|
||||
onError: (error: any) => {
|
||||
alert(error.message || 'Failed to update notes')
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -43,16 +49,21 @@ function OrderDetailPage() {
|
|||
setCancelDialogOpen(false)
|
||||
setCancelReason('')
|
||||
refetch()
|
||||
alert('Order cancelled successfully')
|
||||
},
|
||||
onError: (error: any) => {
|
||||
alert(error.message || 'Failed to cancel order')
|
||||
},
|
||||
})
|
||||
|
||||
const raiseComplaintMutation = trpc.user.complaint.raise.useMutation({
|
||||
onSuccess: () => {
|
||||
setComplaintDialogOpen(false)
|
||||
setComplaintBody('')
|
||||
refetch()
|
||||
},
|
||||
})
|
||||
const handleCancelOrder = () => {
|
||||
if (!order) return
|
||||
if (!cancelReason.trim()) {
|
||||
alert('Please enter a reason for cancellation')
|
||||
return
|
||||
}
|
||||
cancelOrderMutation.mutate({ id: order.id, reason: cancelReason })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (orderData?.userNotes) {
|
||||
|
|
@ -61,36 +72,17 @@ function OrderDetailPage() {
|
|||
}
|
||||
}, [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) {
|
||||
return (
|
||||
<AppContainer>
|
||||
<div className="flex min-h-full flex-1 items-center justify-center bg-white">
|
||||
<div className="flex min-h-screen flex-1 items-center justify-center bg-white">
|
||||
<p className="font-medium text-slate-400">Loading details...</p>
|
||||
</div>
|
||||
</AppContainer>
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !orderData) {
|
||||
return (
|
||||
<AppContainer>
|
||||
<div className="flex min-h-full flex-1 flex-col items-center justify-center bg-white p-8">
|
||||
<div className="flex min-h-screen flex-1 flex-col items-center justify-center bg-white p-8">
|
||||
<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>
|
||||
<button
|
||||
|
|
@ -100,37 +92,36 @@ function OrderDetailPage() {
|
|||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
</AppContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const order = orderData
|
||||
const orderAny = order as any
|
||||
const getStatusConfig = (status: string) => {
|
||||
const s = status.toLowerCase()
|
||||
switch (s) {
|
||||
case 'delivered':
|
||||
case 'success':
|
||||
return { label: 'Delivered', color: '#10B981', bgColor: 'bg-green-50', textColor: 'text-green-700' }
|
||||
return { label: 'Delivered', color: '#10B981' }
|
||||
case 'cancelled':
|
||||
case 'failed':
|
||||
return { label: 'Cancelled', color: '#EF4444', bgColor: 'bg-red-50', textColor: 'text-red-700' }
|
||||
return { label: 'Cancelled', color: '#EF4444' }
|
||||
case 'pending':
|
||||
case 'processing':
|
||||
return { label: 'Pending', color: '#F59E0B', bgColor: 'bg-yellow-50', textColor: 'text-yellow-700' }
|
||||
return { label: 'Pending', color: '#F59E0B' }
|
||||
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 = orderData.items?.reduce((sum: number, item: any) => sum + (item.amount || item.price * item.quantity), 0) || 0
|
||||
const discountAmount = orderData.discountAmount || 0
|
||||
const deliveryCharge = orderData.deliveryCharge || 0
|
||||
const totalAmount = orderData.orderAmount || subtotal - discountAmount + deliveryCharge
|
||||
const statusConfig = getStatusConfig(orderData.deliveryStatus || 'pending')
|
||||
const subtotal = order.items.reduce((sum: number, item: any) => sum + item.amount, 0)
|
||||
const discountAmount = order.discountAmount || 0
|
||||
const deliveryCharge = orderAny.deliveryCharge || 0
|
||||
const totalAmount = order.orderAmount
|
||||
const statusConfig = getStatusConfig(order.deliveryStatus || 'pending')
|
||||
|
||||
return (
|
||||
<AppContainer>
|
||||
<div className="flex min-h-full flex-1 flex-col bg-slate-50">
|
||||
<div className="flex min-h-screen flex-1 flex-col bg-slate-50">
|
||||
{/* 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">
|
||||
|
|
@ -141,25 +132,28 @@ function OrderDetailPage() {
|
|||
<ChevronLeft className="h-6 w-6 text-slate-800" />
|
||||
</button>
|
||||
<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">
|
||||
{dayjs(orderData.orderDate || orderData.createdAt).format('DD MMM, h:mm A')}
|
||||
{dayjs(order.orderDate || order.createdAt).format('DD MMM, h:mm A')}
|
||||
</p>
|
||||
{orderData.isFlashDelivery && (
|
||||
{order.isFlashDelivery && (
|
||||
<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}`}
|
||||
className="rounded-full px-3 py-1"
|
||||
style={{ backgroundColor: statusConfig.color + '10' }}
|
||||
>
|
||||
<p className="text-[10px] font-bold uppercase" style={{ color: statusConfig.color }}>
|
||||
{statusConfig.label}
|
||||
<p
|
||||
className="text-[10px] font-bold uppercase"
|
||||
style={{ color: statusConfig.color }}
|
||||
>
|
||||
{orderStatusManipulator(statusConfig.label)}
|
||||
</p>
|
||||
</div>
|
||||
{orderData.isFlashDelivery && (
|
||||
{order.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>
|
||||
|
|
@ -169,7 +163,7 @@ function OrderDetailPage() {
|
|||
|
||||
<div className="flex-1 overflow-y-auto p-4 pb-12">
|
||||
{/* 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="flex flex-row items-center">
|
||||
<Zap className="h-6 w-6 text-amber-600" />
|
||||
|
|
@ -193,40 +187,38 @@ function OrderDetailPage() {
|
|||
<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}
|
||||
{order.paymentMode?.toUpperCase() === 'COD' ? 'Cash on Delivery' : order.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>
|
||||
<p className="font-bold capitalize text-slate-900">{order.paymentStatus}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delivery Date Info */}
|
||||
{(orderData.deliveryDate || orderData.isFlashDelivery) &&
|
||||
['delivered', 'success'].includes((orderData.deliveryStatus || '').toLowerCase()) && (
|
||||
{(order.deliveryDate || order.isFlashDelivery) &&
|
||||
['delivered', 'success'].includes((order.deliveryStatus || '').toLowerCase()) && (
|
||||
<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" />
|
||||
) : (
|
||||
<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 className={`ml-2 text-xs font-medium ${order.isFlashDelivery ? 'text-amber-700' : 'text-slate-600'}`}>
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* Pending 1 Hr Delivery Info */}
|
||||
{orderData.isFlashDelivery &&
|
||||
!['delivered', 'success'].includes((orderData.deliveryStatus || '').toLowerCase()) && (
|
||||
{order.isFlashDelivery &&
|
||||
!['delivered', 'success'].includes((order.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')}
|
||||
1 Hr Delivery: {dayjs(order.createdAt || order.orderDate).add(30, 'minutes').format('DD MMM YYYY, h:mm A')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -249,10 +241,10 @@ function OrderDetailPage() {
|
|||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
updateNotesMutation.mutate({ id: orderId, userNotes: notesInput })
|
||||
updateNotesMutation.mutate({ id: order.id, userNotes: notesInput })
|
||||
}}
|
||||
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">
|
||||
{updateNotesMutation.isPending ? 'Saving...' : 'Save'}
|
||||
|
|
@ -288,14 +280,14 @@ function OrderDetailPage() {
|
|||
</div>
|
||||
|
||||
{/* Cancellation Detail */}
|
||||
{orderData.cancelReason && (
|
||||
{order.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 && (
|
||||
<p className="text-sm font-medium text-rose-900">{order.cancelReason}</p>
|
||||
{order.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>
|
||||
<p className="text-base font-bold text-rose-900">₹{order.refundAmount}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -306,43 +298,41 @@ function OrderDetailPage() {
|
|||
<p className="mb-3 text-base font-bold text-slate-900">Order Items</p>
|
||||
</div>
|
||||
|
||||
{orderData.items?.map((item: any, index: number) => (
|
||||
{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 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 ? (
|
||||
<img
|
||||
src={item.image}
|
||||
alt=""
|
||||
alt={item.productName}
|
||||
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 className="flex-1">
|
||||
<p className="text-sm font-bold text-slate-900">{item.productName || item.product?.name || `Product #${item.productId}`}</p>
|
||||
<p className="mt-1 text-xs text-slate-400">
|
||||
{item.quantity} × ₹{item.price}
|
||||
</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">{item.quantity} × ₹{item.price}</p>
|
||||
</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>
|
||||
))}
|
||||
|
||||
{/* 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="flex flex-row items-center">
|
||||
<Tag className="h-5 w-5 text-emerald-600" />
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<p className="font-bold text-emerald-700">-₹{orderData.discountAmount}</p>
|
||||
<p className="font-bold text-emerald-700">-₹{order.discountAmount}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -387,7 +377,7 @@ function OrderDetailPage() {
|
|||
</div>
|
||||
|
||||
{/* Cancel Order Button */}
|
||||
{!['success', 'delivered', 'cancelled'].includes((orderData.deliveryStatus || '').toLowerCase()) && (
|
||||
{order.deliveryStatus !== 'success' && order.deliveryStatus !== 'cancelled' && (
|
||||
<div className="mt-4">
|
||||
<button
|
||||
onClick={() => setCancelDialogOpen(true)}
|
||||
|
|
@ -426,54 +416,32 @@ function OrderDetailPage() {
|
|||
/>
|
||||
|
||||
<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'
|
||||
}`}
|
||||
onClick={handleCancelOrder}
|
||||
disabled={cancelOrderMutation.isPending}
|
||||
>
|
||||
{cancelOrderMutation.isPending ? 'Cancelling...' : 'Confirm Cancellation'}
|
||||
{cancelOrderMutation.isPending ? (
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
) : (
|
||||
'Confirm Cancellation'
|
||||
)}
|
||||
</button>
|
||||
</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}
|
||||
<ComplaintForm
|
||||
open={complaintDialogOpen}
|
||||
onClose={() => {
|
||||
setComplaintDialogOpen(false)
|
||||
refetch()
|
||||
}}
|
||||
orderId={order.id}
|
||||
/>
|
||||
|
||||
<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>
|
||||
</AppContainer>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue