diff --git a/apps/backend/src/stores/slot-store.ts b/apps/backend/src/stores/slot-store.ts index eed9c7e..448a8a3 100644 --- a/apps/backend/src/stores/slot-store.ts +++ b/apps/backend/src/stores/slot-store.ts @@ -24,6 +24,12 @@ interface SlotWithProducts { }>; } +interface SlotInfo { + id: number; + deliveryTime: Date; + freezeTime: Date; +} + export async function initializeSlotStore(): Promise { try { console.log('Initializing slot store in Redis...'); @@ -82,6 +88,28 @@ export async function initializeSlotStore(): Promise { await redisClient.set(`slot:${slot.id}`, JSON.stringify(slot)); } + // Build and store product-slots map + // Group slots by productId + const productSlotsMap: Record = {}; + + for (const slot of slotsWithProducts) { + for (const product of slot.products) { + if (!productSlotsMap[product.id]) { + productSlotsMap[product.id] = []; + } + productSlotsMap[product.id].push({ + id: slot.id, + deliveryTime: slot.deliveryTime, + freezeTime: slot.freezeTime, + }); + } + } + + // Store each product's slots in Redis with key pattern "product:{id}:slots" + for (const [productId, slotInfos] of Object.entries(productSlotsMap)) { + await redisClient.set(`product:${productId}:slots`, JSON.stringify(slotInfos)); + } + console.log('Slot store initialized successfully'); } catch (error) { console.error('Error initializing slot store:', error); @@ -122,4 +150,71 @@ export async function getAllSlots(): Promise { console.error('Error getting all slots:', error); return []; } +} + +export async function getProductSlots(productId: number): Promise { + try { + const key = `product:${productId}:slots`; + const data = await redisClient.get(key); + if (!data) return []; + return JSON.parse(data) as SlotInfo[]; + } catch (error) { + console.error(`Error getting slots for product ${productId}:`, error); + return []; + } +} + +export async function getAllProductsSlots(): Promise> { + try { + // Get all keys matching the pattern "product:*:slots" + const keys = await redisClient.KEYS('product:*:slots'); + + if (keys.length === 0) return {}; + + // Get all product slots using MGET for better performance + const productsData = await redisClient.MGET(keys); + + const result: Record = {}; + for (const key of keys) { + // Extract productId from key "product:{id}:slots" + const match = key.match(/product:(\d+):slots/); + if (match) { + const productId = parseInt(match[1], 10); + const dataIndex = keys.indexOf(key); + if (productsData[dataIndex]) { + result[productId] = JSON.parse(productsData[dataIndex]) as SlotInfo[]; + } + } + } + + return result; + } catch (error) { + console.error('Error getting all products slots:', error); + return {}; + } +} + +export async function getMultipleProductsSlots(productIds: number[]): Promise> { + try { + if (productIds.length === 0) return {}; + + // Build keys for all productIds + const keys = productIds.map(id => `product:${id}:slots`); + + // Use MGET for batch retrieval + const productsData = await redisClient.MGET(keys); + + const result: Record = {}; + for (let i = 0; i < productIds.length; i++) { + const data = productsData[i]; + if (data) { + result[productIds[i]] = JSON.parse(data) as SlotInfo[]; + } + } + + return result; + } catch (error) { + console.error('Error getting products slots:', error); + return {}; + } } \ No newline at end of file diff --git a/apps/backend/src/trpc/user-apis/cart.ts b/apps/backend/src/trpc/user-apis/cart.ts index c4a8952..9cd0cd9 100644 --- a/apps/backend/src/trpc/user-apis/cart.ts +++ b/apps/backend/src/trpc/user-apis/cart.ts @@ -5,6 +5,7 @@ import { cartItems, productInfo, units, productSlots, deliverySlotInfo } from '. import { eq, and, sql, inArray, gt } from 'drizzle-orm'; import { ApiError } from '../../lib/api-error'; import { generateSignedUrlsFromS3Urls } from '../../lib/s3-client'; +import { getProductSlots, getMultipleProductsSlots } from '../../stores/slot-store'; interface CartResponse { items: any[]; @@ -181,6 +182,52 @@ export const cartRouter = router({ }; }), + // Original DB-based getCartSlots (commented out) + // getCartSlots: publicProcedure + // .input(z.object({ + // productIds: z.array(z.number().int().positive()) + // })) + // .query(async ({ input }) => { + // const { productIds } = input; + // + // if (productIds.length === 0) { + // return {}; + // } + // + // // Get slots for these products where freeze time is after current time + // const slotsData = await db + // .select({ + // productId: productSlots.productId, + // slotId: deliverySlotInfo.id, + // deliveryTime: deliverySlotInfo.deliveryTime, + // freezeTime: deliverySlotInfo.freezeTime, + // isActive: deliverySlotInfo.isActive, + // }) + // .from(productSlots) + // .innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id)) + // .where(and( + // inArray(productSlots.productId, productIds), + // gt(deliverySlotInfo.freezeTime, sql`NOW()`), + // eq(deliverySlotInfo.isActive, true) + // )); + // + // // Group by productId + // const result: Record = {}; + // slotsData.forEach(slot => { + // if (!result[slot.productId]) { + // result[slot.productId] = []; + // } + // result[slot.productId].push({ + // id: slot.slotId, + // deliveryTime: slot.deliveryTime, + // freezeTime: slot.freezeTime, + // }); + // }); + // + // return result; + // }), + + // Cache-based getCartSlots getCartSlots: publicProcedure .input(z.object({ productIds: z.array(z.number().int().positive()) @@ -192,36 +239,6 @@ export const cartRouter = router({ return {}; } - // Get slots for these products where freeze time is after current time - const slotsData = await db - .select({ - productId: productSlots.productId, - slotId: deliverySlotInfo.id, - deliveryTime: deliverySlotInfo.deliveryTime, - freezeTime: deliverySlotInfo.freezeTime, - isActive: deliverySlotInfo.isActive, - }) - .from(productSlots) - .innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id)) - .where(and( - inArray(productSlots.productId, productIds), - gt(deliverySlotInfo.freezeTime, sql`NOW()`), - eq(deliverySlotInfo.isActive, true) - )); - - // Group by productId - const result: Record = {}; - slotsData.forEach(slot => { - if (!result[slot.productId]) { - result[slot.productId] = []; - } - result[slot.productId].push({ - id: slot.slotId, - deliveryTime: slot.deliveryTime, - freezeTime: slot.freezeTime, - }); - }); - - return result; + return await getMultipleProductsSlots(productIds); }), }); \ No newline at end of file diff --git a/apps/user-ui/components/cart-page.tsx b/apps/user-ui/components/cart-page.tsx index 1160e9d..1906be4 100644 --- a/apps/user-ui/components/cart-page.tsx +++ b/apps/user-ui/components/cart-page.tsx @@ -161,37 +161,6 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { 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) - ); - - // Smart time window formatting function - const formatTimeRange = (deliveryTime: string) => { - const time = dayjs(deliveryTime); - const endTime = time.add(1, 'hour'); - const startPeriod = time.format('A'); - const endPeriod = endTime.format('A'); - - let timeRange; - if (startPeriod === endPeriod) { - timeRange = `${time.format('h')}-${endTime.format('h')} ${startPeriod}`; - } else { - timeRange = `${time.format('h:mm')} ${startPeriod} - ${endTime.format('h:mm')} ${endPeriod}`; - } - - return `${time.format('ddd, DD MMM ')}${timeRange}`; - }; - - return uniqueSlots.map((slot) => ({ - label: `Delivery: ${formatTimeRange(slot.deliveryTime)} - 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) => { @@ -330,7 +299,6 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) { ); if (upcomingSlots.length > 0) { - console.log({upcomingSlots, existingSlotId, cartSlotIds}) if (existingSlotId) { const slotStillValid = upcomingSlots.some(slot => slot.id === existingSlotId);