enh
This commit is contained in:
parent
46f5fa180c
commit
2929e7725a
3 changed files with 143 additions and 63 deletions
|
|
@ -24,6 +24,12 @@ interface SlotWithProducts {
|
|||
}>;
|
||||
}
|
||||
|
||||
interface SlotInfo {
|
||||
id: number;
|
||||
deliveryTime: Date;
|
||||
freezeTime: Date;
|
||||
}
|
||||
|
||||
export async function initializeSlotStore(): Promise<void> {
|
||||
try {
|
||||
console.log('Initializing slot store in Redis...');
|
||||
|
|
@ -82,6 +88,28 @@ export async function initializeSlotStore(): Promise<void> {
|
|||
await redisClient.set(`slot:${slot.id}`, JSON.stringify(slot));
|
||||
}
|
||||
|
||||
// Build and store product-slots map
|
||||
// Group slots by productId
|
||||
const productSlotsMap: Record<number, SlotInfo[]> = {};
|
||||
|
||||
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<SlotWithProducts[]> {
|
|||
console.error('Error getting all slots:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProductSlots(productId: number): Promise<SlotInfo[]> {
|
||||
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<Record<number, SlotInfo[]>> {
|
||||
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<number, SlotInfo[]> = {};
|
||||
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<Record<number, SlotInfo[]>> {
|
||||
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<number, SlotInfo[]> = {};
|
||||
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 {};
|
||||
}
|
||||
}
|
||||
|
|
@ -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<number, any[]> = {};
|
||||
// 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<number, any[]> = {};
|
||||
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);
|
||||
}),
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue