mostly functional apps

This commit is contained in:
shafi54 2026-07-31 18:19:07 +05:30
parent ad714493fc
commit 5c2b3aaa67
25 changed files with 395 additions and 426 deletions

View file

@ -48,7 +48,9 @@ const formatOrderMessageWithFullData = (ordersData: any[]): string => {
message += '📦 <b>Items:</b>\n';
order.orderItems?.forEach((item: any) => {
message += `${item.product?.name || 'Unknown'}${item.product.productQuantity}${item.product.unit?.shortNotation}x${item.quantity}\n`;
const sku = item.sku
const features = (sku?.features || []).map((f: any) => f.featureValue).join(' ')
message += `${sku?.product?.name || 'Unknown'} ${features} x${item.quantity}\n`;
});
message += `\n💰 <b>Total:</b> ₹${order.totalAmount}\n`;
@ -87,7 +89,7 @@ const formatCancellationMessage = (orderData: any, cancellationData: Cancellatio
📞 <b>Phone:</b> ${orderData.address?.phone || 'N/A'}
📦 <b>Items:</b>
${orderData.orderItems?.map((item: any) => `${item.product?.name || 'Unknown'} x${item.quantity}`).join('\n') || ' N/A'}
${orderData.orderItems?.map((item: any) => `${item.sku?.product?.name || 'Unknown'} x${item.quantity}`).join('\n') || ' N/A'}
💰 <b>Total:</b> ${orderData.totalAmount}
💳 <b>Refund:</b> ${orderData.refundStatus === 'na' ? 'N/A (COD)' : orderData.refundStatus || 'Pending'}

View file

@ -282,7 +282,7 @@ export {
type OrderWithFullData,
type OrderWithCancellationData,
// Common API helpers
getSuspendedProductIds,
getSuspendedSkuIds,
getNextDeliveryDateWithCapacity,
getStoresSummary,
healthCheck,

View file

@ -76,18 +76,18 @@ export async function initializeProducts(): Promise<void> {
const allDeliverySlots = await getAllDeliverySlotsForCache()
const deliverySlotsMap = new Map<number, DeliverySlotData[]>()
for (const slot of allDeliverySlots) {
if (!deliverySlotsMap.has(slot.productId))
deliverySlotsMap.set(slot.productId, [])
deliverySlotsMap.get(slot.productId)!.push(slot)
if (!deliverySlotsMap.has(slot.skuId))
deliverySlotsMap.set(slot.skuId, [])
deliverySlotsMap.get(slot.skuId)!.push(slot)
}
// Fetch all special deals
const allSpecialDeals = await getAllSpecialDealsForCache()
const specialDealsMap = new Map<number, SpecialDealData[]>()
for (const deal of allSpecialDeals) {
if (!specialDealsMap.has(deal.productId))
specialDealsMap.set(deal.productId, [])
specialDealsMap.get(deal.productId)!.push(deal)
if (!specialDealsMap.has(deal.skuId))
specialDealsMap.set(deal.skuId, [])
specialDealsMap.get(deal.skuId)!.push(deal)
}
// Fetch all product tags
@ -153,70 +153,11 @@ export async function initializeProducts(): Promise<void> {
export async function getProductById(id: number): Promise<Product | null> {
try {
// const key = `product:${id}`
// const data = await redisClient.get(key)
// if (!data) return null
// return JSON.parse(data) as Product
const product = await getProductByIdFromDb(id)
if (!product) return null
const signedImages = scaffoldAssetUrl(
(product.images as string[]) || []
)
// Fetch store info
const allStores = await getAllStoresForCache()
const store = product.storeId
? allStores.find(s => s.id === product.storeId) || null
: null
// Fetch delivery slots for this product
const allDeliverySlots = await getAllDeliverySlotsForCache()
const productSlots = allDeliverySlots.filter(s => s.productId === id)
// Fetch special deals for this product
const allSpecialDeals = await getAllSpecialDealsForCache()
const productDeals = allSpecialDeals.filter(d => d.productId === id)
// Fetch product tags for this product
const allProductTags = await getAllProductTagsForCache()
const productTagNames = allProductTags
.filter(t => t.productId === id)
.map(t => t.tagName)
return {
id: product.id,
name: product.name,
shortDescription: product.shortDescription,
longDescription: product.longDescription,
price: product.price.toString(),
marketPrice: product.marketPrice?.toString() || null,
unitNotation: product.unit.shortNotation,
images: signedImages,
isOutOfStock: product.isOutOfStock,
store: store
? { id: store.id, name: store.name, description: store.description }
: null,
incrementStep: product.incrementStep,
productQuantity: product.productQuantity,
isFlashAvailable: product.isFlashAvailable,
flashPrice: product.flashPrice?.toString() || null,
deliverySlots: productSlots.map((s) => ({
id: s.id,
deliveryTime: s.deliveryTime,
freezeTime: s.freezeTime,
isCapacityFull: s.isCapacityFull,
})),
specialDeals: productDeals.map((d) => ({
quantity: d.quantity.toString(),
price: d.price.toString(),
validTill: d.validTill,
})),
productTags: productTagNames,
}
const allProducts = await getAllProducts()
const product = allProducts.find(p => p.id === id)
return product || null
} catch (error) {
console.error(`Error getting product ${id}:`, error)
console.error('Error getting product by ID:', error)
return null
}
}
@ -250,17 +191,17 @@ export async function getAllProducts(): Promise<Product[]> {
const allDeliverySlots = await getAllDeliverySlotsForCache()
const deliverySlotsMap = new Map<number, DeliverySlotData[]>()
for (const slot of allDeliverySlots) {
if (!deliverySlotsMap.has(slot.productId))
deliverySlotsMap.set(slot.productId, [])
deliverySlotsMap.get(slot.productId)!.push(slot)
if (!deliverySlotsMap.has(slot.skuId))
deliverySlotsMap.set(slot.skuId, [])
deliverySlotsMap.get(slot.skuId)!.push(slot)
}
const allSpecialDeals = await getAllSpecialDealsForCache()
const specialDealsMap = new Map<number, SpecialDealData[]>()
for (const deal of allSpecialDeals) {
if (!specialDealsMap.has(deal.productId))
specialDealsMap.set(deal.productId, [])
specialDealsMap.get(deal.productId)!.push(deal)
if (!specialDealsMap.has(deal.skuId))
specialDealsMap.set(deal.skuId, [])
specialDealsMap.get(deal.skuId)!.push(deal)
}
const allProductTags = await getAllProductTagsForCache()
@ -281,7 +222,7 @@ export async function getAllProducts(): Promise<Product[]> {
: null
const deliverySlots = deliverySlotsMap.get(product.id) || []
const specialDeals = specialDealsMap.get(product.id) || []
const productTags = productTagsMap.get(product.id) || []
const productTags = productTagsMap.get(product.productId) || []
products.push({
id: product.id,
@ -290,7 +231,7 @@ export async function getAllProducts(): Promise<Product[]> {
longDescription: product.longDescription,
price: product.price.toString(),
marketPrice: product.marketPrice?.toString() || null,
unitNotation: product.unitShortNotation,
unitNotation: product.unitNotation,
images: signedImages,
isOutOfStock: product.isOutOfStock,
store: store

View file

@ -1,6 +1,6 @@
import { router, publicProcedure } from '@/src/trpc/trpc-index'
import {
getSuspendedProductIds,
getSuspendedSkuIds,
getNextDeliveryDateWithCapacity,
getStoresSummary,
getAllSkusSummary as getAllSkusSummaryInDb,
@ -30,10 +30,10 @@ export async function scaffoldProducts() {
.where(eq(productInfo.isSuspended, true));
*/
const suspendedProductIds = new Set(await getSuspendedProductIds());
const suspendedSkuIds = new Set(await getSuspendedSkuIds());
// Filter out suspended products
products = products.filter(product => !suspendedProductIds.has(product.id));
products = products.filter(product => !suspendedSkuIds.has(product.id));
// Format products to match the expected response structure
const formattedProducts = await Promise.all(

View file

@ -46,7 +46,7 @@ import type {
const placeOrderUtil = async (params: {
userId: number;
selectedItems: Array<{
productId: number;
skuId: number;
quantity: number;
slotId: number | null;
}>;
@ -86,7 +86,7 @@ const placeOrderUtil = async (params: {
const ordersBySlot = new Map<
number | null,
Array<{
productId: number;
skuId: number;
quantity: number;
slotId: number | null;
product: Awaited<ReturnType<typeof getOrderProductById>>;
@ -94,9 +94,9 @@ const placeOrderUtil = async (params: {
>();
for (const item of selectedItems) {
const product = await getOrderProductById(item.productId);
const product = await getOrderProductById(item.skuId);
if (!product) {
throw new ApiError(`Product ${item.productId} not found`, 400);
throw new ApiError(`Product ${item.skuId} not found`, 400);
}
if (!ordersBySlot.has(item.slotId)) {
@ -107,9 +107,9 @@ const placeOrderUtil = async (params: {
if (params.isFlash) {
for (const item of selectedItems) {
const product = await getOrderProductById(item.productId);
const product = await getOrderProductById(item.skuId);
if (!product?.isFlashAvailable) {
throw new ApiError(`Product ${item.productId} is not available for flash delivery`, 400);
throw new ApiError(`Product ${item.skuId} is not available for flash delivery`, 400);
}
}
}
@ -198,7 +198,7 @@ const placeOrderUtil = async (params: {
return {
orderId: 0,
productId: item.productId,
skuId: item.skuId,
quantity: item.quantity.toString(),
price: priceString,
discountedPrice: priceString,
@ -225,7 +225,7 @@ const placeOrderUtil = async (params: {
await deleteUserCartItemsForOrder(
userId,
selectedItems.map((item) => item.productId)
selectedItems.map((item) => item.skuId)
);
if (appliedCoupon && createdOrders.length > 0) {
@ -252,7 +252,7 @@ export const orderRouter = router({
z.object({
selectedItems: z.array(
z.object({
productId: z.number().int().positive(),
skuId: z.number().int().positive(),
quantity: z.number().int().positive(),
slotId: z.union([z.number().int(), z.null()]),
})
@ -373,13 +373,14 @@ export const orderRouter = router({
const items = await Promise.all(
order.orderItems.map(async (item) => {
const signedImages = item.product.images
const signedImages = item.sku?.images
? scaffoldAssetUrl(
item.product.images as string[]
item.sku.images as string[]
)
: [];
return {
productName: item.product.name,
productName: item.sku?.product?.name || 'Unknown',
skuName: item.sku?.name || null,
quantity: parseFloat(item.quantity),
price: parseFloat(item.price.toString()),
discountedPrice: parseFloat(
@ -512,13 +513,14 @@ export const orderRouter = router({
const items = await Promise.all(
order.orderItems.map(async (item) => {
const signedImages = item.product.images
const signedImages = item.sku?.images
? scaffoldAssetUrl(
item.product.images as string[]
item.sku.images as string[]
)
: [];
return {
productName: item.product.name,
productName: item.sku?.product?.name || 'Unknown',
skuName: item.sku?.name || null,
quantity: parseFloat(item.quantity),
price: parseFloat(item.price.toString()),
discountedPrice: parseFloat(

View file

@ -26,7 +26,7 @@ const FlashAddToCartDialog = () => {
React.useEffect(() => {
if (isOpen && product) {
addToCart.mutate(
{ productId: product.id, quantity: 1, slotId: 0 },
{ skuId: product.id, quantity: 1, slotId: 0 },
{
onSuccess: () => {
clearAddedFlashProduct();

View file

@ -12,7 +12,7 @@ interface Banner {
name: string;
imageUrl: string;
description?: string | null;
productIds?: number[] | null;
skuIds?: number[] | null;
redirectUrl?: string | null;
serialNum?: number | null;
isActive: boolean;
@ -61,13 +61,13 @@ export default function BannerCarousel() {
if (error || !banners || banners.length === 0) return null;
const handleBannerPress = (banner: Banner) => {
if (banner.productIds && banner.productIds.length > 0) {
if (banner.skuIds && banner.skuIds.length > 0) {
// Navigate to the first product's detail page
router.push(`/(drawer)/(tabs)/home/product-detail/${banner.productIds[0]}`);
router.push(`/(drawer)/(tabs)/home/product-detail/${banner.skuIds[0]}`);
} else if (banner.redirectUrl) {
// Handle external URL - could open in browser or handle deep links
}
// If no productIds or redirectUrl, banner is just for display
// If no skuIds or redirectUrl, banner is just for display
};
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {

View file

@ -128,10 +128,10 @@ const PaymentAndOrderComponent: React.FC<PaymentAndOrderProps> = ({
const availableItems = cartItems
.filter(item => {
if (productSlotsMap[item.productId]?.isOutOfStock) return false;
if (productSlotsMap[item.skuId]?.isOutOfStock) return false;
// For flash delivery, check if product supports flash delivery
if (isFlashDelivery) {
return flashEligibleProductIds.has(item.productId);
return flashEligibleProductIds.has(item.skuId);
}
// For regular delivery, only include items with assigned slots
return selectedSlots[item.id];
@ -150,7 +150,7 @@ const PaymentAndOrderComponent: React.FC<PaymentAndOrderProps> = ({
selectedItems: availableItems.map(itemId => {
const item = cartItems.find(cartItem => cartItem.id === itemId);
return {
productId: item.productId,
skuId: item.skuId,
quantity: item.quantity,
slotId: isFlashDelivery ? null : selectedSlots[itemId]
};

View file

@ -78,7 +78,7 @@ const ProductCard: React.FC<ProductCardProps> = ({
}) || {};
// Find current quantity from cart data
const cartItem = cartData?.items?.find((cartItem: any) => cartItem.productId === item.id);
const cartItem = cartData?.items?.find((cartItem: any) => cartItem.skuId === item.id);
const quantity = cartItem?.quantity || 0;
// Get slots data from central store

View file

@ -83,7 +83,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
}, [slotsData, productDetail])
// Find current quantity from cart data
const cartItem = productDetail ? cartData?.data?.items?.find((item: any) => item.productId === productDetail.id) : null;
const cartItem = productDetail ? cartData?.data?.items?.find((item: any) => item.skuId === productDetail.id) : null;
const quantity = cartItem?.quantity || 0;
const handleQuantityChange = (newQuantity: number) => {
@ -143,7 +143,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
};
const handleSlotAddToCart = (productId: number, selectedSlotId: number) => {
const cartItem = cartData.data?.items?.find((item: any) => item.productId === productId);
const cartItem = cartData.data?.items?.find((item: any) => item.skuId === productId);
setIsLoadingDialogOpen(true);
if (cartItem) {
removeFromCart.mutate(

View file

@ -252,7 +252,7 @@ const CompactProductCard = ({
refetchCart: true,
}, cartType);
const cartItem = cartData?.items?.find((cartItem: any) => cartItem.productId === item.id);
const cartItem = cartData?.items?.find((cartItem: any) => cartItem.skuId === item.id);
const quantity = cartItem?.quantity || 0;
const isOutOfStock = productSlotsMap[item.id]?.isOutOfStock;

View file

@ -49,7 +49,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
} = useGetCart({ refetchOnWindowFocus: true }, cartType);
// Extract product IDs from cart items
const productIds = cartData?.items.map(item => item.productId) || [];
const productIds = cartData?.items.map(item => item.skuId) || [];
// Get cart slots for the products in cart
const { data: slotsData, refetch: refetchSlots, isLoading: isSlotsLoading } = trpc.user.cart.getCartSlots.useQuery(
@ -106,9 +106,9 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
const baseTotalPrice = useMemo(
() =>
cartItems
.filter((item) => !productSlotsMap[item.productId]?.isOutOfStock)
.filter((item) => !productSlotsMap[item.skuId]?.isOutOfStock)
.reduce((sum, item) => {
const product = productsById[item.productId];
const product = productsById[item.skuId];
const price = product?.price || 0;
return sum + price * (quantities[item.id] || item.quantity);
}, 0),
@ -208,9 +208,9 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
);
const totalPrice = cartItems
.filter((item) => !productSlotsMap[item.productId]?.isOutOfStock)
.filter((item) => !productSlotsMap[item.skuId]?.isOutOfStock)
.reduce((sum, item) => {
const product = productsById[item.productId];
const product = productsById[item.skuId];
const quantity = quantities[item.id] || item.quantity;
const price = isFlashDelivery ? (product?.flashPrice ?? product?.price ?? 0) : (product?.price || 0);
return sum + Number(price) * quantity;
@ -282,7 +282,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
const finalTotalWithDelivery = finalTotal + deliveryCharge;
const hasAvailableItems = cartItems.some(item => !productSlotsMap[item.productId]?.isOutOfStock);
const hasAvailableItems = cartItems.some(item => !productSlotsMap[item.skuId]?.isOutOfStock);
useEffect(() => {
const initial: Record<number, number> = {};
@ -308,7 +308,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
if (isFlashDelivery) {
newSelectedSlots[item.id] = 0;
} else {
const productSlots = slotsData?.[item.productId];
const productSlots = slotsData?.[item.skuId];
if (!productSlots || productSlots.length === 0) return;
const now = dayjs();
@ -416,11 +416,11 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
<>
<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 productSlots = getAvailableSlotsForProduct(item.skuId);
const selectedSlotForItem = selectedSlots[item.id];
const isFlashEligible = isFlashDelivery ? flashEligibleProductIds.has(item.productId) : true;
const product = productsById[item.productId];
const productSlotInfo = productSlotsMap[item.productId];
const isFlashEligible = isFlashDelivery ? flashEligibleProductIds.has(item.skuId) : true;
const product = productsById[item.skuId];
const productSlotInfo = productSlotsMap[item.skuId];
// const isAvailable = (productSlots.length > 0 || isFlashDelivery) && !item.product?.isOutOfStock && isFlashEligible;
let isAvailable = true;
@ -687,7 +687,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
<MyText style={tw`text-xs font-bold text-red-600`}>
{productSlotInfo?.isOutOfStock
? "Out of Stock"
: isFlashDelivery && !flashEligibleProductIds.has(item.productId)
: isFlashDelivery && !flashEligibleProductIds.has(item.skuId)
? "Not available for flash delivery. Please remove"
: "No delivery slots available"}
</MyText>
@ -919,10 +919,10 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
onPress={() => {
const availableItems = cartItems
.filter(item => {
if (productSlotsMap[item.productId]?.isOutOfStock) return false;
if (productSlotsMap[item.skuId]?.isOutOfStock) return false;
if (isFlashDelivery) {
// Check if product supports flash delivery
return flashEligibleProductIds.has(item.productId);
return flashEligibleProductIds.has(item.skuId);
}
return selectedSlots[item.id]; // Regular delivery requires slot selection
})
@ -930,8 +930,8 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
if (availableItems.length === 0) {
// Determine why no items are available
const outOfStockItems = cartItems.filter(item => productSlotsMap[item.productId]?.isOutOfStock);
const inStockItems = cartItems.filter(item => !productSlotsMap[item.productId]?.isOutOfStock);
const outOfStockItems = cartItems.filter(item => productSlotsMap[item.skuId]?.isOutOfStock);
const inStockItems = cartItems.filter(item => !productSlotsMap[item.skuId]?.isOutOfStock);
let errorTitle = "Cannot Proceed";
let errorMessage = "";
@ -943,7 +943,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
} else if (isFlashDelivery) {
// Check if any items are flash-eligible
const flashEligibleItems = inStockItems.filter(item =>
flashEligibleProductIds.has(item.productId)
flashEligibleProductIds.has(item.skuId)
);
if (flashEligibleItems.length === 0) {
errorTitle = "1 Hr Delivery Unavailable";
@ -970,7 +970,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
// Check if there are items without slots (for regular delivery)
if (!isFlashDelivery && availableItems.length < cartItems.length) {
const itemsWithoutSlots = cartItems.filter(item => !selectedSlots[item.id] && !productSlotsMap[item.productId]?.isOutOfStock);
const itemsWithoutSlots = cartItems.filter(item => !selectedSlots[item.id] && !productSlotsMap[item.skuId]?.isOutOfStock);
if (itemsWithoutSlots.length > 0) {
Alert.alert(
"Delivery Slot Required",

View file

@ -90,7 +90,7 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
const selectedItems = cartItems.filter(item => {
// For flash delivery, check if product supports flash delivery
if (isFlashDelivery) {
return flashEligibleProductIds.has(item.productId);
return flashEligibleProductIds.has(item.skuId);
}
// For regular delivery, only include items with assigned slots
return selectedSlots[item.id];
@ -132,10 +132,10 @@ const CheckoutPage: React.FC<CheckoutPageProps> = ({ isFlashDelivery = false })
const totalPrice = selectedItems
.filter((item) => !productSlotsMap[item.productId]?.isOutOfStock)
.filter((item) => !productSlotsMap[item.skuId]?.isOutOfStock)
.reduce(
(sum, item) => {
const product = productsById[item.productId];
const product = productsById[item.skuId];
const price = isFlashDelivery ? (product?.flashPrice ?? product?.price ?? 0) : (product?.price || 0);
return sum + price * item.quantity;
},

View file

@ -122,17 +122,17 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
const itemsToUpdate = cartItems.filter(item => {
if (isFlashDelivery || !item.slotId) return false;
const availableSlots = productSlotsMap[item.productId]?.slots || [];
const availableSlots = productSlotsMap[item.skuId]?.slots || [];
const isSlotAvailable = availableSlots.some((slot) => slot.id === item.slotId);
return !isSlotAvailable;
});
itemsToUpdate.forEach((item) => {
const availableSlots = productSlotsMap[item.productId]?.slots || [];
const availableSlots = productSlotsMap[item.skuId]?.slots || [];
if (availableSlots.length > 0 && !isFlashDelivery) {
const nearestSlotId = availableSlots[0].id;
removeFromCart.mutate({ itemId: item.id });
addToCartHook.addToCart(item.productId, item.quantity, nearestSlotId);
addToCartHook.addToCart(item.skuId, item.quantity, nearestSlotId);
}
});
}, []);
@ -143,7 +143,7 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
// Calculate total cart value and free delivery info
const totalCartValue = cartItems.reduce(
(sum, item) => {
const product = productsById[item.productId];
const product = productsById[item.skuId];
const basePrice = product?.price ?? 0;
const price = isFlashDelivery ? (product?.flashPrice ?? basePrice) : basePrice;
return sum + price * item.quantity;
@ -268,20 +268,20 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
<View style={tw`flex-row items-center`}>
<View style={tw`items-center`}>
<Image
source={{ uri: productsById[item.productId]?.images?.[0] }}
source={{ uri: productsById[item.skuId]?.images?.[0] }}
style={tw`w-8 h-8 rounded-lg bg-slate-50 border border-slate-100`}
/>
<MyText style={tw`text-gray-500 text-[9px] font-medium mt-1`}>
<MyText style={tw`text-[#f81260] font-semibold`}>{formatQuantity(productsById[item.productId]?.productQuantity || 1, productsById[item.productId]?.unitNotation || '').display}</MyText>
<MyText style={tw`text-[#f81260] font-semibold`}>{formatQuantity(productsById[item.skuId]?.productQuantity || 1, productsById[item.skuId]?.unitNotation || '').display}</MyText>
</MyText>
</View>
<View style={tw`flex-1 ml-4`}>
<View style={tw`flex-row items-center justify-between mb-1`}>
<ProductNameWithQuantity
name={productsById[item.productId]?.name || ''}
productQuantity={productsById[item.productId]?.productQuantity || 0}
unitNotation={productsById[item.productId]?.unitNotation || ''}
name={productsById[item.skuId]?.name || ''}
productQuantity={productsById[item.skuId]?.productQuantity || 0}
unitNotation={productsById[item.skuId]?.unitNotation || ''}
/>
<MiniQuantifier
value={quantities[item.id] || item.quantity}
@ -293,17 +293,17 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
updateCartItem.mutate({ itemId: item.id, quantity: value });
}
}}
step={productsById[item.productId]?.incrementStep || 1}
step={productsById[item.skuId]?.incrementStep || 1}
showUnits={true}
// unit={productsById[item.productId]?.unitNotation}
// unit={productsById[item.skuId]?.unitNotation}
/>
</View>
<View style={tw`flex-row items-center justify-between`}>
{item.slotId && slotsData && productSlotsMap[item.productId] && (
{item.slotId && slotsData && productSlotsMap[item.skuId] && (
<BottomDropdown
label="Select Delivery Slot"
value={item.slotId}
options={(productSlotsMap[item.productId]?.slots || []).map((slot) => {
options={(productSlotsMap[item.skuId]?.slots || []).map((slot) => {
return {
label: slot ? formatTimeRange(slot.deliveryTime) : "N/A",
value: slot.id,
@ -313,7 +313,7 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
const newSlot = slotsData.slots.find(s => s.id === val);
if (!newSlot) return;
const productId = item.productId;
const productId = item.skuId;
const quantity = item.quantity;
const itemId = item.id;
const slotId = typeof val === 'number' ? val : Number(val);
@ -340,7 +340,7 @@ const FloatingCartBar: React.FC<FloatingCartBarProps> = ({
)}
<MyText style={tw`text-slate-900 text-sm font-bold`}>
{(() => {
const product = productsById[item.productId];
const product = productsById[item.skuId];
const basePrice = product?.price ?? 0;
const price = isFlashDelivery ? (product?.flashPrice ?? basePrice) : basePrice;
return price * item.quantity;

View file

@ -13,7 +13,7 @@ const getCartStorageKey = (cartType: CartType = "regular"): string => {
interface LocalCartItem {
id: number;
productId: number;
skuId: number;
quantity: number;
slotId: number;
addedAt: string;
@ -35,7 +35,7 @@ interface ProductSummary {
export interface CartItem {
id: number;
productId: number;
skuId: number;
quantity: number;
addedAt: string;
subtotal: number;
@ -66,7 +66,7 @@ interface UseGetCartReturn {
}
interface AddToCartVariables {
productId: number;
skuId: number;
quantity: number;
slotId: number;
}
@ -94,8 +94,8 @@ interface UseAddToCartReturn {
isLoading: boolean;
error: Error | null;
data: LocalCartItem[] | undefined;
addToCart: (productId: number, quantity?: number, slotId?: number, onSettled?: (data: LocalCartItem[] | undefined, error: Error | null) => void) => void;
addToCartAsync: (productId: number, quantity?: number, slotId?: number) => Promise<LocalCartItem[]>;
addToCart: (skuId: number, quantity?: number, slotId?: number, onSettled?: (data: LocalCartItem[] | undefined, error: Error | null) => void) => void;
addToCartAsync: (skuId: number, quantity?: number, slotId?: number) => Promise<LocalCartItem[]>;
}
interface UseUpdateCartItemReturn {
@ -135,9 +135,9 @@ const getNextCartItemId = (items: LocalCartItem[]): number => {
return maxId + 1;
};
const addToLocalCart = async (productId: number, quantity: number, slotId: number | undefined, cartType: CartType = "regular"): Promise<LocalCartItem[]> => {
const addToLocalCart = async (skuId: number, quantity: number, slotId: number | undefined, cartType: CartType = "regular"): Promise<LocalCartItem[]> => {
const items = await getLocalCart(cartType);
const existingIndex = items.findIndex(item => item.productId === productId);
const existingIndex = items.findIndex(item => item.skuId === skuId);
if (existingIndex >= 0) {
items[existingIndex].quantity += quantity;
@ -148,7 +148,7 @@ const addToLocalCart = async (productId: number, quantity: number, slotId: numbe
const newId = getNextCartItemId(items);
const cartItem: LocalCartItem = {
id: newId,
productId,
skuId,
quantity,
slotId: slotId ?? 0,
addedAt: new Date().toISOString(),
@ -211,14 +211,14 @@ export function useGetCart(options: UseGetCartOptions = {}, cartType: CartType =
const items: CartItem[] = cartItems
.map((cartItem): CartItem | null => {
const productBasic = productMap[cartItem.productId];
const productAvailability = productSlotsMap[cartItem.productId];
const productBasic = productMap[cartItem.skuId];
const productAvailability = productSlotsMap[cartItem.skuId];
if (!productBasic || !productAvailability) return null;
return {
id: cartItem.id,
productId: cartItem.productId,
skuId: cartItem.skuId,
quantity: cartItem.quantity,
addedAt: cartItem.addedAt,
subtotal: Number(productBasic.price) * cartItem.quantity,
@ -256,8 +256,8 @@ export function useAddToCart(options: MutationOptions<LocalCartItem[], AddToCart
const queryClient = useQueryClient();
const mutation: UseMutationResult<LocalCartItem[], Error, AddToCartVariables> = useMutation({
mutationFn: async ({ productId, quantity, slotId }: AddToCartVariables): Promise<LocalCartItem[]> => {
return await addToLocalCart(productId, quantity, slotId, cartType);
mutationFn: async ({ skuId, quantity, slotId }: AddToCartVariables): Promise<LocalCartItem[]> => {
return await addToLocalCart(skuId, quantity, slotId, cartType);
},
onSuccess: (data: LocalCartItem[], variables: AddToCartVariables) => {
queryClient.invalidateQueries({ queryKey: [`local-cart-${cartType}`] });
@ -274,11 +274,11 @@ export function useAddToCart(options: MutationOptions<LocalCartItem[], AddToCart
},
});
const addToCart = (productId: number, quantity = 1, slotId?: number, onSettled?: (data: LocalCartItem[] | undefined, error: Error | null) => void): void => {
const addToCart = (skuId: number, quantity = 1, slotId?: number, onSettled?: (data: LocalCartItem[] | undefined, error: Error | null) => void): void => {
if (slotId == null) {
throw new Error('slotId is required for adding to cart');
}
mutation.mutate({ productId, quantity, slotId }, {
mutation.mutate({ skuId, quantity, slotId }, {
onSettled: (data: LocalCartItem[] | undefined, error: Error | null) => {
onSettled?.(data, error);
}
@ -292,11 +292,11 @@ export function useAddToCart(options: MutationOptions<LocalCartItem[], AddToCart
error: mutation.error,
data: mutation.data,
addToCart,
addToCartAsync: (productId: number, quantity = 1, slotId?: number): Promise<LocalCartItem[]> => {
addToCartAsync: (skuId: number, quantity = 1, slotId?: number): Promise<LocalCartItem[]> => {
if (slotId == null) {
throw new Error('slotId is required for adding to cart');
}
return mutation.mutateAsync({ productId, quantity, slotId });
return mutation.mutateAsync({ skuId, quantity, slotId });
},
};
}

View file

@ -132,7 +132,7 @@ export default function AddToCartDialog() {
} else {
const slotId = selectedSlotId ?? availableSlotIds[0] ?? 0;
addToCart.mutate(
{ productId: product.id, quantity, slotId },
{ skuId: product.id, quantity, slotId },
{ onSuccess: () => clearAddedToCartProduct() }
);
}

View file

@ -222,7 +222,18 @@ ALTER TABLE `coupons` RENAME COLUMN `product_ids` TO `sku_ids`;
ALTER TABLE `reserved_coupons` RENAME COLUMN `product_ids` TO `sku_ids`;
ALTER TABLE `vendor_snippets` RENAME COLUMN `product_ids` TO `sku_ids`;
-- 8. Clean up helper table.
-- 8. Update popularItems in key_val_store from product IDs to SKU IDs.
UPDATE `key_val_store`
SET `value` = (
SELECT json_group_array(`m`.`sku_id`)
FROM json_each(`key_val_store`.`value`) AS `je`
JOIN `__product_to_sku` `m` ON `m`.`product_id` = `je`.`value`
)
WHERE `key` = 'popularItems'
AND `value` IS NOT NULL
AND `value` LIKE '[%';
-- 9. Clean up helper table.
DROP TABLE `__product_to_sku`;
-- PRAGMA foreign_keys=ON;

View file

@ -311,7 +311,7 @@ export {
cancelOrderTransaction as cancelUserOrderTransaction,
updateOrderNotes as updateUserOrderNotes,
getRecentlyDeliveredOrderIds as getUserRecentlyDeliveredOrderIds,
getProductIdsFromOrders as getUserProductIdsFromOrders,
getSkuIdsFromOrders as getUserProductIdsFromOrders,
getProductsForRecentOrders as getUserProductsForRecentOrders,
// Post-order handler helpers
getOrdersByIdsWithFullData,
@ -364,7 +364,7 @@ export {
// Common API Helpers
export {
getSuspendedProductIds,
getSuspendedSkuIds,
getNextDeliveryDateWithCapacity,
} from './src/user-apis/product'

View file

@ -792,12 +792,12 @@ export async function updateProductPrices(updates: Array<{
}
const productIds = updates.map((update) => update.productId)
const existingProducts = await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
const existingSkus = await db.query.productSkus.findMany({
where: inArray(productSkus.id, productIds),
columns: { id: true },
}) as Array<{ id: number }>
const existingIds = new Set(existingProducts.map((product: { id: number }) => product.id))
const existingIds = new Set(existingSkus.map((sku: { id: number }) => sku.id))
const invalidIds = productIds.filter((id) => !existingIds.has(id))
if (invalidIds.length > 0) {
@ -806,7 +806,7 @@ export async function updateProductPrices(updates: Array<{
const updatePromises = updates.map((update) => {
const { productId, price, marketPrice, flashPrice, isFlashAvailable } = update
const updateData: Partial<Pick<ProductInfoInsert, 'price' | 'marketPrice' | 'flashPrice' | 'isFlashAvailable'>> = {}
const updateData: any = {}
if (price !== undefined) updateData.price = price.toString()
if (marketPrice !== undefined) updateData.marketPrice = marketPrice === null ? null : marketPrice.toString()
@ -814,9 +814,9 @@ export async function updateProductPrices(updates: Array<{
if (isFlashAvailable !== undefined) updateData.isFlashAvailable = isFlashAvailable
return db
.update(productInfo)
.update(productSkus)
.set(updateData)
.where(eq(productInfo.id, productId))
.where(eq(productSkus.id, productId))
})
await Promise.all(updatePromises)

View file

@ -5,7 +5,8 @@ import { db } from '../db/db_index'
import {
homeBanners,
productInfo,
units,
productSkus,
skuFeatures,
deliverySlotInfo,
specialDeals,
storeInfo,
@ -24,7 +25,7 @@ export interface BannerData {
name: string
imageUrl: string | null
serialNum: number | null
productIds: number[] | null
skuIds: number[] | null
createdAt: Date
}
@ -41,7 +42,9 @@ export async function getAllBannersForCache(): Promise<BannerData[]> {
export interface ProductBasicData {
id: number
productId: number
name: string
skuName: string | null
shortDescription: string | null
longDescription: string | null
price: string
@ -49,7 +52,7 @@ export interface ProductBasicData {
images: unknown
isOutOfStock: boolean
storeId: number | null
unitShortNotation: string
unitNotation: string
incrementStep: number
productQuantity: number
isFlashAvailable: boolean
@ -63,7 +66,7 @@ export interface StoreBasicData {
}
export interface DeliverySlotData {
productId: number
skuId: number
id: number
deliveryTime: Date
freezeTime: Date
@ -71,44 +74,64 @@ export interface DeliverySlotData {
}
export interface SpecialDealData {
productId: number
skuId: number
quantity: string
price: string
validTill: Date
}
export async function getAllSpecialDealsForCache(): Promise<SpecialDealData[]> {
const results = await db
.select({
skuId: specialDeals.skuId,
quantity: specialDeals.quantity,
price: specialDeals.price,
validTill: specialDeals.validTill,
})
.from(specialDeals)
.where(gt(specialDeals.validTill, sql`CURRENT_TIMESTAMP`))
return results.map((deal) => ({
...deal,
quantity: String(deal.quantity ?? '0'),
price: String(deal.price ?? '0'),
}))
}
export interface ProductTagData {
productId: number
tagName: string
}
export async function getAllProductsForCache(): Promise<ProductBasicData[]> {
const results = await db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
longDescription: productInfo.longDescription,
price: productInfo.price,
marketPrice: productInfo.marketPrice,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
storeId: productInfo.storeId,
unitShortNotation: units.shortNotation,
incrementStep: productInfo.incrementStep,
productQuantity: productInfo.productQuantity,
isFlashAvailable: productInfo.isFlashAvailable,
flashPrice: productInfo.flashPrice,
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id))
const skus = await db.query.productSkus.findMany({
with: {
product: true,
features: true,
},
})
return results.map((product) => ({
...product,
price: String(product.price ?? '0'),
marketPrice: product.marketPrice ? String(product.marketPrice) : null,
flashPrice: product.flashPrice ? String(product.flashPrice) : null,
}))
return skus.map((sku) => {
const features = sku.features || []
return {
id: sku.id,
productId: sku.productId,
name: sku.product?.name ?? 'Unknown',
skuName: sku.name ?? null,
shortDescription: sku.product?.shortDescription ?? null,
longDescription: sku.product?.longDescription ?? null,
price: String(sku.price ?? '0'),
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
images: sku.images,
isOutOfStock: sku.isOutOfStock,
storeId: sku.product?.storeId ?? null,
unitNotation: features.map((f) => f.featureValue).join(' '),
incrementStep: sku.product?.incrementStep ?? 1,
productQuantity: 1,
isFlashAvailable: sku.isFlashAvailable,
flashPrice: sku.flashPrice ? String(sku.flashPrice) : null,
}
})
}
export async function getAllStoresForCache(): Promise<StoreBasicData[]> {
@ -126,13 +149,12 @@ export async function getAllDeliverySlotsForCache(): Promise<DeliverySlotData[]>
),
})
// Flatten slots with their product IDs
const result: DeliverySlotData[] = []
for (const slot of slots) {
const productIds = slot.productIds || []
for (const productId of productIds) {
const skuIds = slot.skuIds || []
for (const skuId of skuIds) {
result.push({
productId,
skuId,
id: slot.id,
deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime,
@ -144,24 +166,6 @@ export async function getAllDeliverySlotsForCache(): Promise<DeliverySlotData[]>
return result
}
export async function getAllSpecialDealsForCache(): Promise<SpecialDealData[]> {
const results = await db
.select({
productId: specialDeals.productId,
quantity: specialDeals.quantity,
price: specialDeals.price,
validTill: specialDeals.validTill,
})
.from(specialDeals)
.where(gt(specialDeals.validTill, sql`CURRENT_TIMESTAMP`))
return results.map((deal) => ({
...deal,
quantity: String(deal.quantity ?? '0'),
price: String(deal.price ?? '0'),
}))
}
export async function getAllProductTagsForCache(): Promise<ProductTagData[]> {
return db
.select({
@ -224,23 +228,26 @@ export interface SlotWithProductsData {
isCapacityFull: boolean
products: Array<{
id: number
productId: number
name: string
skuName: string | null
productQuantity: number
shortDescription: string | null
price: string
marketPrice: string | null
unit: { shortNotation: string } | null
unitNotation: string
store: { id: number; name: string; description: string | null } | null
images: unknown
isOutOfStock: boolean
storeId: number | null
isFlashAvailable: boolean
flashPrice: string | null
}>
}
export async function getAllSlotsWithProductsForCache(): Promise<SlotWithProductsData[]> {
const now = new Date()
// Get all active future slots
const slots = await db.query.deliverySlotInfo.findMany({
where: and(
eq(deliverySlotInfo.isActive, true),
@ -249,68 +256,64 @@ export async function getAllSlotsWithProductsForCache(): Promise<SlotWithProduct
orderBy: asc(deliverySlotInfo.deliveryTime),
})
// Get all unique product IDs from all slots
const allProductIds = new Set<number>()
const allSkuIds = new Set<number>()
for (const slot of slots) {
for (const productId of (slot.productIds || [])) {
allProductIds.add(productId)
for (const skuId of (slot.skuIds || [])) {
allSkuIds.add(skuId)
}
}
// Fetch all products in one query
const productIdsArray = Array.from(allProductIds)
const productIdSet = new Set(productIdsArray);
// const productsData = productIdsArray.length > 0
// ? await db.query.productInfo.findMany({
// where: inArray(productInfo.id, productIdsArray),
// with: {
// unit: true,
// store: true,
// },
// })
// : []
let productsData = productIdsArray.length > 0
? await db.query.productInfo.findMany({
// where: inArray(productInfo.id, productIdsArray),
with: {
unit: true,
store: true,
const skuIdsArray = Array.from(allSkuIds)
const skuIdSet = new Set(skuIdsArray)
let skusData: any[] = []
if (skuIdsArray.length > 0) {
skusData = await db.query.productSkus.findMany({
with: {
product: {
with: { store: true },
},
})
: []
features: true,
},
})
skusData = skusData.filter((item: any) => skuIdSet.has(item.id))
}
productsData = productsData.filter(item => productIdSet.has(item.id))
const skuMap = new Map(skusData.map((s: any) => [s.id, s]))
// Create a map for quick lookup
const productMap = new Map(productsData.map(p => [p.id, p]))
// Build the result
return slots.map(slot => ({
return slots.map((slot) => ({
id: slot.id,
deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime,
isActive: slot.isActive,
isCapacityFull: slot.isCapacityFull,
products: (slot.productIds || [])
.map(productId => productMap.get(productId))
products: (slot.skuIds || [])
.map((skuId: number) => skuMap.get(skuId))
.filter((p): p is NonNullable<typeof p> => p != null)
.map(product => ({
id: product.id,
name: product.name,
productQuantity: product.productQuantity,
shortDescription: product.shortDescription,
price: String(product.price ?? '0'),
marketPrice: product.marketPrice ? String(product.marketPrice) : null,
unit: product.unit ? { shortNotation: product.unit.shortNotation } : null,
store: product.store ? {
id: product.store.id,
name: product.store.name,
description: product.store.description
} : null,
images: product.images,
isOutOfStock: product.isOutOfStock,
storeId: product.storeId,
})),
.map((sku: any) => {
const features = sku.features || []
return {
id: sku.id,
productId: sku.productId,
name: sku.product?.name ?? 'Unknown',
skuName: sku.name ?? null,
productQuantity: 1,
shortDescription: sku.product?.shortDescription ?? null,
price: String(sku.price ?? '0'),
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
unitNotation: features.map((f: any) => f.featureValue).join(' '),
store: sku.product?.store ? {
id: sku.product.store.id,
name: sku.product.store.name,
description: sku.product.store.description
} : null,
images: sku.images,
isOutOfStock: sku.isOutOfStock,
storeId: sku.product?.storeId ?? null,
isFlashAvailable: sku.isFlashAvailable,
flashPrice: sku.flashPrice ? String(sku.flashPrice) : null,
}
}),
})) as SlotWithProductsData[]
}

View file

@ -5,6 +5,7 @@ import {
orderStatus,
addresses,
productInfo,
productSkus,
paymentInfoTable,
coupons,
couponUsage,
@ -258,9 +259,9 @@ export async function getAddressByIdAndUser(
})
}
export async function getProductById(productId: number) {
return db.query.productInfo.findFirst({
where: eq(productInfo.id, productId),
export async function getProductById(skuId: number) {
return db.query.productSkus.findFirst({
where: eq(productSkus.id, skuId),
})
}
@ -341,12 +342,12 @@ export async function placeOrderTransaction(params: {
export async function deleteCartItemsForOrder(
userId: number,
productIds: number[]
skuIds: number[]
): Promise<void> {
await db.delete(cartItems).where(
and(
eq(cartItems.userId, userId),
inArray(cartItems.productId, productIds)
inArray(cartItems.skuId, skuIds)
)
)
}
@ -375,10 +376,16 @@ export async function getOrdersWithRelations(
with: {
orderItems: {
with: {
product: {
sku: {
with: {
product: {
columns: {
name: true,
},
},
},
columns: {
id: true,
name: true,
images: true,
},
},
@ -451,10 +458,16 @@ export async function getOrderByIdWithRelations(
with: {
orderItems: {
with: {
product: {
sku: {
with: {
product: {
columns: {
name: true,
},
},
},
columns: {
id: true,
name: true,
images: true,
},
},
@ -610,15 +623,15 @@ export async function getRecentlyDeliveredOrderIds(
return recentOrders.map((order) => order.id)
}
export async function getProductIdsFromOrders(
export async function getSkuIdsFromOrders(
orderIds: number[]
): Promise<number[]> {
const orderItemsResult = await db
.select({ productId: orderItems.productId })
.select({ skuId: orderItems.skuId })
.from(orderItems)
.where(inArray(orderItems.orderId, orderIds))
return [...new Set(orderItemsResult.map((item) => item.productId))]
return [...new Set(orderItemsResult.map((item) => item.skuId))]
}
export interface RecentProductData {
@ -714,14 +727,17 @@ export async function getOrdersByIdsWithFullData(
},
orderItems: {
with: {
product: {
sku: {
columns: {
name: true,
productQuantity: true,
price: true,
},
with: {
unit: true
}
product: {
columns: { name: true },
},
features: true,
},
},
},
},

View file

@ -1,5 +1,5 @@
import { db } from '../db/db_index'
import { deliverySlotInfo, productInfo, productReviews, productTags, specialDeals, storeInfo, units, users } from '../db/schema'
import { deliverySlotInfo, productInfo, productSkus, productReviews, productTags, specialDeals, storeInfo, units, users } from '../db/schema'
import { and, desc, eq, gt, sql } from 'drizzle-orm'
import type { UserProductDetailData, UserProductReview } from '@packages/shared'
@ -8,42 +8,27 @@ const getStringArray = (value: unknown): string[] | null => {
return value.map((item) => String(item))
}
export async function getProductDetailById(productId: number): Promise<UserProductDetailData | null> {
const productData = await db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
longDescription: productInfo.longDescription,
price: productInfo.price,
marketPrice: productInfo.marketPrice,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
storeId: productInfo.storeId,
unitShortNotation: units.shortNotation,
incrementStep: productInfo.incrementStep,
productQuantity: productInfo.productQuantity,
isFlashAvailable: productInfo.isFlashAvailable,
flashPrice: productInfo.flashPrice,
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(eq(productInfo.id, productId))
.limit(1)
export async function getProductDetailById(skuId: number): Promise<UserProductDetailData | null> {
const sku = await db.query.productSkus.findFirst({
where: eq(productSkus.id, skuId),
with: {
product: true,
features: true,
},
})
if (productData.length === 0) {
if (!sku) {
return null
}
const product = productData[0]
const features = sku.features || []
const product = sku.product
const storeData = product.storeId ? await db.query.storeInfo.findFirst({
const storeData = product?.storeId ? await db.query.storeInfo.findFirst({
where: eq(storeInfo.id, product.storeId),
columns: { id: true, name: true, description: true },
}) : null
// Note: deliverySlots are now fetched from cache in the frontend via useSlots()
// This avoids expensive database joins on every product detail view
const specialDealsData = await db
.select({
quantity: specialDeals.quantity,
@ -53,32 +38,32 @@ export async function getProductDetailById(productId: number): Promise<UserProdu
.from(specialDeals)
.where(
and(
eq(specialDeals.productId, productId),
eq(specialDeals.skuId, skuId),
gt(specialDeals.validTill, sql`CURRENT_TIMESTAMP`)
)
)
.orderBy(specialDeals.quantity)
return {
id: product.id,
name: product.name,
shortDescription: product.shortDescription ?? null,
longDescription: product.longDescription ?? null,
price: String(product.price ?? '0'),
marketPrice: product.marketPrice ? String(product.marketPrice) : null,
unitNotation: product.unitShortNotation,
images: getStringArray(product.images),
isOutOfStock: product.isOutOfStock,
id: sku.id,
name: product?.name ?? 'Unknown',
shortDescription: product?.shortDescription ?? null,
longDescription: product?.longDescription ?? null,
price: String(sku.price ?? '0'),
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
unitNotation: features.map((f) => f.featureValue).join(' '),
images: getStringArray(sku.images),
isOutOfStock: sku.isOutOfStock,
store: storeData ? {
id: storeData.id,
name: storeData.name,
description: storeData.description ?? null,
} : null,
incrementStep: product.incrementStep,
productQuantity: product.productQuantity,
isFlashAvailable: product.isFlashAvailable,
flashPrice: product.flashPrice?.toString() || null,
deliverySlots: [], // Fetched from cache in frontend via useSlots()
incrementStep: product?.incrementStep ?? 1,
productQuantity: 1,
isFlashAvailable: sku.isFlashAvailable,
flashPrice: sku.flashPrice?.toString() || null,
deliverySlots: [],
specialDeals: specialDealsData.map((deal) => ({
quantity: String(deal.quantity ?? '0'),
price: String(deal.price ?? '0'),
@ -126,9 +111,9 @@ export async function getProductReviews(productId: number, limit: number, offset
}
}
export async function getProductById(productId: number) {
return db.query.productInfo.findFirst({
where: eq(productInfo.id, productId),
export async function getProductById(skuId: number) {
return db.query.productSkus.findFirst({
where: eq(productSkus.id, skuId),
})
}
@ -221,20 +206,20 @@ export async function getAllProductsWithUnits(tagId?: number): Promise<ProductSu
/**
* Get all suspended product IDs
*/
export async function getSuspendedProductIds(): Promise<number[]> {
const suspendedProducts = await db
.select({ id: productInfo.id })
.from(productInfo)
.where(eq(productInfo.isSuspended, true))
export async function getSuspendedSkuIds(): Promise<number[]> {
const suspendedSkus = await db
.select({ id: productSkus.id })
.from(productSkus)
.where(eq(productSkus.isSuspended, true))
return suspendedProducts.map(sp => sp.id)
return suspendedSkus.map(sp => sp.id)
}
/**
* Get next delivery date for a product (with capacity check)
* This version filters by both isActive AND isCapacityFull
*/
export async function getNextDeliveryDateWithCapacity(productId: number): Promise<Date | null> {
export async function getNextDeliveryDateWithCapacity(skuId: number): Promise<Date | null> {
const slots = await db.query.deliverySlotInfo.findMany({
where: and(
eq(deliverySlotInfo.isActive, true),
@ -244,10 +229,9 @@ export async function getNextDeliveryDateWithCapacity(productId: number): Promis
orderBy: desc(deliverySlotInfo.deliveryTime),
})
// Find the first slot that contains this product
for (const slot of slots) {
const productIds = slot.productIds || []
if (productIds.includes(productId)) {
const skuIds = slot.skuIds || []
if (skuIds.includes(skuId)) {
return slot.deliveryTime
}
}

View file

@ -1,5 +1,5 @@
import { db } from '../db/db_index'
import { deliverySlotInfo, productInfo } from '../db/schema'
import { deliverySlotInfo, productSkus } from '../db/schema'
import { asc, eq } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm'
import type { UserDeliverySlot, UserSlotAvailability } from '@packages/shared'
@ -27,20 +27,17 @@ export async function getActiveSlotsList(): Promise<UserDeliverySlot[]> {
}
export async function getProductAvailability(): Promise<UserSlotAvailability[]> {
const products = await db
.select({
id: productInfo.id,
name: productInfo.name,
isOutOfStock: productInfo.isOutOfStock,
isFlashAvailable: productInfo.isFlashAvailable,
})
.from(productInfo)
.where(eq(productInfo.isSuspended, false))
const skus = await db.query.productSkus.findMany({
where: eq(productSkus.isSuspended, false),
with: {
product: { columns: { name: true } },
},
})
return products.map((product) => ({
id: product.id,
name: product.name,
isOutOfStock: product.isOutOfStock,
isFlashAvailable: product.isFlashAvailable,
return skus.map((sku) => ({
id: sku.id,
name: sku.product?.name ?? 'Unknown',
isOutOfStock: sku.isOutOfStock,
isFlashAvailable: sku.isFlashAvailable,
}))
}

View file

@ -1,22 +1,10 @@
import { db } from '../db/db_index'
import { productInfo, storeInfo, units } from '../db/schema'
import { and, eq, sql } from 'drizzle-orm'
import { productInfo, productSkus, storeInfo } from '../db/schema'
import { and, eq, inArray, sql } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm'
import type { UserStoreDetailData, UserStoreProductData, UserStoreSummaryData, StoreSummary } from '@packages/shared'
type StoreRow = InferSelectModel<typeof storeInfo>
type StoreProductRow = {
id: number
name: string
shortDescription: string | null
price: string | null
marketPrice: string | null
images: unknown
isOutOfStock: boolean
incrementStep: number
unitShortNotation: string
productQuantity: number
}
const getStringArray = (value: unknown): string[] | null => {
if (!Array.isArray(value)) return null
@ -24,32 +12,59 @@ const getStringArray = (value: unknown): string[] | null => {
}
export async function getStoreSummaries(): Promise<UserStoreSummaryData[]> {
// Count SKUs per store, filtering by suspended SKUs
const storesData = await db
.select({
id: storeInfo.id,
name: storeInfo.name,
description: storeInfo.description,
imageUrl: storeInfo.imageUrl,
productCount: sql<number>`count(${productInfo.id})`.as('productCount'),
productCount: sql<number>`count(${productSkus.id})`.as('productCount'),
})
.from(storeInfo)
.leftJoin(
productInfo,
and(eq(productInfo.storeId, storeInfo.id), eq(productInfo.isSuspended, false))
eq(productInfo.storeId, storeInfo.id)
)
.leftJoin(
productSkus,
and(
eq(productSkus.productId, productInfo.id),
eq(productSkus.isSuspended, false)
)
)
.groupBy(storeInfo.id)
const storesWithDetails = await Promise.all(
storesData.map(async (store) => {
const sampleProducts = await db
.select({
id: productInfo.id,
name: productInfo.name,
images: productInfo.images,
})
.from(productInfo)
.where(and(eq(productInfo.storeId, store.id), eq(productInfo.isSuspended, false)))
.limit(3)
let sampleProducts: any[] = []
// Get sample SKUs from this store
if (store.productCount > 0) {
const storeProductIds = await db
.select({ id: productInfo.id })
.from(productInfo)
.where(eq(productInfo.storeId, store.id))
const productIdArr = storeProductIds.map((p) => p.id)
if (productIdArr.length > 0) {
const skus = await db.query.productSkus.findMany({
where: and(
inArray(productSkus.productId, productIdArr),
eq(productSkus.isSuspended, false)
),
with: {
product: { columns: { name: true } },
},
columns: { id: true, images: true, name: true },
limit: 3,
})
sampleProducts = skus.map((sku) => ({
id: sku.id,
name: sku.product?.name ?? sku.name ?? 'Unknown',
images: getStringArray(sku.images),
}))
}
}
return {
id: store.id,
@ -57,11 +72,7 @@ export async function getStoreSummaries(): Promise<UserStoreSummaryData[]> {
description: store.description ?? null,
imageUrl: store.imageUrl ?? null,
productCount: store.productCount || 0,
sampleProducts: sampleProducts.map((product) => ({
id: product.id,
name: product.name,
images: getStringArray(product.images),
})),
sampleProducts,
}
})
)
@ -84,36 +95,42 @@ export async function getStoreDetail(storeId: number): Promise<UserStoreDetailDa
return null
}
const productsData = await db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
price: productInfo.price,
marketPrice: productInfo.marketPrice,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
incrementStep: productInfo.incrementStep,
unitShortNotation: units.shortNotation,
productQuantity: productInfo.productQuantity,
})
const storeProductIds = await db
.select({ id: productInfo.id })
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(and(eq(productInfo.storeId, storeId), eq(productInfo.isSuspended, false)))
.where(eq(productInfo.storeId, storeId))
const products = productsData.map((product: StoreProductRow): UserStoreProductData => ({
id: product.id,
name: product.name,
shortDescription: product.shortDescription ?? null,
price: String(product.price ?? '0'),
marketPrice: product.marketPrice ? String(product.marketPrice) : null,
incrementStep: product.incrementStep,
unit: product.unitShortNotation,
unitNotation: product.unitShortNotation,
images: getStringArray(product.images),
isOutOfStock: product.isOutOfStock,
productQuantity: product.productQuantity,
}))
const productIdArr = storeProductIds.map((p) => p.id)
const skus = productIdArr.length > 0
? await db.query.productSkus.findMany({
where: and(
inArray(productSkus.productId, productIdArr),
eq(productSkus.isSuspended, false)
),
with: {
product: true,
features: true,
},
})
: []
const products: UserStoreProductData[] = skus.map((sku) => {
const features = sku.features || []
return {
id: sku.id,
name: sku.product?.name ?? 'Unknown',
shortDescription: sku.product?.shortDescription ?? null,
price: String(sku.price ?? '0'),
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
incrementStep: sku.product?.incrementStep ?? 1,
unit: features.map((f) => f.featureValue).join(' '),
unitNotation: features.map((f) => f.featureValue).join(' '),
images: getStringArray(sku.images),
isOutOfStock: sku.isOutOfStock,
productQuantity: 1,
}
})
return {
store: {
@ -126,10 +143,6 @@ export async function getStoreDetail(storeId: number): Promise<UserStoreDetailDa
}
}
/**
* Get simple store summary (id, name, description only)
* Used for common API endpoints
*/
export async function getStoresSummary(): Promise<StoreSummary[]> {
return db.query.storeInfo.findMany({
columns: {

View file

@ -121,7 +121,7 @@ export interface UserBanner {
name: string;
imageUrl: string;
description: string | null;
productIds: number[] | null;
skuIds: number[] | null;
redirectUrl: string | null;
serialNum: number | null;
isActive: boolean;