Compare commits
No commits in common. "5338505bfc265a225c88bc7fe91a36e190fa9c80" and "8c9df8151aad5abe440d4cfc29e3514ed9283405" have entirely different histories.
5338505bfc
...
8c9df8151a
6 changed files with 393 additions and 671 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
|
||||||
|
|
|
||||||
|
|
@ -29,13 +29,6 @@ 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;
|
||||||
|
|
@ -266,15 +259,10 @@ 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`}>
|
||||||
<View style={tw`items-center`}>
|
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: productsById[item.productId]?.images?.[0] }}
|
source={{ uri: productsById[item.productId]?.images?.[0] }}
|
||||||
style={tw`w-8 h-8 rounded-lg bg-slate-50 border border-slate-100`}
|
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`}>
|
||||||
|
|
@ -295,7 +283,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`}>
|
||||||
|
|
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
export const orderStatusManipulator = (status: string): string => {
|
|
||||||
if (status.toLowerCase() === 'pending') {
|
|
||||||
return 'Confirmed'
|
|
||||||
}
|
|
||||||
return status
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
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 { ChevronLeft, CreditCard, CheckCircle, Zap, Edit2, Tag, XCircle, X, AlertCircle, Loader2 } from 'lucide-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 { 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')({
|
||||||
|
|
@ -14,15 +13,11 @@ 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 {
|
const { data: orderData, isLoading, error, refetch } = trpc.user.order.getOrderById.useQuery(
|
||||||
data: orderData,
|
{ orderId: orderId+'' },
|
||||||
isLoading,
|
{ enabled: !!orderId }
|
||||||
error,
|
|
||||||
refetch,
|
|
||||||
} = trpc.user.order.getOrderById.useQuery(
|
|
||||||
{ orderId: id },
|
|
||||||
{ enabled: !!id }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const [isEditingNotes, setIsEditingNotes] = useState(false)
|
const [isEditingNotes, setIsEditingNotes] = useState(false)
|
||||||
|
|
@ -31,16 +26,15 @@ 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')
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -49,21 +43,16 @@ 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 handleCancelOrder = () => {
|
const raiseComplaintMutation = trpc.user.complaint.raise.useMutation({
|
||||||
if (!order) return
|
onSuccess: () => {
|
||||||
if (!cancelReason.trim()) {
|
setComplaintDialogOpen(false)
|
||||||
alert('Please enter a reason for cancellation')
|
setComplaintBody('')
|
||||||
return
|
refetch()
|
||||||
}
|
},
|
||||||
cancelOrderMutation.mutate({ id: order.id, reason: cancelReason })
|
})
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (orderData?.userNotes) {
|
if (orderData?.userNotes) {
|
||||||
|
|
@ -72,17 +61,36 @@ 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 (
|
||||||
<div className="flex min-h-screen flex-1 items-center justify-center bg-white">
|
<AppContainer>
|
||||||
|
<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 (
|
||||||
<div className="flex min-h-screen flex-1 flex-col items-center justify-center bg-white p-8">
|
<AppContainer>
|
||||||
|
<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
|
||||||
|
|
@ -92,36 +100,37 @@ function OrderDetailPage() {
|
||||||
Go Back
|
Go Back
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</AppContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const order = orderData
|
|
||||||
const orderAny = order as any
|
|
||||||
const getStatusConfig = (status: string) => {
|
const getStatusConfig = (status: string) => {
|
||||||
const s = status.toLowerCase()
|
const s = status.toLowerCase()
|
||||||
switch (s) {
|
switch (s) {
|
||||||
case 'delivered':
|
case 'delivered':
|
||||||
case 'success':
|
case 'success':
|
||||||
return { label: 'Delivered', color: '#10B981' }
|
return { label: 'Delivered', color: '#10B981', bgColor: 'bg-green-50', textColor: 'text-green-700' }
|
||||||
case 'cancelled':
|
case 'cancelled':
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return { label: 'Cancelled', color: '#EF4444' }
|
return { label: 'Cancelled', color: '#EF4444', bgColor: 'bg-red-50', textColor: 'text-red-700' }
|
||||||
case 'pending':
|
case 'pending':
|
||||||
case 'processing':
|
case 'processing':
|
||||||
return { label: 'Pending', color: '#F59E0B' }
|
return { label: 'Pending', color: '#F59E0B', bgColor: 'bg-yellow-50', textColor: 'text-yellow-700' }
|
||||||
default:
|
default:
|
||||||
return { label: status, color: '#1570EF' }
|
return { label: status, color: '#3B82F6', bgColor: 'bg-blue-50', textColor: 'text-blue-700' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const subtotal = order.items.reduce((sum: number, item: any) => sum + item.amount, 0)
|
const orderAny = orderData as any
|
||||||
const discountAmount = order.discountAmount || 0
|
const subtotal = orderData.items?.reduce((sum: number, item: any) => sum + (item.amount || item.price * item.quantity), 0) || 0
|
||||||
const deliveryCharge = orderAny.deliveryCharge || 0
|
const discountAmount = orderData.discountAmount || 0
|
||||||
const totalAmount = order.orderAmount
|
const deliveryCharge = orderData.deliveryCharge || 0
|
||||||
const statusConfig = getStatusConfig(order.deliveryStatus || 'pending')
|
const totalAmount = orderData.orderAmount || subtotal - discountAmount + deliveryCharge
|
||||||
|
const statusConfig = getStatusConfig(orderData.deliveryStatus || 'pending')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-1 flex-col bg-slate-50">
|
<AppContainer>
|
||||||
|
<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">
|
||||||
|
|
@ -132,28 +141,25 @@ function OrderDetailPage() {
|
||||||
<ChevronLeft className="h-6 w-6 text-slate-800" />
|
<ChevronLeft className="h-6 w-6 text-slate-800" />
|
||||||
</button>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-lg font-bold text-slate-900">Order #{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>
|
</div>
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className="rounded-full px-3 py-1"
|
className={`rounded-full px-3 py-1 ${statusConfig.bgColor}`}
|
||||||
style={{ backgroundColor: statusConfig.color + '10' }}
|
style={{ backgroundColor: statusConfig.color + '10' }}
|
||||||
>
|
>
|
||||||
<p
|
<p className="text-[10px] font-bold uppercase" style={{ color: statusConfig.color }}>
|
||||||
className="text-[10px] font-bold uppercase"
|
{statusConfig.label}
|
||||||
style={{ color: statusConfig.color }}
|
|
||||||
>
|
|
||||||
{orderStatusManipulator(statusConfig.label)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{order.isFlashDelivery && (
|
{orderData.isFlashDelivery && (
|
||||||
<div className="rounded-full border border-amber-200 bg-amber-100 px-2 py-1">
|
<div className="rounded-full border border-amber-200 bg-amber-100 px-2 py-1">
|
||||||
<p className="text-[10px] font-black uppercase text-amber-700">⚡</p>
|
<p className="text-[10px] font-black uppercase text-amber-700">⚡</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -163,7 +169,7 @@ function OrderDetailPage() {
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-4 pb-12">
|
<div className="flex-1 overflow-y-auto p-4 pb-12">
|
||||||
{/* 1 Hr Delivery Banner */}
|
{/* 1 Hr Delivery Banner */}
|
||||||
{order.isFlashDelivery && (
|
{orderData.isFlashDelivery && (
|
||||||
<div className="mb-4 rounded-2xl border border-amber-200 bg-linear-to-r from-amber-50 to-yellow-50 p-4">
|
<div className="mb-4 rounded-2xl border border-amber-200 bg-linear-to-r from-amber-50 to-yellow-50 p-4">
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<Zap className="h-6 w-6 text-amber-600" />
|
<Zap className="h-6 w-6 text-amber-600" />
|
||||||
|
|
@ -187,38 +193,40 @@ function OrderDetailPage() {
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[10px] font-bold uppercase text-slate-400">Payment Method</p>
|
<p className="text-[10px] font-bold uppercase text-slate-400">Payment Method</p>
|
||||||
<p className="font-bold text-slate-900">
|
<p className="font-bold text-slate-900">
|
||||||
{order.paymentMode?.toUpperCase() === 'COD' ? 'Cash on Delivery' : order.paymentMode}
|
{orderData.paymentMode?.toUpperCase() === 'COD' ? 'Cash on Delivery' : orderData.paymentMode}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="items-end text-right">
|
<div className="items-end text-right">
|
||||||
<p className="text-[10px] font-bold uppercase text-slate-400">Status</p>
|
<p className="text-[10px] font-bold uppercase text-slate-400">Status</p>
|
||||||
<p className="font-bold capitalize text-slate-900">{order.paymentStatus}</p>
|
<p className="font-bold capitalize text-slate-900">{orderData.paymentStatus}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(order.deliveryDate || order.isFlashDelivery) &&
|
{/* Delivery Date Info */}
|
||||||
['delivered', 'success'].includes((order.deliveryStatus || '').toLowerCase()) && (
|
{(orderData.deliveryDate || orderData.isFlashDelivery) &&
|
||||||
|
['delivered', 'success'].includes((orderData.deliveryStatus || '').toLowerCase()) && (
|
||||||
<div className="flex flex-row items-center border-t border-slate-50 pt-4">
|
<div className="flex flex-row items-center border-t border-slate-50 pt-4">
|
||||||
{order.isFlashDelivery ? (
|
{orderData.isFlashDelivery ? (
|
||||||
<Zap className="h-4 w-4 text-amber-600" />
|
<Zap className="h-4 w-4 text-amber-600" />
|
||||||
) : (
|
) : (
|
||||||
<CheckCircle className="h-4 w-4 text-green-600" />
|
<CheckCircle className="h-4 w-4 text-green-600" />
|
||||||
)}
|
)}
|
||||||
<p className={`ml-2 text-xs font-medium ${order.isFlashDelivery ? 'text-amber-700' : 'text-slate-600'}`}>
|
<p className={`ml-2 text-xs font-medium ${orderData.isFlashDelivery ? 'text-amber-700' : 'text-slate-600'}`}>
|
||||||
{order.isFlashDelivery
|
{orderData.isFlashDelivery
|
||||||
? `Flash Delivered on ${dayjs(order.createdAt || order.orderDate).add(30, 'minutes').format('DD MMM YYYY, h:mm A')}`
|
? `Flash Delivered on ${dayjs(orderData.createdAt || orderData.orderDate).add(30, 'minutes').format('DD MMM YYYY, h:mm A')}`
|
||||||
: `Delivered on ${dayjs(order.deliveryDate).format('DD MMM YYYY, h:mm A')}`}
|
: `Delivered on ${dayjs(orderData.deliveryDate).format('DD MMM YYYY, h:mm A')}`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{order.isFlashDelivery &&
|
{/* Pending 1 Hr Delivery Info */}
|
||||||
!['delivered', 'success'].includes((order.deliveryStatus || '').toLowerCase()) && (
|
{orderData.isFlashDelivery &&
|
||||||
|
!['delivered', 'success'].includes((orderData.deliveryStatus || '').toLowerCase()) && (
|
||||||
<div className="flex flex-row items-center border-t border-slate-50 pt-4">
|
<div className="flex flex-row items-center border-t border-slate-50 pt-4">
|
||||||
<Zap className="h-4 w-4 text-amber-600" />
|
<Zap className="h-4 w-4 text-amber-600" />
|
||||||
<p className="ml-2 text-xs font-medium text-amber-700">
|
<p className="ml-2 text-xs font-medium text-amber-700">
|
||||||
1 Hr Delivery: {dayjs(order.createdAt || order.orderDate).add(30, 'minutes').format('DD MMM YYYY, h:mm A')}
|
1 Hr Delivery: {dayjs(orderData.createdAt || orderData.orderDate).add(30, 'minutes').format('DD MMM YYYY, h:mm A')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -241,10 +249,10 @@ function OrderDetailPage() {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateNotesMutation.mutate({ id: order.id, userNotes: notesInput })
|
updateNotesMutation.mutate({ id: orderId, userNotes: notesInput })
|
||||||
}}
|
}}
|
||||||
disabled={updateNotesMutation.isPending}
|
disabled={updateNotesMutation.isPending}
|
||||||
className="rounded-lg bg-brand-500 px-3 py-1"
|
className="rounded-lg bg-blue-500 px-3 py-1"
|
||||||
>
|
>
|
||||||
<p className="text-xs font-bold text-white">
|
<p className="text-xs font-bold text-white">
|
||||||
{updateNotesMutation.isPending ? 'Saving...' : 'Save'}
|
{updateNotesMutation.isPending ? 'Saving...' : 'Save'}
|
||||||
|
|
@ -280,14 +288,14 @@ function OrderDetailPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cancellation Detail */}
|
{/* Cancellation Detail */}
|
||||||
{order.cancelReason && (
|
{orderData.cancelReason && (
|
||||||
<div className="mb-4 rounded-2xl border border-rose-100 bg-rose-50 p-5">
|
<div className="mb-4 rounded-2xl border border-rose-100 bg-rose-50 p-5">
|
||||||
<p className="mb-2 text-[10px] font-bold uppercase text-rose-700">Cancellation Reason</p>
|
<p className="mb-2 text-[10px] font-bold uppercase text-rose-700">Cancellation Reason</p>
|
||||||
<p className="text-sm font-medium text-rose-900">{order.cancelReason}</p>
|
<p className="text-sm font-medium text-rose-900">{orderData.cancelReason}</p>
|
||||||
{order.refundAmount && (
|
{orderData.refundAmount && (
|
||||||
<div className="mt-3 flex flex-row items-center justify-between border-t border-rose-200/50 pt-3">
|
<div className="mt-3 flex flex-row items-center justify-between border-t border-rose-200/50 pt-3">
|
||||||
<p className="text-xs text-rose-700">Refund Amount</p>
|
<p className="text-xs text-rose-700">Refund Amount</p>
|
||||||
<p className="text-base font-bold text-rose-900">₹{order.refundAmount}</p>
|
<p className="text-base font-bold text-rose-900">₹{orderData.refundAmount}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -298,41 +306,43 @@ function OrderDetailPage() {
|
||||||
<p className="mb-3 text-base font-bold text-slate-900">Order Items</p>
|
<p className="mb-3 text-base font-bold text-slate-900">Order Items</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{order.items.map((item: any, index: number) => (
|
{orderData.items?.map((item: any, index: number) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="mb-3 flex flex-row items-center rounded-2xl border border-slate-100 bg-white p-3 shadow-sm"
|
className="mb-3 flex flex-row items-center rounded-2xl border border-slate-100 bg-white p-3 shadow-sm"
|
||||||
>
|
>
|
||||||
<div className="mr-4 flex h-14 w-14 items-center justify-center overflow-hidden rounded-xl border border-slate-100 bg-slate-50 p-1">
|
<div className="mr-4 flex h-14 w-14 items-center justify-center rounded-xl border border-slate-100 bg-slate-50 p-1">
|
||||||
{item.image ? (
|
{item.image ? (
|
||||||
<img
|
<img
|
||||||
src={item.image}
|
src={item.image}
|
||||||
alt={item.productName}
|
alt=""
|
||||||
className="h-full w-full rounded-lg object-cover"
|
className="h-full w-full rounded-lg object-cover"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-[10px] font-bold text-slate-400">No img</div>
|
<Package className="h-6 w-6 text-slate-400" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="line-clamp-1 text-sm font-bold text-slate-900">{item.productName}</p>
|
<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="mt-1 text-xs text-slate-400">
|
||||||
|
{item.quantity} × ₹{item.price}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="ml-2 text-base font-bold text-slate-900">₹{item.amount}</p>
|
<p className="ml-2 text-base font-bold text-slate-900">₹{item.amount || item.price * item.quantity}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Coupon */}
|
{/* Coupon */}
|
||||||
{order.couponCode && (
|
{orderData.couponCode && (
|
||||||
<div className="mb-4 mt-2 flex flex-row items-center justify-between rounded-2xl border border-emerald-100 bg-emerald-50 p-4">
|
<div className="mb-4 mt-2 flex flex-row items-center justify-between rounded-2xl border border-emerald-100 bg-emerald-50 p-4">
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<Tag className="h-5 w-5 text-emerald-600" />
|
<Tag className="h-5 w-5 text-emerald-600" />
|
||||||
<div className="ml-3">
|
<div className="ml-3">
|
||||||
<p className="text-sm font-bold text-emerald-900">{order.couponCode}</p>
|
<p className="text-sm font-bold text-emerald-900">{orderData.couponCode}</p>
|
||||||
<p className="text-[10px] text-emerald-600">Coupon Applied</p>
|
<p className="text-[10px] text-emerald-600">Coupon Applied</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-bold text-emerald-700">-₹{order.discountAmount}</p>
|
<p className="font-bold text-emerald-700">-₹{orderData.discountAmount}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -377,7 +387,7 @@ function OrderDetailPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Cancel Order Button */}
|
{/* Cancel Order Button */}
|
||||||
{order.deliveryStatus !== 'success' && order.deliveryStatus !== 'cancelled' && (
|
{!['success', 'delivered', 'cancelled'].includes((orderData.deliveryStatus || '').toLowerCase()) && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => setCancelDialogOpen(true)}
|
onClick={() => setCancelDialogOpen(true)}
|
||||||
|
|
@ -416,32 +426,54 @@ function OrderDetailPage() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={`flex w-full items-center justify-center rounded-xl py-4 text-center font-bold text-white shadow-sm ${
|
className={`w-full rounded-xl py-4 text-center font-bold text-white shadow-sm ${
|
||||||
cancelOrderMutation.isPending ? 'bg-red-400 opacity-70' : 'bg-red-600'
|
cancelOrderMutation.isPending ? 'bg-red-400 opacity-70' : 'bg-red-600'
|
||||||
}`}
|
}`}
|
||||||
onClick={handleCancelOrder}
|
onClick={handleCancelOrder}
|
||||||
disabled={cancelOrderMutation.isPending}
|
disabled={cancelOrderMutation.isPending}
|
||||||
>
|
>
|
||||||
{cancelOrderMutation.isPending ? (
|
{cancelOrderMutation.isPending ? 'Cancelling...' : 'Confirm Cancellation'}
|
||||||
<Loader2 className="h-5 w-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
'Confirm Cancellation'
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Raise Complaint Dialog */}
|
{/* Raise Complaint Dialog */}
|
||||||
<Dialog open={complaintDialogOpen} onClose={() => setComplaintDialogOpen(false)}>
|
<Dialog open={complaintDialogOpen} onClose={() => setComplaintDialogOpen(false)}>
|
||||||
<ComplaintForm
|
<div className="p-6">
|
||||||
open={complaintDialogOpen}
|
<div className="mb-4 flex flex-row items-center justify-between">
|
||||||
onClose={() => {
|
<p className="text-xl font-bold text-gray-900">Raise Complaint</p>
|
||||||
setComplaintDialogOpen(false)
|
<button onClick={() => setComplaintDialogOpen(false)}>
|
||||||
refetch()
|
<X className="h-6 w-6 text-gray-400" />
|
||||||
}}
|
</button>
|
||||||
orderId={order.id}
|
</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>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
</AppContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Loading…
Add table
Reference in a new issue