254 lines
7.7 KiB
TypeScript
254 lines
7.7 KiB
TypeScript
import { db } from '../db/db_index'
|
|
import { deliverySlotInfo, productInfo, productReviews, productSlots, productTags, specialDeals, storeInfo, units, users } from '../db/schema'
|
|
import { and, desc, eq, gt, inArray, sql } from 'drizzle-orm'
|
|
import type { UserProductDetailData, UserProductReview } from '@packages/shared'
|
|
|
|
const getStringArray = (value: unknown): string[] | null => {
|
|
if (!Array.isArray(value)) return 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)
|
|
|
|
if (productData.length === 0) {
|
|
return null
|
|
}
|
|
|
|
const product = productData[0]
|
|
|
|
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,
|
|
price: specialDeals.price,
|
|
validTill: specialDeals.validTill,
|
|
})
|
|
.from(specialDeals)
|
|
.where(
|
|
and(
|
|
eq(specialDeals.productId, productId),
|
|
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,
|
|
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()
|
|
specialDeals: specialDealsData.map((deal) => ({
|
|
quantity: String(deal.quantity ?? '0'),
|
|
price: String(deal.price ?? '0'),
|
|
validTill: deal.validTill,
|
|
})),
|
|
}
|
|
}
|
|
|
|
export async function getProductReviews(productId: number, limit: number, offset: number) {
|
|
const reviews = await db
|
|
.select({
|
|
id: productReviews.id,
|
|
reviewBody: productReviews.reviewBody,
|
|
ratings: productReviews.ratings,
|
|
imageUrls: productReviews.imageUrls,
|
|
reviewTime: productReviews.reviewTime,
|
|
userName: users.name,
|
|
})
|
|
.from(productReviews)
|
|
.innerJoin(users, eq(productReviews.userId, users.id))
|
|
.where(eq(productReviews.productId, productId))
|
|
.orderBy(desc(productReviews.reviewTime))
|
|
.limit(limit)
|
|
.offset(offset)
|
|
|
|
const totalCountResult = await db
|
|
.select({ count: sql`count(*)` })
|
|
.from(productReviews)
|
|
.where(eq(productReviews.productId, productId))
|
|
|
|
const totalCount = Number(totalCountResult[0].count)
|
|
|
|
const mappedReviews: UserProductReview[] = reviews.map((review) => ({
|
|
id: review.id,
|
|
reviewBody: review.reviewBody,
|
|
ratings: review.ratings,
|
|
imageUrls: getStringArray(review.imageUrls),
|
|
reviewTime: review.reviewTime,
|
|
userName: review.userName ?? null,
|
|
}))
|
|
|
|
return {
|
|
reviews: mappedReviews,
|
|
totalCount,
|
|
}
|
|
}
|
|
|
|
export async function getProductById(productId: number) {
|
|
return db.query.productInfo.findFirst({
|
|
where: eq(productInfo.id, productId),
|
|
})
|
|
}
|
|
|
|
export async function createProductReview(
|
|
userId: number,
|
|
productId: number,
|
|
reviewBody: string,
|
|
ratings: number,
|
|
imageUrls: string[]
|
|
): Promise<UserProductReview> {
|
|
const [newReview] = await db.insert(productReviews).values({
|
|
userId,
|
|
productId,
|
|
reviewBody,
|
|
ratings,
|
|
imageUrls,
|
|
}).returning({
|
|
id: productReviews.id,
|
|
reviewBody: productReviews.reviewBody,
|
|
ratings: productReviews.ratings,
|
|
imageUrls: productReviews.imageUrls,
|
|
reviewTime: productReviews.reviewTime,
|
|
})
|
|
|
|
return {
|
|
id: newReview.id,
|
|
reviewBody: newReview.reviewBody,
|
|
ratings: newReview.ratings,
|
|
imageUrls: getStringArray(newReview.imageUrls),
|
|
reviewTime: newReview.reviewTime,
|
|
userName: null,
|
|
}
|
|
}
|
|
|
|
export interface ProductSummaryData {
|
|
id: number
|
|
name: string
|
|
shortDescription: string | null
|
|
price: string
|
|
marketPrice: string | null
|
|
images: unknown
|
|
isOutOfStock: boolean
|
|
unitShortNotation: string
|
|
productQuantity: number
|
|
}
|
|
|
|
export async function getAllProductsWithUnits(tagId?: number): Promise<ProductSummaryData[]> {
|
|
let productIds: number[] | null = null
|
|
|
|
// If tagId is provided, get products that have this tag
|
|
if (tagId) {
|
|
const taggedProducts = await db
|
|
.select({ productId: productTags.productId })
|
|
.from(productTags)
|
|
.where(eq(productTags.tagId, tagId))
|
|
|
|
productIds = taggedProducts.map(tp => tp.productId)
|
|
}
|
|
|
|
let whereCondition = undefined
|
|
|
|
// Filter by product IDs if tag filtering is applied
|
|
if (productIds && productIds.length > 0) {
|
|
whereCondition = inArray(productInfo.id, productIds)
|
|
}
|
|
|
|
const results = await db
|
|
.select({
|
|
id: productInfo.id,
|
|
name: productInfo.name,
|
|
shortDescription: productInfo.shortDescription,
|
|
price: productInfo.price,
|
|
marketPrice: productInfo.marketPrice,
|
|
images: productInfo.images,
|
|
isOutOfStock: productInfo.isOutOfStock,
|
|
unitShortNotation: units.shortNotation,
|
|
productQuantity: productInfo.productQuantity,
|
|
})
|
|
.from(productInfo)
|
|
.innerJoin(units, eq(productInfo.unitId, units.id))
|
|
.where(whereCondition)
|
|
|
|
return results.map((product) => ({
|
|
...product,
|
|
price: String(product.price ?? '0'),
|
|
marketPrice: product.marketPrice ? String(product.marketPrice) : null,
|
|
}))
|
|
}
|
|
|
|
/**
|
|
* 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))
|
|
|
|
return suspendedProducts.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> {
|
|
const result = await db
|
|
.select({ deliveryTime: deliverySlotInfo.deliveryTime })
|
|
.from(productSlots)
|
|
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
|
|
.where(
|
|
and(
|
|
eq(productSlots.productId, productId),
|
|
eq(deliverySlotInfo.isActive, true),
|
|
eq(deliverySlotInfo.isCapacityFull, false),
|
|
gt(deliverySlotInfo.deliveryTime, sql`CURRENT_TIMESTAMP`)
|
|
)
|
|
)
|
|
.orderBy(deliverySlotInfo.deliveryTime)
|
|
.limit(1)
|
|
|
|
return result[0]?.deliveryTime || null
|
|
}
|