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> {
|
export async function initializeSlotStore(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
console.log('Initializing slot store in Redis...');
|
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));
|
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');
|
console.log('Slot store initialized successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing slot store:', 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);
|
console.error('Error getting all slots:', error);
|
||||||
return [];
|
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 { eq, and, sql, inArray, gt } from 'drizzle-orm';
|
||||||
import { ApiError } from '../../lib/api-error';
|
import { ApiError } from '../../lib/api-error';
|
||||||
import { generateSignedUrlsFromS3Urls } from '../../lib/s3-client';
|
import { generateSignedUrlsFromS3Urls } from '../../lib/s3-client';
|
||||||
|
import { getProductSlots, getMultipleProductsSlots } from '../../stores/slot-store';
|
||||||
|
|
||||||
interface CartResponse {
|
interface CartResponse {
|
||||||
items: any[];
|
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
|
getCartSlots: publicProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
productIds: z.array(z.number().int().positive())
|
productIds: z.array(z.number().int().positive())
|
||||||
|
|
@ -192,36 +239,6 @@ export const cartRouter = router({
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get slots for these products where freeze time is after current time
|
return await getMultipleProductsSlots(productIds);
|
||||||
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;
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
@ -161,37 +161,6 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
||||||
|
|
||||||
const router = useRouter();
|
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
|
// Get available slots for a specific product
|
||||||
const getAvailableSlotsForProduct = React.useMemo(() => {
|
const getAvailableSlotsForProduct = React.useMemo(() => {
|
||||||
return (productId: number) => {
|
return (productId: number) => {
|
||||||
|
|
@ -330,7 +299,6 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (upcomingSlots.length > 0) {
|
if (upcomingSlots.length > 0) {
|
||||||
console.log({upcomingSlots, existingSlotId, cartSlotIds})
|
|
||||||
|
|
||||||
if (existingSlotId) {
|
if (existingSlotId) {
|
||||||
const slotStillValid = upcomingSlots.some(slot => slot.id === existingSlotId);
|
const slotStillValid = upcomingSlots.some(slot => slot.id === existingSlotId);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue