Compare commits

..

2 commits

Author SHA1 Message Date
shafi54
5338505bfc Update floating-cart-bar.tsx 2026-07-21 10:07:27 +05:30
shafi54
306297ed09 enh 2026-07-19 09:54:14 +05:30
6 changed files with 659 additions and 381 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

@ -29,6 +29,13 @@ import { LinearGradient } from "expo-linear-gradient";
const { height: screenHeight } = Dimensions.get("window"); 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 { interface FloatingCartBarProps {
isFlashDelivery?: boolean; isFlashDelivery?: boolean;
isExpanded?: boolean; isExpanded?: boolean;
@ -259,10 +266,15 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
<React.Fragment key={item.id}> <React.Fragment key={item.id}>
<View style={tw`py-4`}> <View style={tw`py-4`}>
<View style={tw`flex-row items-center`}> <View style={tw`flex-row items-center`}>
<Image <View style={tw`items-center`}>
source={{ uri: productsById[item.productId]?.images?.[0] }} <Image
style={tw`w-8 h-8 rounded-lg bg-slate-50 border border-slate-100`} 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-1 ml-4`}>
<View style={tw`flex-row items-center justify-between mb-1`}> <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} step={productsById[item.productId]?.incrementStep || 1}
showUnits={true} showUnits={true}
unit={productsById[item.productId]?.unitNotation} // unit={productsById[item.productId]?.unitNotation}
/> />
</View> </View>
<View style={tw`flex-row items-center justify-between`}> <View style={tw`flex-row items-center justify-between`}>

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,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>
) )
} }