freshyo/apps/user-ui/components/cart-page.tsx
2026-01-24 00:13:15 +05:30

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&apos;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>
);
}