1044 lines
No EOL
44 KiB
TypeScript
1044 lines
No EOL
44 KiB
TypeScript
import React, { useState, useEffect, useMemo } from "react";
|
|
import {
|
|
View,
|
|
ScrollView,
|
|
Image,
|
|
Alert,
|
|
Platform,
|
|
TouchableOpacity,
|
|
} from "react-native";
|
|
import { useRouter } from "expo-router";
|
|
import {
|
|
tw,
|
|
useManualRefresh,
|
|
AppContainer,
|
|
useMarkDataFetchers,
|
|
MyText,
|
|
MyTouchableOpacity,
|
|
BottomDropdown, BottomDialog , Quantifier } from "common-ui";
|
|
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
|
import { useHideTabNav } from "@/src/hooks/useHideTabNav";
|
|
|
|
import TestingPhaseNote from "@/components/TestingPhaseNote";
|
|
|
|
import dayjs from "dayjs";
|
|
import { trpc } from "@/src/trpc-client";
|
|
import { useGetCart, useUpdateCartItem, useRemoveFromCart } from '@/hooks/cart-query-hooks';
|
|
import { useGetEssentialConsts } from '@/src/api-hooks/essential-consts.api';
|
|
|
|
interface CartItem {
|
|
id: number;
|
|
productId: number;
|
|
quantity: number;
|
|
product: {
|
|
price: number;
|
|
isOutOfStock: boolean;
|
|
name: string;
|
|
images: string[];
|
|
unit?: string;
|
|
incrementStep?: number;
|
|
productQuantity?: number;
|
|
} | null;
|
|
}
|
|
|
|
interface CartPageProps {
|
|
isFlashDelivery?: boolean;
|
|
}
|
|
|
|
export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
|
// Hide tabs when cart page is active
|
|
useHideTabNav();
|
|
|
|
const cartType: "regular" | "flash" = isFlashDelivery ? "flash" : "regular";
|
|
|
|
const [quantities, setQuantities] = useState<Record<number, number>>({});
|
|
const {
|
|
data: cartData,
|
|
isLoading,
|
|
error,
|
|
refetch: refetchCart,
|
|
} = useGetCart({ refetchOnWindowFocus: true }, cartType);
|
|
|
|
// Extract product IDs from cart items
|
|
const productIds = cartData?.items.map(item => item.productId) || [];
|
|
|
|
// Get cart slots for the products in cart
|
|
const { data: slotsData, refetch: refetchSlots, error: slotsError } = trpc.user.cart.getCartSlots.useQuery(
|
|
{ productIds },
|
|
{
|
|
enabled: productIds.length > 0,
|
|
refetchOnWindowFocus: false
|
|
}
|
|
);
|
|
|
|
const generateCouponDescription = (coupon: any): string => {
|
|
let desc = "";
|
|
|
|
if (coupon.discountPercent) {
|
|
desc += `${coupon.discountPercent}% off`;
|
|
} else if (coupon.flatDiscount) {
|
|
desc += `₹${coupon.flatDiscount} off`;
|
|
}
|
|
|
|
if (coupon.minOrder) {
|
|
desc += ` on orders above ₹${coupon.minOrder}`;
|
|
}
|
|
|
|
if (coupon.maxValue) {
|
|
desc += ` (max discount ₹${coupon.maxValue})`;
|
|
}
|
|
|
|
return desc;
|
|
};
|
|
|
|
const { data: couponsRaw, error: couponsError } = trpc.user.coupon.getEligible.useQuery();
|
|
const { data: constsData } = useGetEssentialConsts();
|
|
const { data: productsData } = trpc.user.product.getAllProductsSummary.useQuery();
|
|
|
|
const cartItems = cartData?.items || [];
|
|
|
|
|
|
// Memoized flash-eligible product IDs
|
|
const flashEligibleProductIds = useMemo(() => {
|
|
if (!productsData) return new Set<number>();
|
|
return new Set(
|
|
productsData
|
|
.filter((product: any) => product.isFlashAvailable)
|
|
.map((product: any) => product.id)
|
|
);
|
|
}, [productsData]);
|
|
|
|
// Base total price without discounts for coupon eligibility check
|
|
const baseTotalPrice = useMemo(
|
|
() =>
|
|
cartItems
|
|
.filter((item) => !item.product?.isOutOfStock)
|
|
.reduce(
|
|
(sum, item) =>
|
|
sum +
|
|
(item.product?.price || 0) * (quantities[item.id] || item.quantity),
|
|
0
|
|
),
|
|
[cartItems, quantities]
|
|
);
|
|
|
|
const eligibleCoupons = useMemo(() => {
|
|
if (!couponsRaw?.data) return [];
|
|
return couponsRaw.data
|
|
.map((coupon) => {
|
|
let isEligible = true;
|
|
let ineligibilityReason = "";
|
|
if (
|
|
coupon.maxLimitForUser &&
|
|
coupon.usages.length >= coupon.maxLimitForUser
|
|
) {
|
|
isEligible = false;
|
|
ineligibilityReason = "Usage limit exceeded";
|
|
}
|
|
if (coupon.minOrder && parseFloat(coupon.minOrder) > baseTotalPrice) {
|
|
isEligible = false;
|
|
ineligibilityReason = `Min order ₹${coupon.minOrder}`;
|
|
}
|
|
return {
|
|
id: coupon.id,
|
|
code: coupon.couponCode,
|
|
discountType: coupon.discountPercent ? "percentage" : "flat",
|
|
discountValue: parseFloat(
|
|
coupon.discountPercent || coupon.flatDiscount || "0"
|
|
),
|
|
maxValue: coupon.maxValue ? parseFloat(coupon.maxValue) : undefined,
|
|
minOrder: coupon.minOrder ? parseFloat(coupon.minOrder) : undefined,
|
|
description: generateCouponDescription(coupon),
|
|
exclusiveApply: coupon.exclusiveApply,
|
|
isEligible,
|
|
ineligibilityReason: isEligible ? undefined : ineligibilityReason,
|
|
};
|
|
})
|
|
.filter(
|
|
(coupon) => coupon.ineligibilityReason !== "Usage limit exceeded"
|
|
);
|
|
}, [couponsRaw, baseTotalPrice]);
|
|
|
|
const updateCartItem = useUpdateCartItem({ showSuccessAlert: false, showErrorAlert: false, refetchCart: true }, cartType);
|
|
const removeFromCart = useRemoveFromCart({ showSuccessAlert: false, showErrorAlert: false, refetchCart: true }, cartType);
|
|
|
|
useMarkDataFetchers(() => {
|
|
refetchCart();
|
|
refetchSlots();
|
|
});
|
|
useManualRefresh(() => {
|
|
refetchCart();
|
|
refetchSlots();
|
|
});
|
|
const [selectedSlots, setSelectedSlots] = useState<Record<number, number>>({});
|
|
const [selectedCouponId, setSelectedCouponId] = useState<number | null>(null);
|
|
const [couponDialogOpen, setCouponDialogOpen] = useState(false);
|
|
|
|
const router = useRouter();
|
|
|
|
// Process slots: flatten and unique
|
|
const availableSlots = React.useMemo(() => {
|
|
if (!slotsData) return [];
|
|
const allSlots = Object.values(slotsData).flat();
|
|
const uniqueSlots = allSlots.filter(
|
|
(slot, index, self) => index === self.findIndex((s) => s.id === slot.id)
|
|
);
|
|
return uniqueSlots.map((slot) => ({
|
|
label: `Delivery: ${dayjs(slot.deliveryTime).format(
|
|
"ddd DD MMM, h:mm a"
|
|
)} - Close time: ${dayjs(slot.freezeTime).format("h:mm a")}`,
|
|
value: slot.id,
|
|
}));
|
|
}, [slotsData]);
|
|
|
|
// Get available slots for a specific product
|
|
const getAvailableSlotsForProduct = React.useMemo(() => {
|
|
return (productId: number) => {
|
|
if (!slotsData || !slotsData[productId]) return [];
|
|
return slotsData[productId].map((slot) => ({
|
|
label: `Delivery: ${dayjs(slot.deliveryTime).format(
|
|
"ddd DD MMM, h:mm a"
|
|
)} - Close time: ${dayjs(slot.freezeTime).format("h:mm a")}`,
|
|
value: slot.id,
|
|
}));
|
|
};
|
|
}, [slotsData]);
|
|
|
|
// Calculate coupon discount
|
|
const selectedCoupons = useMemo(
|
|
() =>
|
|
selectedCouponId ? eligibleCoupons?.filter((coupon) => coupon.id === selectedCouponId) : [],
|
|
[eligibleCoupons, selectedCouponId]
|
|
);
|
|
|
|
const totalPrice = cartItems
|
|
.filter((item) => !item.product?.isOutOfStock)
|
|
.reduce((sum, item) => {
|
|
const quantity = quantities[item.id] || item.quantity;
|
|
const price = isFlashDelivery ? (item.product?.flashPrice ?? item.product?.price ?? 0) : (item.product?.price || 0);
|
|
return sum + price * quantity;
|
|
}, 0);
|
|
const dropdownData = useMemo(
|
|
() =>
|
|
eligibleCoupons?.map((coupon) => {
|
|
const discount =
|
|
coupon.discountType === "percentage"
|
|
? Math.min(
|
|
(totalPrice * coupon.discountValue) / 100,
|
|
coupon.maxValue || Infinity
|
|
)
|
|
: Math.min(coupon.discountValue, coupon.maxValue || totalPrice);
|
|
const saveString = !isNaN(discount) ? ` (Save ₹${discount})` : "";
|
|
const baseLabel = `${coupon.code} - ${coupon.description}${coupon.isEligible ? saveString : ""
|
|
}`;
|
|
const label = coupon.isEligible
|
|
? baseLabel
|
|
: `${baseLabel} (${coupon.ineligibilityReason})`;
|
|
return {
|
|
label,
|
|
value: coupon.id,
|
|
disabled: !coupon.isEligible,
|
|
};
|
|
}) || [],
|
|
[eligibleCoupons, totalPrice]
|
|
);
|
|
|
|
const discountAmount = useMemo(
|
|
() =>
|
|
selectedCoupons?.reduce(
|
|
(sum, coupon) =>
|
|
sum +
|
|
(coupon.discountType === "percentage"
|
|
? Math.min(
|
|
(totalPrice * coupon.discountValue) / 100,
|
|
coupon.maxValue || Infinity
|
|
)
|
|
: Math.min(coupon.discountValue, coupon.maxValue || totalPrice)),
|
|
0
|
|
) || 0,
|
|
[selectedCoupons, totalPrice]
|
|
);
|
|
|
|
const finalTotal = totalPrice - discountAmount;
|
|
|
|
const deliveryCharge = useMemo(
|
|
() => {
|
|
const threshold = isFlashDelivery
|
|
? constsData?.flashFreeDeliveryThreshold
|
|
: constsData?.freeDeliveryThreshold;
|
|
const charge = isFlashDelivery
|
|
? constsData?.flashDeliveryCharge
|
|
: constsData?.deliveryCharge;
|
|
return finalTotal < threshold ? charge : 0;
|
|
},
|
|
[finalTotal, constsData, isFlashDelivery]
|
|
);
|
|
|
|
const finalTotalWithDelivery = finalTotal + deliveryCharge;
|
|
|
|
const hasAvailableItems = cartItems.some(item => !item.product?.isOutOfStock);
|
|
|
|
useEffect(() => {
|
|
const initial: Record<number, number> = {};
|
|
cartItems.forEach((item) => {
|
|
initial[item.id] = item.quantity;
|
|
});
|
|
setQuantities(initial);
|
|
}, [cartData]);
|
|
|
|
// Auto-select delivery slots for each cart item
|
|
useEffect(() => {
|
|
if (cartItems.length > 0) {
|
|
const newSelectedSlots = { ...selectedSlots };
|
|
|
|
cartItems.forEach(item => {
|
|
// Skip if already has a selected slot
|
|
if (selectedSlots[item.id]) return;
|
|
|
|
if (isFlashDelivery) {
|
|
// For flash delivery, always use slot 0
|
|
newSelectedSlots[item.id] = 0;
|
|
} else {
|
|
// For regular delivery, find earliest available slot
|
|
const productSlots = slotsData?.[item.productId];
|
|
if (!productSlots || productSlots.length === 0) return;
|
|
|
|
const now = dayjs();
|
|
const upcomingSlots = productSlots.filter(slot =>
|
|
dayjs(slot.deliveryTime).isAfter(now)
|
|
).sort((a, b) =>
|
|
dayjs(a.deliveryTime).diff(dayjs(b.deliveryTime))
|
|
);
|
|
|
|
if (upcomingSlots.length > 0) {
|
|
// Select the earliest available slot for this product
|
|
const earliestSlot = upcomingSlots[0];
|
|
newSelectedSlots[item.id] = earliestSlot.id;
|
|
}
|
|
}
|
|
});
|
|
|
|
setSelectedSlots(newSelectedSlots);
|
|
}
|
|
}, [slotsData, cartItems, isFlashDelivery]);
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<View style={tw`flex-1 justify-center items-center bg-gray-50`}>
|
|
<MyText style={tw`text-gray-500 font-medium`}>Loading cart...</MyText>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<View style={tw`flex-1 justify-center items-center bg-gray-50`}>
|
|
<MaterialIcons name="error-outline" size={48} color="#EF4444" />
|
|
<MyText style={tw`text-gray-900 text-lg font-bold mt-4`}>Oops!</MyText>
|
|
<MyText style={tw`text-gray-500 mt-2`}>Failed to load your cart</MyText>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={tw`flex-1 bg-gray-50`}>
|
|
{/* Fixed Cart Type Header */}
|
|
<View style={tw`bg-white px-4 py-3 border-b border-gray-100 mt-6`}>
|
|
<View style={tw`flex-row items-center`}>
|
|
<MyTouchableOpacity
|
|
onPress={() => router.back()}
|
|
style={tw`p-2 -ml-2 mr-1`}
|
|
activeOpacity={0.7}
|
|
>
|
|
<MaterialIcons name="chevron-left" size={28} color="#374151" />
|
|
</MyTouchableOpacity>
|
|
<MaterialIcons
|
|
name={isFlashDelivery ? "flash-on" : "schedule"}
|
|
size={24}
|
|
color={isFlashDelivery ? "#f81260" : "#374151"}
|
|
/>
|
|
<MyText style={[
|
|
tw`text-lg font-bold ml-2`,
|
|
{ color: isFlashDelivery ? '#f81260' : '#374151' }
|
|
]}>
|
|
{isFlashDelivery ? "Flash Delivery Cart" : "Scheduled Delivery Cart"}
|
|
</MyText>
|
|
</View>
|
|
</View>
|
|
|
|
<AppContainer>
|
|
<ScrollView
|
|
style={tw`flex-1`}
|
|
contentContainerStyle={tw`pb-32`}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
|
|
{/* Cart Items */}
|
|
{cartItems.length === 0 ? (
|
|
<View style={tw`items-center justify-center py-12`}>
|
|
<View
|
|
style={tw`w-20 h-20 bg-gray-100 rounded-full items-center justify-center mb-4`}
|
|
>
|
|
<MaterialIcons name="shopping-cart" size={32} color="#9CA3AF" />
|
|
</View>
|
|
<MyText style={tw`text-lg font-bold text-gray-900`}>
|
|
Your cart is empty
|
|
</MyText>
|
|
<MyText style={tw`text-gray-500 mt-2 text-center px-8`}>
|
|
Looks like you haven't added anything to your cart yet.
|
|
</MyText>
|
|
<MyTouchableOpacity
|
|
style={tw`mt-6 bg-brand500 px-6 py-3 rounded-xl`}
|
|
onPress={() => router.push("/(drawer)/(tabs)/home")}
|
|
>
|
|
<MyText style={tw`text-white font-bold`}>Start Shopping</MyText>
|
|
</MyTouchableOpacity>
|
|
</View>
|
|
) : (
|
|
<>
|
|
<View style={tw`bg-white rounded-2xl shadow-sm mb-4 border border-gray-100`}>
|
|
{cartItems.map((item, index) => {
|
|
const productSlots = getAvailableSlotsForProduct(item.productId);
|
|
const selectedSlotForItem = selectedSlots[item.id];
|
|
const isFlashEligible = isFlashDelivery ? flashEligibleProductIds.has(item.productId) : true;
|
|
// const isAvailable = (productSlots.length > 0 || isFlashDelivery) && !item.product?.isOutOfStock && isFlashEligible;
|
|
let isAvailable = true;
|
|
|
|
if(item.product?.isOutOfStock) {
|
|
isAvailable = false;
|
|
} else if(isFlashDelivery) {
|
|
if(!isFlashEligible) {
|
|
isAvailable = false;
|
|
}
|
|
} else {
|
|
if(productSlots.length === 0) {
|
|
isAvailable = false;
|
|
}
|
|
}
|
|
// if (item.product?.isOutOfStock) {
|
|
// isAvailable = false;
|
|
// } else if (isFlashDelivery) {
|
|
// isAvailable = isFlashEligible;
|
|
// }
|
|
const quantity = quantities[item.id] || item.quantity;
|
|
const price = isFlashDelivery ? (item.product?.flashPrice ?? item.product?.price ?? 0) : (item.product?.price || 0);
|
|
const itemPrice = price * quantity;
|
|
|
|
return (
|
|
<View key={item.id}>
|
|
<View style={tw`p-4`}>
|
|
<View style={tw`flex-row items-center mb-2`}>
|
|
<Image
|
|
source={{ uri: item.product.images?.[0] }}
|
|
style={tw`w-8 h-8 rounded-lg bg-gray-100 mr-3`}
|
|
/>
|
|
|
|
<MyText
|
|
style={tw`text-sm text-gray-900 flex-1 mr-3`}
|
|
numberOfLines={2}
|
|
>
|
|
{item.product.name}
|
|
</MyText>
|
|
<MyText style={tw`text-xs text-gray-500 mr-2`}>
|
|
{(() => {
|
|
const qty = item.product?.productQuantity || 1;
|
|
const unit = item.product?.unit || '';
|
|
if (unit?.toLowerCase() === 'kg' && qty < 1) {
|
|
return `${Math.round(qty * 1000)}g`;
|
|
}
|
|
return `${qty}${unit}`;
|
|
})()}
|
|
</MyText>
|
|
|
|
<View style={tw`flex-row items-center w-32 justify-end`}>
|
|
<View style={[tw`w-12`, { width: 120 }]}>
|
|
<Quantifier
|
|
value={quantities[item.id] || item.quantity}
|
|
setValue={(value) => {
|
|
if (value === 0) {
|
|
// Show confirmation alert before removing item
|
|
Alert.alert(
|
|
"Remove Item",
|
|
"Are you sure you want to remove this item from your cart?",
|
|
[
|
|
{
|
|
text: "Cancel",
|
|
style: "cancel",
|
|
onPress: () => {
|
|
// Reset quantity back to 1
|
|
setQuantities((prev) => ({ ...prev, [item.id]: 1 }));
|
|
}
|
|
},
|
|
{
|
|
text: "Remove",
|
|
style: "destructive",
|
|
onPress: () => {
|
|
// Proceed with removal
|
|
removeFromCart.mutate(
|
|
{ itemId: item.id },
|
|
{
|
|
onSuccess: () => {
|
|
refetchCart();
|
|
},
|
|
onError: (error: any) => {
|
|
Alert.alert("Error", error.message || "Failed to remove item");
|
|
// Restore quantity on error
|
|
setQuantities((prev) => ({ ...prev, [item.id]: 1 }));
|
|
},
|
|
}
|
|
);
|
|
}
|
|
}
|
|
]
|
|
);
|
|
} else {
|
|
// Update quantity normally
|
|
setQuantities((prev) => ({
|
|
...prev,
|
|
[item.id]: value,
|
|
}));
|
|
updateCartItem.mutate({
|
|
itemId: item.id,
|
|
quantity: value,
|
|
});
|
|
}
|
|
}}
|
|
step={item.product.incrementStep}
|
|
unit={item.product?.unit}
|
|
/>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Delivery Slot Selection per Product - Hidden for Flash Delivery */}
|
|
{!isFlashDelivery && (
|
|
<View style={tw`flex flex-row items-center justify-between mt-2`}>
|
|
<View style={tw`flex-1 mr-4`}>
|
|
<BottomDropdown
|
|
label="Select Delivery Slot"
|
|
options={productSlots}
|
|
value={selectedSlotForItem || ""}
|
|
onValueChange={(value) => {
|
|
setSelectedSlots((prev) => ({
|
|
...prev,
|
|
[item.id]: Number(value)
|
|
}));
|
|
}}
|
|
disabled={productSlots.length === 0}
|
|
|
|
triggerComponent={({ onPress, disabled, displayText }) => {
|
|
const selectedSlotForItem = selectedSlots[item.id];
|
|
const selectedSlot = productSlots.find(slot => slot.value === selectedSlotForItem);
|
|
|
|
const deliveryTimeText = selectedSlot
|
|
? selectedSlot.label.split(' - ')[0].replace('Delivery: ', '')
|
|
: null;
|
|
|
|
return (
|
|
<View style={tw`flex-row items-center py-1`}>
|
|
<MaterialIcons
|
|
name="local-shipping"
|
|
size={14}
|
|
color="#6B7280"
|
|
style={tw`mr-1`}
|
|
/>
|
|
<MyText style={tw`text-sm text-gray-900 mr-2`}>
|
|
{deliveryTimeText || (productSlots.length === 0
|
|
? "No delivery slots available"
|
|
: "Choose delivery slot")}
|
|
</MyText>
|
|
<MyTouchableOpacity
|
|
onPress={onPress}
|
|
disabled={disabled}
|
|
activeOpacity={0.7}
|
|
>
|
|
<MyText style={tw`text-sm text-brand500 font-medium`}>
|
|
Change
|
|
</MyText>
|
|
</MyTouchableOpacity>
|
|
</View>
|
|
);
|
|
}}
|
|
/>
|
|
</View>
|
|
<View style={tw`justify-end`}>
|
|
<View style={tw`flex-row items-center gap-2`}>
|
|
<MyText style={tw`text-md font-medium text-brand900`}>
|
|
₹{itemPrice}
|
|
</MyText>
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
Alert.alert(
|
|
"Remove Item",
|
|
`Remove ${item.product.name} from cart?`,
|
|
[
|
|
{ text: "Cancel", style: "cancel" },
|
|
{
|
|
text: "Remove",
|
|
style: "destructive",
|
|
onPress: () => {
|
|
removeFromCart.mutate(
|
|
{ itemId: item.id },
|
|
{
|
|
onSuccess: () => {
|
|
refetchCart();
|
|
},
|
|
onError: (error: any) => {
|
|
Alert.alert(
|
|
"Error",
|
|
error.message ||
|
|
"Failed to remove item"
|
|
);
|
|
},
|
|
}
|
|
);
|
|
},
|
|
},
|
|
]
|
|
);
|
|
}}
|
|
style={tw`p-1`}
|
|
>
|
|
<MaterialIcons
|
|
name="delete"
|
|
size={20}
|
|
color="#EF4444"
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Price for Flash Delivery (already in same row as slot) */}
|
|
{isFlashDelivery && (
|
|
<View style={tw`flex-row justify-end mt-2`}>
|
|
<View style={tw`flex-row items-center gap-2`}>
|
|
<MyText style={tw`text-md font-medium text-brand900`}>
|
|
₹{itemPrice}
|
|
</MyText>
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
Alert.alert(
|
|
"Remove Item",
|
|
`Remove ${item.product.name} from cart?`,
|
|
[
|
|
{ text: "Cancel", style: "cancel" },
|
|
{
|
|
text: "Remove",
|
|
style: "destructive",
|
|
onPress: () => {
|
|
removeFromCart.mutate(
|
|
{ itemId: item.id },
|
|
{
|
|
onSuccess: () => {
|
|
refetchCart();
|
|
},
|
|
onError: (error: any) => {
|
|
Alert.alert(
|
|
"Error",
|
|
error.message ||
|
|
"Failed to remove item"
|
|
);
|
|
},
|
|
}
|
|
);
|
|
},
|
|
},
|
|
]
|
|
);
|
|
}}
|
|
style={tw`p-1`}
|
|
>
|
|
<MaterialIcons
|
|
name="delete"
|
|
size={20}
|
|
color="#EF4444"
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{!isAvailable && (
|
|
<View
|
|
style={tw`bg-red-50 self-start px-2 py-1 rounded-md mt-2`}
|
|
>
|
|
<MyText style={tw`text-xs font-bold text-red-600`}>
|
|
{item.product?.isOutOfStock
|
|
? "Out of Stock"
|
|
: isFlashDelivery && !flashEligibleProductIds.has(item.productId)
|
|
? "Not available for flash delivery. Please remove"
|
|
: "No delivery slots available"}
|
|
</MyText>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{/* Gray horizontal line between items (except for the last item) */}
|
|
{index < cartItems.length - 1 && (
|
|
<View style={tw`h-px bg-gray-200 mx-4`} />
|
|
)}
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
</>
|
|
)}
|
|
|
|
{/* Cart Type Switcher */}
|
|
{cartItems.length > 0 && (
|
|
<View style={tw`bg-white rounded-2xl shadow-sm mb-4 border border-gray-100 overflow-hidden`}>
|
|
<View style={tw`p-4`}>
|
|
{/* First row: Text content */}
|
|
<View style={tw`flex-row items-center mb-3`}>
|
|
<View style={tw`w-10 h-10 rounded-full items-center justify-center mr-3 ${isFlashDelivery ? 'bg-orange-100' : 'bg-blue-100'}`}>
|
|
<MaterialIcons
|
|
name={isFlashDelivery ? "flash-on" : "schedule"}
|
|
size={20}
|
|
color={isFlashDelivery ? "#EA580C" : "#2563EB"}
|
|
/>
|
|
</View>
|
|
<View style={tw`flex-1`}>
|
|
<MyText style={tw`text-base font-bold text-gray-900`}>
|
|
{isFlashDelivery ? "Flash Delivery" : "Scheduled Delivery"}
|
|
</MyText>
|
|
<MyText style={tw`text-sm text-gray-500 mt-0.5`}>
|
|
{isFlashDelivery
|
|
? "30 min delivery • Immediate pickup"
|
|
: "Choose your preferred delivery time"
|
|
}
|
|
</MyText>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Second row: Navigation trigger */}
|
|
<MyTouchableOpacity
|
|
style={tw`bg-gray-100 px-4 py-2 rounded-lg flex-row items-center self-start`}
|
|
onPress={() => {
|
|
if (isFlashDelivery) {
|
|
// Switch from flash to scheduled delivery
|
|
router.push("/(drawer)/(tabs)/home" as any);
|
|
} else {
|
|
// Switch from scheduled to flash delivery
|
|
router.push("/(drawer)/(tabs)/flash-delivery/(products)");
|
|
}
|
|
}}
|
|
activeOpacity={0.8}
|
|
>
|
|
<MyText style={tw`text-gray-700 font-medium mr-1`}>
|
|
{isFlashDelivery ? "Go to Scheduled Delivery" : "Go to Flash Delivery"}
|
|
</MyText>
|
|
<MaterialIcons
|
|
name="arrow-forward"
|
|
size={16}
|
|
color="#374151"
|
|
/>
|
|
</MyTouchableOpacity>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
{/* Coupon Selection */}
|
|
{hasAvailableItems && (
|
|
<View
|
|
style={tw`bg-white p-5 rounded-2xl shadow-sm mb-4 border border-gray-100`}
|
|
>
|
|
<View style={tw`flex-row items-center mb-3`}>
|
|
<View
|
|
style={tw`w-8 h-8 bg-pink-50 rounded-full items-center justify-center mr-3`}
|
|
>
|
|
<MaterialIcons name="local-offer" size={18} color="#EC4899" />
|
|
</View>
|
|
<MyText style={tw`text-base font-bold text-gray-900`}>
|
|
Offers & Coupons
|
|
</MyText>
|
|
</View>
|
|
|
|
<BottomDropdown
|
|
label="Available Coupons"
|
|
options={dropdownData}
|
|
value={selectedCouponId || ""}
|
|
disabled={eligibleCoupons.length === 0}
|
|
onValueChange={(value) => {
|
|
setSelectedCouponId(value ? Number(value) : null);
|
|
}}
|
|
placeholder={
|
|
eligibleCoupons.length === 0
|
|
? "No coupons available"
|
|
: "Select a coupon"
|
|
}
|
|
/>
|
|
|
|
{eligibleCoupons.length === 0 && (
|
|
<MyText style={tw`text-gray-400 text-xs mt-2 ml-1`}>
|
|
No coupons available for this order
|
|
</MyText>
|
|
)}
|
|
|
|
|
|
|
|
{selectedCouponId && (
|
|
<MyTouchableOpacity
|
|
style={tw`mt-3 self-end`}
|
|
onPress={() => setSelectedCouponId(null)}
|
|
>
|
|
<MyText style={tw`text-gray-400 text-xs font-medium`}>
|
|
Remove coupon
|
|
</MyText>
|
|
</MyTouchableOpacity>
|
|
)}
|
|
</View>
|
|
)}
|
|
|
|
<TestingPhaseNote />
|
|
|
|
{/* Bottom Checkout Bar - Now Static */}
|
|
{hasAvailableItems && (
|
|
<View style={tw`bg-white mt-4 rounded-2xl shadow-sm border border-gray-100 overflow-hidden mb-6`}>
|
|
|
|
{/* Bill Header */}
|
|
<View style={tw`bg-gray-50 px-4 py-3 border-b border-gray-100`}>
|
|
<MyText style={tw`text-gray-900 font-bold text-base`}>Bill Details</MyText>
|
|
</View>
|
|
|
|
<View style={tw`p-4`}>
|
|
{/* Item Total */}
|
|
<View style={tw`flex-row justify-between items-center mb-3`}>
|
|
<MyText style={tw`text-gray-500`}>Item Total</MyText>
|
|
<MyText style={tw`text-gray-900 font-medium`}>₹{totalPrice}</MyText>
|
|
</View>
|
|
|
|
{/* Discount */}
|
|
{discountAmount > 0 && (
|
|
<View style={tw`flex-row justify-between items-center mb-3`}>
|
|
<MyText style={tw`text-gray-500`}>Product Discount</MyText>
|
|
<MyText style={tw`text-green-600 font-medium`}>-₹{discountAmount}</MyText>
|
|
</View>
|
|
)}
|
|
|
|
{/* Applied Coupon */}
|
|
{selectedCoupons.length > 0 && (
|
|
<View style={tw`flex-row justify-between items-center mb-3`}>
|
|
<MyText style={tw`text-gray-500`}>Coupon Applied</MyText>
|
|
<MyTouchableOpacity onPress={() => setCouponDialogOpen(true)}>
|
|
<MyText style={tw`text-pink-600 font-medium`}>{selectedCoupons[0].code}</MyText>
|
|
</MyTouchableOpacity>
|
|
</View>
|
|
)}
|
|
|
|
{/* Delivery Fee */}
|
|
<View style={tw`flex-row justify-between items-center mb-3`}>
|
|
<View style={tw`flex-row items-center`}>
|
|
<MyText style={tw`text-gray-500`}>Delivery Fee</MyText>
|
|
<MaterialIcons name="info-outline" size={14} color="#9CA3AF" style={tw`ml-1`} />
|
|
</View>
|
|
<View style={tw`flex-row items-center`}>
|
|
{deliveryCharge === 0 && (constsData?.deliveryCharge || 0) > 0 && (
|
|
<MyText style={tw`text-gray-400 line-through mr-2 text-xs`}>
|
|
₹{constsData?.deliveryCharge}
|
|
</MyText>
|
|
)}
|
|
<MyText style={tw`${deliveryCharge === 0 ? 'text-green-600' : 'text-gray-900'} font-medium`}>
|
|
{deliveryCharge === 0 ? 'Free' : `₹${deliveryCharge}`}
|
|
</MyText>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Free Delivery Nudge */}
|
|
{deliveryCharge > 0 && (constsData?.freeDeliveryThreshold || 0) > 0 && finalTotal < (constsData?.freeDeliveryThreshold || 0) && (
|
|
<View style={tw`bg-blue-50 p-2.5 rounded-lg mb-3 flex-row items-center`}>
|
|
<MaterialIcons name="shopping-bag" size={16} color="#2563EB" style={tw`mr-2`} />
|
|
<MyText style={tw`text-blue-700 text-xs font-medium flex-1`}>
|
|
Add products worth ₹{((constsData?.freeDeliveryThreshold || 0) - finalTotal).toFixed(0)} for free delivery
|
|
</MyText>
|
|
</View>
|
|
)}
|
|
|
|
{/* Divider */}
|
|
<View style={tw`h-px bg-gray-100 my-2`} />
|
|
|
|
{/* Grand Total */}
|
|
<View style={tw`flex-row justify-between items-center mt-2`}>
|
|
<MyText style={tw`text-lg font-bold text-gray-900`}>To Pay</MyText>
|
|
<MyText style={tw`text-xl font-bold text-gray-900`}>₹{finalTotalWithDelivery}</MyText>
|
|
</View>
|
|
|
|
{/* Savings Banner */}
|
|
{(discountAmount > 0 || deliveryCharge === 0) && (
|
|
<View style={tw`bg-green-50 rounded-lg p-2 mt-4 flex-row justify-center items-center`}>
|
|
<MaterialIcons name="stars" size={16} color="#059669" style={tw`mr-1.5`} />
|
|
<MyText style={tw`text-green-700 text-xs font-bold`}>
|
|
You saved ₹{discountAmount + (deliveryCharge === 0 ? (constsData?.deliveryCharge || 0) : 0)} on this order
|
|
</MyText>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{/* Action Buttons */}
|
|
<View style={tw`p-4 border-t border-gray-100 bg-gray-50`}>
|
|
<View style={tw`flex-row gap-3`}>
|
|
<MyTouchableOpacity
|
|
style={tw`flex-1 bg-white border border-gray-200 py-3.5 rounded-xl items-center shadow-sm`}
|
|
onPress={() => router.push(isFlashDelivery ? "/(drawer)/(tabs)/flash-delivery" : "/(drawer)/(tabs)/home")}
|
|
>
|
|
<MyText style={tw`text-gray-700 font-bold`}>Shop More</MyText>
|
|
</MyTouchableOpacity>
|
|
<MyTouchableOpacity
|
|
style={tw`flex-1 bg-brand500 py-3.5 rounded-xl items-center shadow-md flex-row justify-center`}
|
|
onPress={() => {
|
|
const availableItems = cartItems
|
|
.filter(item => {
|
|
if (item.product?.isOutOfStock) return false;
|
|
if (isFlashDelivery) {
|
|
// Check if product supports flash delivery
|
|
return flashEligibleProductIds.has(item.productId);
|
|
}
|
|
return selectedSlots[item.id]; // Regular delivery requires slot selection
|
|
})
|
|
.map(item => item.id);
|
|
|
|
|
|
|
|
if (availableItems.length === 0) {
|
|
// Determine why no items are available
|
|
const outOfStockItems = cartItems.filter(item => item.product?.isOutOfStock);
|
|
const inStockItems = cartItems.filter(item => !item.product?.isOutOfStock);
|
|
|
|
let errorTitle = "Cannot Proceed";
|
|
let errorMessage = "";
|
|
|
|
if (outOfStockItems.length === cartItems.length) {
|
|
// All items are out of stock
|
|
errorTitle = "Items Unavailable";
|
|
errorMessage = "All items in your cart are currently out of stock. Please remove them and add available items.";
|
|
} else if (isFlashDelivery) {
|
|
// Check if any items are flash-eligible
|
|
const flashEligibleItems = inStockItems.filter(item =>
|
|
flashEligibleProductIds.has(item.productId)
|
|
);
|
|
if (flashEligibleItems.length === 0) {
|
|
errorTitle = "Flash Delivery Unavailable";
|
|
errorMessage = "None of the items in your cart are available for flash delivery. Please remove ineligible items or switch to regular delivery.";
|
|
} else {
|
|
errorTitle = "Some Items Not Available";
|
|
errorMessage = "Some items in your cart are not available for flash delivery. You can proceed with only the eligible items, or remove ineligible items.";
|
|
}
|
|
} else {
|
|
// Regular delivery - check slot selection
|
|
const itemsWithoutSlots = inStockItems.filter(item => !selectedSlots[item.id]);
|
|
if (itemsWithoutSlots.length > 0) {
|
|
errorTitle = "Delivery Slot Required";
|
|
errorMessage = `${itemsWithoutSlots.length} item(s) don't have a delivery slot. Please remove them or select a slot for each item.`;
|
|
} else {
|
|
errorTitle = "Cannot Proceed";
|
|
errorMessage = "Please check your cart items and try again.";
|
|
}
|
|
}
|
|
|
|
Alert.alert(errorTitle, errorMessage);
|
|
return;
|
|
}
|
|
|
|
// Check if there are items without slots (for regular delivery)
|
|
if (!isFlashDelivery && availableItems.length < cartItems.length) {
|
|
const itemsWithoutSlots = cartItems.filter(item => !selectedSlots[item.id] && !item.product?.isOutOfStock);
|
|
if (itemsWithoutSlots.length > 0) {
|
|
Alert.alert(
|
|
"Delivery Slot Required",
|
|
`${itemsWithoutSlots.length} item(s) don't have a delivery slot. Please select a slot or remove the item.`
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Group items by selected slot
|
|
const itemsBySlot: Record<number, number[]> = {};
|
|
availableItems.forEach(itemId => {
|
|
const slotId = isFlashDelivery ? 0 : selectedSlots[itemId];
|
|
if (!itemsBySlot[slotId]) {
|
|
itemsBySlot[slotId] = [];
|
|
}
|
|
itemsBySlot[slotId].push(itemId);
|
|
});
|
|
|
|
// Create checkout URL with slot groupings
|
|
const slotParams = Object.entries(itemsBySlot)
|
|
.map(([slotId, itemIds]) => `${slotId}:${itemIds.join(',')}`)
|
|
.join(';');
|
|
|
|
router.push(
|
|
`${isFlashDelivery ? '/(drawer)/(tabs)/flash-delivery/checkout' : '/(drawer)/(tabs)/home/checkout'}?slots=${encodeURIComponent(slotParams)}${selectedCouponId ? `&coupons=${selectedCouponId}` : ''}&deliveryPrice=${deliveryCharge}` as any
|
|
);
|
|
}}
|
|
>
|
|
<MyText style={tw`text-white font-bold mr-1`}>Checkout</MyText>
|
|
<MaterialIcons name="arrow-forward" size={18} color="white" />
|
|
</MyTouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
)}
|
|
</ScrollView>
|
|
</AppContainer>
|
|
|
|
{/* Coupon Details Dialog */}
|
|
<BottomDialog
|
|
open={couponDialogOpen}
|
|
onClose={() => setCouponDialogOpen(false)}
|
|
>
|
|
<View style={tw`p-6`}>
|
|
<View style={tw`flex-row items-center mb-6`}>
|
|
<View
|
|
style={tw`w-10 h-10 bg-pink-50 rounded-full items-center justify-center mr-3`}
|
|
>
|
|
<MaterialIcons name="local-offer" size={20} color="#EC4899" />
|
|
</View>
|
|
<MyText style={tw`text-xl font-bold text-gray-900`}>
|
|
Applied Coupons
|
|
</MyText>
|
|
</View>
|
|
|
|
{selectedCoupons.map((coupon) => (
|
|
<View
|
|
key={coupon.id}
|
|
style={tw`mb-4 p-4 bg-white border border-gray-100 rounded-xl shadow-sm`}
|
|
>
|
|
<View style={tw`flex-row justify-between items-start`}>
|
|
<View>
|
|
<MyText style={tw`font-bold text-gray-900 text-lg`}>
|
|
{coupon.code}
|
|
</MyText>
|
|
<MyText style={tw`text-sm text-gray-500 mt-1`}>
|
|
{coupon.description}
|
|
</MyText>
|
|
</View>
|
|
<View style={tw`bg-green-50 px-2 py-1 rounded`}>
|
|
<MyText style={tw`text-xs font-bold text-green-700`}>
|
|
{coupon.discountType === "percentage"
|
|
? `${coupon.discountValue}% OFF`
|
|
: `₹${coupon.discountValue} OFF`}
|
|
</MyText>
|
|
</View>
|
|
</View>
|
|
{coupon.maxValue && (
|
|
<MyText style={tw`text-xs text-gray-400 mt-2`}>
|
|
Maximum discount up to ₹{coupon.maxValue}
|
|
</MyText>
|
|
)}
|
|
</View>
|
|
))}
|
|
|
|
<MyTouchableOpacity
|
|
style={tw`mt-4 bg-gray-900 py-3.5 rounded-xl items-center`}
|
|
onPress={() => setCouponDialogOpen(false)}
|
|
>
|
|
<MyText style={tw`text-white font-bold`}>Close</MyText>
|
|
</MyTouchableOpacity>
|
|
</View>
|
|
</BottomDialog>
|
|
</View>
|
|
);
|
|
} |