import { db } from '../db/db_index' import { productInfo, units, specialDeals, deliverySlotInfo, productTags, productReviews, productGroupInfo, productGroupMembership, productTagInfo, users, storeInfo, } from '../db/schema' import { and, desc, eq, inArray, sql } from 'drizzle-orm' import type { InferInsertModel, InferSelectModel } from 'drizzle-orm' import type { AdminProduct, AdminProductGroupInfo, AdminProductTagInfo, AdminProductTagWithProducts, AdminProductReview, AdminProductWithDetails, AdminProductWithRelations, AdminSpecialDeal, AdminUnit, AdminUpdateSlotProductsResult, Store, } from '@packages/shared' type ProductRow = InferSelectModel type UnitRow = InferSelectModel type StoreRow = InferSelectModel type SpecialDealRow = InferSelectModel type ProductTagInfoRow = InferSelectModel const getStringArray = (value: unknown): string[] | null => { if (!Array.isArray(value)) return null return value.map((item) => String(item)) } const mapUnit = (unit: UnitRow): AdminUnit => ({ id: unit.id, shortNotation: unit.shortNotation, fullName: unit.fullName, }) const mapStore = (store: StoreRow): Store => ({ id: store.id, name: store.name, description: store.description, imageUrl: store.imageUrl, owner: store.owner, createdAt: store.createdAt, // updatedAt: store.createdAt, }) const mapProduct = (product: ProductRow): AdminProduct => ({ id: product.id, name: product.name, shortDescription: product.shortDescription ?? null, longDescription: product.longDescription ?? null, unitId: product.unitId, price: product.price.toString(), marketPrice: product.marketPrice ? product.marketPrice.toString() : null, images: getStringArray(product.images), imageKeys: getStringArray(product.images), isOutOfStock: product.isOutOfStock, isSuspended: product.isSuspended, isFlashAvailable: product.isFlashAvailable, flashPrice: product.flashPrice ? product.flashPrice.toString() : null, createdAt: product.createdAt, incrementStep: product.incrementStep, productQuantity: product.productQuantity, storeId: product.storeId, }) const mapSpecialDeal = (deal: SpecialDealRow): AdminSpecialDeal => ({ id: deal.id, productId: deal.productId, quantity: deal.quantity.toString(), price: deal.price.toString(), validTill: deal.validTill, }) const mapTagInfo = (tag: ProductTagInfoRow): AdminProductTagInfo => ({ id: tag.id, tagName: tag.tagName, tagDescription: tag.tagDescription ?? null, imageUrl: tag.imageUrl ?? null, isDashboardTag: tag.isDashboardTag, relatedStores: tag.relatedStores, createdAt: tag.createdAt, }) export async function getAllProducts(): Promise { const products = await db.query.productInfo.findMany({ orderBy: productInfo.name, with: { unit: true, store: true, }, }) return products.map((product) => ({ ...mapProduct(product), unit: mapUnit(product.unit), store: product.store ? mapStore(product.store) : null, })) } export async function getProductById(id: number): Promise { const product = await db.query.productInfo.findFirst({ where: eq(productInfo.id, id), with: { unit: true, }, }) if (!product) { return null } const deals = await db.query.specialDeals.findMany({ where: eq(specialDeals.productId, id), orderBy: specialDeals.quantity, }) const productTagsData = await db.query.productTags.findMany({ where: eq(productTags.productId, id), with: { tag: true, }, }) return { ...mapProduct(product), unit: mapUnit(product.unit), deals: deals.map(mapSpecialDeal), tags: productTagsData.map((tag) => mapTagInfo(tag.tag)), } } export async function deleteProduct(id: number): Promise { const [deletedProduct] = await db .delete(productInfo) .where(eq(productInfo.id, id)) .returning() if (!deletedProduct) { return null } return mapProduct(deletedProduct) } type ProductInfoInsert = InferInsertModel type ProductInfoUpdate = Partial export async function createProduct(input: ProductInfoInsert): Promise { const [product] = await db.insert(productInfo).values(input).returning() return mapProduct(product) } export async function updateProduct(id: number, updates: ProductInfoUpdate): Promise { const [product] = await db.update(productInfo) .set(updates) .where(eq(productInfo.id, id)) .returning() if (!product) { return null } return mapProduct(product) } export async function updateSlotProducts(slotId: string, productIds: string[]): Promise { const slot = await db.query.deliverySlotInfo.findFirst({ where: eq(deliverySlotInfo.id, parseInt(slotId)), }) if (!slot) { throw new Error(`Slot ${slotId} not found`) } const currentProductIds = slot.productIds || [] const newProductIds = productIds.map((id) => parseInt(id)) // Simply update the productIds array await db.update(deliverySlotInfo) .set({ productIds: newProductIds }) .where(eq(deliverySlotInfo.id, parseInt(slotId))) const productsToAdd = newProductIds.filter((id) => !currentProductIds.includes(id)) const productsToRemove = currentProductIds.filter((id) => !newProductIds.includes(id)) return { message: 'Slot products updated successfully', added: productsToAdd.length, removed: productsToRemove.length, } } export async function getAllProductTags(): Promise { const tags = await db.query.productTagInfo.findMany({ with: { products: { with: { product: true, }, }, }, }) return tags.map((tag) => ({ ...mapTagInfo(tag), products: tag.products.map((assignment) => ({ productId: assignment.productId, tagId: assignment.tagId, assignedAt: assignment.assignedAt, product: mapProduct(assignment.product), })), })) } export async function getAllProductTagInfos(): Promise { const tags = await db.query.productTagInfo.findMany({ orderBy: productTagInfo.tagName, }) return tags.map(mapTagInfo) } export async function getProductTagInfoById(tagId: number): Promise { const tag = await db.query.productTagInfo.findFirst({ where: eq(productTagInfo.id, tagId), }) if (!tag) { return null } return mapTagInfo(tag) } export async function getSlotsProductIds(slotIds: number[]): Promise> { if (slotIds.length === 0) { return {} } const slots = await db.query.deliverySlotInfo.findMany({ where: inArray(deliverySlotInfo.id, slotIds), }) const result: Record = {} for (const slot of slots) { result[slot.id] = slot.productIds || [] } slotIds.forEach((slotId) => { if (!result[slotId]) { result[slotId] = [] } }) return result } 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, adminResponse: productReviews.adminResponse, adminResponseImages: productReviews.adminResponseImages, 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: AdminProductReview[] = reviews.map((review) => ({ id: review.id, reviewBody: review.reviewBody, ratings: review.ratings, imageUrls: review.imageUrls, reviewTime: review.reviewTime, adminResponse: review.adminResponse ?? null, adminResponseImages: review.adminResponseImages, userName: review.userName ?? null, })) return { reviews: mappedReviews, totalCount, } } export async function respondToReview( reviewId: number, adminResponse: string | undefined, adminResponseImages: string[] ): Promise { const [updatedReview] = await db .update(productReviews) .set({ adminResponse, adminResponseImages, }) .where(eq(productReviews.id, reviewId)) .returning() if (!updatedReview) { return null } return { id: updatedReview.id, reviewBody: updatedReview.reviewBody, ratings: updatedReview.ratings, imageUrls: updatedReview.imageUrls, reviewTime: updatedReview.reviewTime, adminResponse: updatedReview.adminResponse ?? null, adminResponseImages: updatedReview.adminResponseImages, userName: null, } } export async function getAllProductGroups() { const groups = await db.query.productGroupInfo.findMany({ with: { memberships: { with: { product: true, }, }, }, orderBy: desc(productGroupInfo.createdAt), }) return groups.map((group) => ({ id: group.id, groupName: group.groupName, description: group.description ?? null, createdAt: group.createdAt, products: group.memberships.map((membership) => mapProduct(membership.product)), productCount: group.memberships.length, memberships: group.memberships })) } export async function createProductGroup( groupName: string, description: string | undefined, productIds: number[] ): Promise { const [newGroup] = await db .insert(productGroupInfo) .values({ groupName, description, }) .returning() if (productIds.length > 0) { const memberships = productIds.map((productId) => ({ productId, groupId: newGroup.id, })) await db.insert(productGroupMembership).values(memberships) } return { id: newGroup.id, groupName: newGroup.groupName, description: newGroup.description ?? null, createdAt: newGroup.createdAt, } } export async function updateProductGroup( id: number, groupName: string | undefined, description: string | undefined, productIds: number[] | undefined ): Promise { const updateData: Partial<{ groupName: string description: string | null }> = {} if (groupName !== undefined) updateData.groupName = groupName if (description !== undefined) updateData.description = description const [updatedGroup] = await db .update(productGroupInfo) .set(updateData) .where(eq(productGroupInfo.id, id)) .returning() if (!updatedGroup) { return null } if (productIds !== undefined) { await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id)) if (productIds.length > 0) { const memberships = productIds.map((productId) => ({ productId, groupId: id, })) await db.insert(productGroupMembership).values(memberships) } } return { id: updatedGroup.id, groupName: updatedGroup.groupName, description: updatedGroup.description ?? null, createdAt: updatedGroup.createdAt, } } export async function deleteProductGroup(id: number): Promise { await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id)) const [deletedGroup] = await db .delete(productGroupInfo) .where(eq(productGroupInfo.id, id)) .returning() if (!deletedGroup) { return null } return { id: deletedGroup.id, groupName: deletedGroup.groupName, description: deletedGroup.description ?? null, createdAt: deletedGroup.createdAt, } } export async function updateProductPrices(updates: Array<{ productId: number price?: number marketPrice?: number | null flashPrice?: number | null isFlashAvailable?: boolean }>) { if (updates.length === 0) { return { updatedCount: 0, invalidIds: [] } } const productIds = updates.map((update) => update.productId) const existingProducts = await db.query.productInfo.findMany({ where: inArray(productInfo.id, productIds), columns: { id: true }, }) const existingIds = new Set(existingProducts.map((product) => product.id)) const invalidIds = productIds.filter((id) => !existingIds.has(id)) if (invalidIds.length > 0) { return { updatedCount: 0, invalidIds } } const updatePromises = updates.map((update) => { const { productId, price, marketPrice, flashPrice, isFlashAvailable } = update const updateData: Partial> = {} if (price !== undefined) updateData.price = price.toString() if (marketPrice !== undefined) updateData.marketPrice = marketPrice === null ? null : marketPrice.toString() if (flashPrice !== undefined) updateData.flashPrice = flashPrice === null ? null : flashPrice.toString() if (isFlashAvailable !== undefined) updateData.isFlashAvailable = isFlashAvailable return db .update(productInfo) .set(updateData) .where(eq(productInfo.id, productId)) }) await Promise.all(updatePromises) return { updatedCount: updates.length, invalidIds: [] } } // ============================================================================ // Product Tag CRUD Operations // ============================================================================ export interface CreateProductTagInput { tagName: string tagDescription?: string | null imageUrl?: string | null isDashboardTag?: boolean relatedStores?: number[] } export async function createProductTag(input: CreateProductTagInput): Promise { const [tag] = await db.insert(productTagInfo).values({ tagName: input.tagName, tagDescription: input.tagDescription || null, imageUrl: input.imageUrl || null, isDashboardTag: input.isDashboardTag || false, relatedStores: input.relatedStores || [], }).returning() return { ...mapTagInfo(tag), products: [], } } export async function getProductTagById(tagId: number): Promise { const tag = await db.query.productTagInfo.findFirst({ where: eq(productTagInfo.id, tagId), with: { products: { with: { product: true, }, }, }, }) if (!tag) { return null } return { ...mapTagInfo(tag), products: tag.products.map((assignment) => ({ productId: assignment.productId, tagId: assignment.tagId, assignedAt: assignment.assignedAt, product: mapProduct(assignment.product), })), } } export interface UpdateProductTagInput { tagName?: string tagDescription?: string | null imageUrl?: string | null isDashboardTag?: boolean relatedStores?: number[] } export async function updateProductTag(tagId: number, input: UpdateProductTagInput): Promise { const [tag] = await db.update(productTagInfo).set({ ...(input.tagName !== undefined && { tagName: input.tagName }), ...(input.tagDescription !== undefined && { tagDescription: input.tagDescription }), ...(input.imageUrl !== undefined && { imageUrl: input.imageUrl }), ...(input.isDashboardTag !== undefined && { isDashboardTag: input.isDashboardTag }), ...(input.relatedStores !== undefined && { relatedStores: input.relatedStores }), }).where(eq(productTagInfo.id, tagId)).returning() const fullTag = await db.query.productTagInfo.findFirst({ where: eq(productTagInfo.id, tagId), with: { products: { with: { product: true, }, }, }, }) return { ...mapTagInfo(tag), products: fullTag?.products.map((assignment) => ({ productId: assignment.productId, tagId: assignment.tagId, assignedAt: assignment.assignedAt, product: mapProduct(assignment.product), })) || [], } } export async function deleteProductTag(tagId: number): Promise { await db.delete(productTagInfo).where(eq(productTagInfo.id, tagId)) } export async function checkProductTagExistsByName(tagName: string): Promise { const tag = await db.query.productTagInfo.findFirst({ where: eq(productTagInfo.tagName, tagName), }) return !!tag } // ========================================================================== // Product Helpers for Admin Controller // ========================================================================== export async function checkProductExistsByName(name: string): Promise { const product = await db.query.productInfo.findFirst({ where: eq(productInfo.name, name), columns: { id: true }, }) return !!product } export async function checkUnitExists(unitId: number): Promise { const unit = await db.query.units.findFirst({ where: eq(units.id, unitId), columns: { id: true }, }) return !!unit } export async function getProductImagesById(productId: number): Promise { const product = await db.query.productInfo.findFirst({ where: eq(productInfo.id, productId), columns: { images: true }, }) if (!product) { return null } return getStringArray(product.images) || [] } export async function replaceProductTags(productId: number, tagIds: number[]): Promise { await db.delete(productTags).where(eq(productTags.productId, productId)) if (tagIds.length === 0) { return } const tagAssociations = tagIds.map((tagId) => ({ productId, tagId, })) await db.insert(productTags).values(tagAssociations) }