From 3c836e274d31ef89a02a628cc62cdc9988ff669c Mon Sep 17 00:00:00 2001 From: shafi54 <108669266+shafi-aviz@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:30:01 +0530 Subject: [PATCH] enh --- apps/backend/src/dbService.ts | 74 ++- .../src/trpc/apis/admin-apis/apis/product.ts | 268 +++++++-- .../src/trpc/apis/admin-apis/apis/slots.ts | 223 +++++-- .../apis/admin-apis/apis/vendor-snippets.ts | 251 +++++++- packages/db_helper_postgres/index.ts | 22 +- .../src/admin-apis/order.ts | 43 +- .../src/admin-apis/product.ts | 560 +++++++++++++++--- .../src/admin-apis/slots.ts | 400 ++++++++++--- .../src/admin-apis/staff-user.ts | 2 +- .../src/admin-apis/store.ts | 8 +- .../src/admin-apis/vendor-snippets.ts | 214 +++++-- packages/shared/types/admin.ts | 366 +++++++++++- 12 files changed, 2099 insertions(+), 332 deletions(-) diff --git a/apps/backend/src/dbService.ts b/apps/backend/src/dbService.ts index cd29594..76f8e04 100644 --- a/apps/backend/src/dbService.ts +++ b/apps/backend/src/dbService.ts @@ -92,12 +92,17 @@ export { getOrderItemsByOrderIds, getOrderStatusByOrderIds, updateVendorOrderItemPackaging, + getVendorOrders, // Product methods getAllProducts, getProductById, + deleteProduct, createProduct, updateProduct, toggleProductOutOfStock, + updateSlotProducts, + getSlotProductIds, + getSlotsProductIds, getAllUnits, getAllProductTags, getProductReviews, @@ -108,16 +113,15 @@ export { deleteProductGroup, addProductToGroup, removeProductFromGroup, + updateProductPrices, // Slots methods - getAllSlots, - getSlotById, - createSlot, - updateSlot, - deleteSlot, - getSlotProducts, - addProductToSlot, - removeProductFromSlot, - clearSlotProducts, + getActiveSlotsWithProducts, + getActiveSlots, + getSlotsAfterDate, + getSlotByIdWithRelations, + createSlotWithRelations, + updateSlotWithRelations, + deleteSlotById, updateSlotCapacity, getSlotDeliverySequence, updateSlotDeliverySequence, @@ -164,6 +168,58 @@ export type { AdminGetAllOrdersResultWithUserId, AdminRebalanceSlotsResult, AdminCancelOrderResult, + AdminUnit, + AdminProduct, + AdminProductWithRelations, + AdminProductWithDetails, + AdminProductTagInfo, + AdminProductTagWithProducts, + AdminProductListResponse, + AdminProductResponse, + AdminDeleteProductResult, + AdminToggleOutOfStockResult, + AdminUpdateSlotProductsResult, + AdminSlotProductIdsResult, + AdminSlotsProductIdsResult, + AdminProductReview, + AdminProductReviewWithSignedUrls, + AdminProductReviewsResult, + AdminProductReviewResponse, + AdminProductGroup, + AdminProductGroupsResult, + AdminProductGroupResponse, + AdminProductGroupInfo, + AdminUpdateProductPricesResult, + AdminDeliverySlot, + AdminSlotProductSummary, + AdminSlotWithProducts, + AdminSlotWithProductsAndSnippets, + AdminSlotWithProductsAndSnippetsBase, + AdminSlotsResult, + AdminSlotsListResult, + AdminSlotResult, + AdminSlotCreateResult, + AdminSlotUpdateResult, + AdminSlotDeleteResult, + AdminDeliverySequence, + AdminDeliverySequenceResult, + AdminUpdateDeliverySequenceResult, + AdminUpdateSlotCapacityResult, + AdminVendorSnippet, + AdminVendorSnippetWithAccess, + AdminVendorSnippetWithSlot, + AdminVendorSnippetProduct, + AdminVendorSnippetWithProducts, + AdminVendorSnippetCreateInput, + AdminVendorSnippetUpdateInput, + AdminVendorSnippetDeleteResult, + AdminVendorSnippetOrderProduct, + AdminVendorSnippetOrderSummary, + AdminVendorSnippetOrdersResult, + AdminVendorSnippetOrdersWithSlotResult, + AdminVendorOrderSummary, + AdminUpcomingSlotsResult, + AdminVendorUpdatePackagingResult, } from '@packages/shared'; export type { diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/product.ts b/apps/backend/src/trpc/apis/admin-apis/apis/product.ts index a7b6d2c..febce94 100644 --- a/apps/backend/src/trpc/apis/admin-apis/apis/product.ts +++ b/apps/backend/src/trpc/apis/admin-apis/apis/product.ts @@ -1,24 +1,47 @@ import { router, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import { db } from '@/src/db/db_index' -import { productInfo, units, specialDeals, productSlots, productTags, productReviews, users, productGroupInfo, productGroupMembership } from '@/src/db/schema' -import { eq, and, inArray, desc, sql } from 'drizzle-orm'; +import { z } from 'zod' import { ApiError } from '@/src/lib/api-error' -import { imageUploadS3, generateSignedUrlsFromS3Urls, getOriginalUrlFromSignedUrl, claimUploadUrl } from '@/src/lib/s3-client' -import { deleteS3Image } from '@/src/lib/delete-image' -import type { SpecialDeal } from '@/src/db/types' +import { generateSignedUrlsFromS3Urls, claimUploadUrl } from '@/src/lib/s3-client' import { scheduleStoreInitialization } from '@/src/stores/store-initializer' +import { + getAllProducts as getAllProductsInDb, + getProductById as getProductByIdInDb, + deleteProduct as deleteProductInDb, + toggleProductOutOfStock as toggleProductOutOfStockInDb, + updateSlotProducts as updateSlotProductsInDb, + getSlotProductIds as getSlotProductIdsInDb, + getSlotsProductIds as getSlotsProductIdsInDb, + getProductReviews as getProductReviewsInDb, + respondToReview as respondToReviewInDb, + getAllProductGroups as getAllProductGroupsInDb, + createProductGroup as createProductGroupInDb, + updateProductGroup as updateProductGroupInDb, + deleteProductGroup as deleteProductGroupInDb, + updateProductPrices as updateProductPricesInDb, +} from '@/src/dbService' +import type { + AdminProductGroupsResult, + AdminProductGroupResponse, + AdminProductReviewsResult, + AdminProductReviewResponse, + AdminProductListResponse, + AdminProductResponse, + AdminDeleteProductResult, + AdminToggleOutOfStockResult, + AdminUpdateSlotProductsResult, + AdminSlotProductIdsResult, + AdminSlotsProductIdsResult, + AdminUpdateProductPricesResult, +} from '@packages/shared' -type CreateDeal = { - quantity: number; - price: number; - validTill: string; -}; - export const productRouter = router({ getProducts: protectedProcedure - .query(async ({ ctx }) => { + .query(async (): Promise => { + const products = await getAllProductsInDb() + + /* + // Old implementation - direct DB query: const products = await db.query.productInfo.findMany({ orderBy: productInfo.name, with: { @@ -26,28 +49,32 @@ export const productRouter = router({ store: true, }, }); + */ - // Generate signed URLs for all product images const productsWithSignedUrls = await Promise.all( products.map(async (product) => ({ ...product, images: await generateSignedUrlsFromS3Urls((product.images as string[]) || []), })) - ); + ) return { products: productsWithSignedUrls, count: productsWithSignedUrls.length, - }; + } }), getProductById: protectedProcedure .input(z.object({ id: z.number(), })) - .query(async ({ input, ctx }) => { + .query(async ({ input }): Promise => { const { id } = input; + const product = await getProductByIdInDb(id) + + /* + // Old implementation - direct DB queries: const product = await db.query.productInfo.findFirst({ where: eq(productInfo.id, id), with: { @@ -84,15 +111,33 @@ export const productRouter = router({ return { product: productWithSignedUrls, }; + */ + + if (!product) { + throw new ApiError('Product not found', 404) + } + + const productWithSignedUrls = { + ...product, + images: await generateSignedUrlsFromS3Urls((product.images as string[]) || []), + } + + return { + product: productWithSignedUrls, + } }), deleteProduct: protectedProcedure .input(z.object({ id: z.number(), })) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input }): Promise => { const { id } = input; + const deletedProduct = await deleteProductInDb(id) + + /* + // Old implementation - direct DB query: const [deletedProduct] = await db .delete(productInfo) .where(eq(productInfo.id, id)) @@ -101,22 +146,31 @@ export const productRouter = router({ if (!deletedProduct) { throw new ApiError("Product not found", 404); } + */ + + if (!deletedProduct) { + throw new ApiError('Product not found', 404) + } // Reinitialize stores to reflect changes scheduleStoreInitialization() return { - message: "Product deleted successfully", - }; + message: 'Product deleted successfully', + } }), toggleOutOfStock: protectedProcedure .input(z.object({ id: z.number(), })) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input }): Promise => { const { id } = input; + const updatedProduct = await toggleProductOutOfStockInDb(id) + + /* + // Old implementation - direct DB queries: const product = await db.query.productInfo.findFirst({ where: eq(productInfo.id, id), }); @@ -132,14 +186,18 @@ export const productRouter = router({ }) .where(eq(productInfo.id, id)) .returning(); + */ + + if (!updatedProduct) { + throw new ApiError('Product not found', 404) + } - // Reinitialize stores to reflect changes scheduleStoreInitialization() return { product: updatedProduct, message: `Product marked as ${updatedProduct.isOutOfStock ? 'out of stock' : 'in stock'}`, - }; + } }), updateSlotProducts: protectedProcedure @@ -147,13 +205,17 @@ export const productRouter = router({ slotId: z.string(), productIds: z.array(z.string()), })) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input }): Promise => { const { slotId, productIds } = input; if (!Array.isArray(productIds)) { throw new ApiError("productIds must be an array", 400); } + const result = await updateSlotProductsInDb(slotId, productIds) + + /* + // Old implementation - direct DB queries: // Get current associations const currentAssociations = await db.query.productSlots.findMany({ where: eq(productSlots.slotId, parseInt(slotId)), @@ -197,15 +259,28 @@ export const productRouter = router({ added: productsToAdd.length, removed: productsToRemove.length, }; + */ + + scheduleStoreInitialization() + + return { + message: 'Slot products updated successfully', + added: result.added, + removed: result.removed, + } }), getSlotProductIds: protectedProcedure .input(z.object({ slotId: z.string(), })) - .query(async ({ input, ctx }) => { + .query(async ({ input }): Promise => { const { slotId } = input; + const productIds = await getSlotProductIdsInDb(slotId) + + /* + // Old implementation - direct DB queries: const associations = await db.query.productSlots.findMany({ where: eq(productSlots.slotId, parseInt(slotId)), columns: { @@ -218,19 +293,28 @@ export const productRouter = router({ return { productIds, }; + */ + + return { + productIds, + } }), getSlotsProductIds: protectedProcedure .input(z.object({ slotIds: z.array(z.number()), })) - .query(async ({ input, ctx }) => { + .query(async ({ input }): Promise => { const { slotIds } = input; if (!Array.isArray(slotIds)) { throw new ApiError("slotIds must be an array", 400); } + const result = await getSlotsProductIdsInDb(slotIds) + + /* + // Old implementation - direct DB queries: if (slotIds.length === 0) { return {}; } @@ -261,6 +345,9 @@ export const productRouter = router({ }); return result; + */ + + return result }), getProductReviews: protectedProcedure @@ -269,9 +356,13 @@ export const productRouter = router({ limit: z.number().int().min(1).max(50).optional().default(10), offset: z.number().int().min(0).optional().default(0), })) - .query(async ({ input }) => { + .query(async ({ input }): Promise => { const { productId, limit, offset } = input; + const { reviews, totalCount } = await getProductReviewsInDb(productId, limit, offset) + + /* + // Old implementation - direct DB queries: const reviews = await db .select({ id: productReviews.id, @@ -309,6 +400,19 @@ export const productRouter = router({ const hasMore = offset + limit < totalCount; return { reviews: reviewsWithSignedUrls, hasMore }; + */ + + const reviewsWithSignedUrls = await Promise.all( + reviews.map(async (review) => ({ + ...review, + signedImageUrls: await generateSignedUrlsFromS3Urls((review.imageUrls as string[]) || []), + signedAdminImageUrls: await generateSignedUrlsFromS3Urls((review.adminResponseImages as string[]) || []), + })) + ) + + const hasMore = offset + limit < totalCount + + return { reviews: reviewsWithSignedUrls, hasMore } }), respondToReview: protectedProcedure @@ -318,9 +422,13 @@ export const productRouter = router({ adminResponseImages: z.array(z.string()).optional().default([]), uploadUrls: z.array(z.string()).optional().default([]), })) - .mutation(async ({ input }) => { + .mutation(async ({ input }): Promise => { const { reviewId, adminResponse, adminResponseImages, uploadUrls } = input; - + + const updatedReview = await respondToReviewInDb(reviewId, adminResponse, adminResponseImages) + + /* + // Old implementation - direct DB queries: const [updatedReview] = await db .update(productReviews) .set({ @@ -341,10 +449,25 @@ export const productRouter = router({ } return { success: true, review: updatedReview }; + */ + + if (!updatedReview) { + throw new ApiError('Review not found', 404) + } + + if (uploadUrls && uploadUrls.length > 0) { + await Promise.all(uploadUrls.map(url => claimUploadUrl(url))) + } + + return { success: true, review: updatedReview } }), getGroups: protectedProcedure - .query(async ({ ctx }) => { + .query(async (): Promise => { + const groups = await getAllProductGroupsInDb() + + /* + // Old implementation - direct DB queries: const groups = await db.query.productGroupInfo.findMany({ with: { memberships: { @@ -355,14 +478,18 @@ export const productRouter = router({ }, orderBy: desc(productGroupInfo.createdAt), }); + */ return { groups: groups.map(group => ({ ...group, - products: group.memberships.map(m => m.product), + products: group.memberships.map(m => ({ + ...m.product, + images: (m.product.images as string[]) || null, + })), productCount: group.memberships.length, })), - }; + } }), createGroup: protectedProcedure @@ -371,9 +498,13 @@ export const productRouter = router({ description: z.string().optional(), product_ids: z.array(z.number()).default([]), })) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input }): Promise => { const { group_name, description, product_ids } = input; + const newGroup = await createProductGroupInDb(group_name, description, product_ids) + + /* + // Old implementation - direct DB queries: const [newGroup] = await db .insert(productGroupInfo) .values({ @@ -398,6 +529,14 @@ export const productRouter = router({ group: newGroup, message: 'Group created successfully', }; + */ + + scheduleStoreInitialization() + + return { + group: newGroup, + message: 'Group created successfully', + } }), updateGroup: protectedProcedure @@ -407,9 +546,13 @@ export const productRouter = router({ description: z.string().optional(), product_ids: z.array(z.number()).optional(), })) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input }): Promise => { const { id, group_name, description, product_ids } = input; + const updatedGroup = await updateProductGroupInDb(id, group_name, description, product_ids) + + /* + // Old implementation - direct DB queries: const updateData: any = {}; if (group_name !== undefined) updateData.groupName = group_name; if (description !== undefined) updateData.description = description; @@ -446,15 +589,31 @@ export const productRouter = router({ group: updatedGroup, message: 'Group updated successfully', }; + */ + + if (!updatedGroup) { + throw new ApiError('Group not found', 404) + } + + scheduleStoreInitialization() + + return { + group: updatedGroup, + message: 'Group updated successfully', + } }), deleteGroup: protectedProcedure .input(z.object({ id: z.number(), })) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input }): Promise => { const { id } = input; + const deletedGroup = await deleteProductGroupInDb(id) + + /* + // Old implementation - direct DB queries: // Delete memberships first await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id)); @@ -474,6 +633,17 @@ export const productRouter = router({ return { message: 'Group deleted successfully', }; + */ + + if (!deletedGroup) { + throw new ApiError('Group not found', 404) + } + + scheduleStoreInitialization() + + return { + message: 'Group deleted successfully', + } }), updateProductPrices: protectedProcedure @@ -486,9 +656,17 @@ export const productRouter = router({ isFlashAvailable: z.boolean().optional(), })), })) - .mutation(async ({ input, ctx }) => { - const { updates } = input; + .mutation(async ({ input }): Promise => { + const { updates } = input; + if (updates.length === 0) { + throw new ApiError('No updates provided', 400) + } + + const result = await updateProductPricesInDb(updates) + + /* + // Old implementation - direct DB queries: if (updates.length === 0) { throw new ApiError('No updates provided', 400); } @@ -531,5 +709,17 @@ export const productRouter = router({ message: `Updated prices for ${updates.length} product(s)`, updatedCount: updates.length, }; - }), + */ + + if (result.invalidIds.length > 0) { + throw new ApiError(`Invalid product IDs: ${result.invalidIds.join(', ')}`, 400) + } + + scheduleStoreInitialization() + + return { + message: `Updated prices for ${result.updatedCount} product(s)`, + updatedCount: result.updatedCount, + } + }), }); diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/slots.ts b/apps/backend/src/trpc/apis/admin-apis/apis/slots.ts index 1cc40e8..e9c7fca 100644 --- a/apps/backend/src/trpc/apis/admin-apis/apis/slots.ts +++ b/apps/backend/src/trpc/apis/admin-apis/apis/slots.ts @@ -1,14 +1,38 @@ import { router, protectedProcedure } from "@/src/trpc/trpc-index" import { TRPCError } from "@trpc/server"; import { z } from "zod"; -import { db } from "@/src/db/db_index" -import { deliverySlotInfo, productSlots, productInfo, vendorSnippets, productGroupInfo } from "@/src/db/schema" -import { eq, inArray, and, desc } from "drizzle-orm"; import { ApiError } from "@/src/lib/api-error" import { appUrl } from "@/src/lib/env-exporter" import redisClient from "@/src/lib/redis-client" import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters" import { scheduleStoreInitialization } from '@/src/stores/store-initializer' +import { + getActiveSlotsWithProducts as getActiveSlotsWithProductsInDb, + getActiveSlots as getActiveSlotsInDb, + getSlotsAfterDate as getSlotsAfterDateInDb, + getSlotByIdWithRelations as getSlotByIdWithRelationsInDb, + createSlotWithRelations as createSlotWithRelationsInDb, + updateSlotWithRelations as updateSlotWithRelationsInDb, + deleteSlotById as deleteSlotByIdInDb, + updateSlotCapacity as updateSlotCapacityInDb, + getSlotDeliverySequence as getSlotDeliverySequenceInDb, + updateSlotDeliverySequence as updateSlotDeliverySequenceInDb, + updateSlotProducts as updateSlotProductsInDb, + getSlotsProductIds as getSlotsProductIdsInDb, +} from '@/src/dbService' +import type { + AdminDeliverySequenceResult, + AdminSlotResult, + AdminSlotsResult, + AdminSlotsListResult, + AdminSlotCreateResult, + AdminSlotUpdateResult, + AdminSlotDeleteResult, + AdminUpdateDeliverySequenceResult, + AdminUpdateSlotCapacityResult, + AdminSlotsProductIdsResult, + AdminUpdateSlotProductsResult, +} from '@packages/shared' interface CachedDeliverySequence { @@ -64,11 +88,15 @@ const updateDeliverySequenceSchema = z.object({ export const slotsRouter = router({ // Exact replica of GET /av/slots - getAll: protectedProcedure.query(async ({ ctx }) => { + getAll: protectedProcedure.query(async ({ ctx }): Promise => { if (!ctx.staffUser?.id) { throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); } + const slots = await getActiveSlotsWithProductsInDb() + + /* + // Old implementation - direct DB queries: const slots = await db.query.deliverySlotInfo .findMany({ where: eq(deliverySlotInfo.isActive, true), @@ -94,17 +122,18 @@ export const slotsRouter = router({ products: slot.productSlots.map((ps) => ps.product), })) ); + */ return { slots, count: slots.length, - }; + } }), // Exact replica of POST /av/products/slots/product-ids getSlotsProductIds: protectedProcedure .input(z.object({ slotIds: z.array(z.number()) })) - .query(async ({ input, ctx }) => { + .query(async ({ input, ctx }): Promise => { if (!ctx.staffUser?.id) { throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); } @@ -118,6 +147,10 @@ export const slotsRouter = router({ }); } + const result = await getSlotsProductIdsInDb(slotIds) + + /* + // Old implementation - direct DB queries: if (slotIds.length === 0) { return {}; } @@ -148,6 +181,9 @@ export const slotsRouter = router({ }); return result; + */ + + return result }), // Exact replica of PUT /av/products/slots/:slotId/products @@ -158,7 +194,7 @@ export const slotsRouter = router({ productIds: z.array(z.number()), }) ) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { if (!ctx.staffUser?.id) { throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); } @@ -172,6 +208,10 @@ export const slotsRouter = router({ }); } + const result = await updateSlotProductsInDb(String(slotId), productIds.map(String)) + + /* + // Old implementation - direct DB queries: // Get current associations const currentAssociations = await db.query.productSlots.findMany({ where: eq(productSlots.slotId, slotId), @@ -223,11 +263,20 @@ export const slotsRouter = router({ added: productsToAdd.length, removed: productsToRemove.length, }; + */ + + scheduleStoreInitialization() + + return { + message: result.message, + added: result.added, + removed: result.removed, + } }), createSlot: protectedProcedure .input(createSlotSchema) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { if (!ctx.staffUser?.id) { throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); } @@ -239,6 +288,17 @@ export const slotsRouter = router({ throw new ApiError("Delivery time and orders close time are required", 400); } + const result = await createSlotWithRelationsInDb({ + deliveryTime, + freezeTime, + isActive, + productIds, + vendorSnippets: snippets, + groupIds, + }) + + /* + // Old implementation - direct DB queries: const result = await db.transaction(async (tx) => { // Create slot const [newSlot] = await tx @@ -297,76 +357,84 @@ export const slotsRouter = router({ message: "Slot created successfully", }; }); + */ // Reinitialize stores to reflect changes (outside transaction) scheduleStoreInitialization() - return result; + return result }), - getSlots: protectedProcedure.query(async ({ ctx }) => { + getSlots: protectedProcedure.query(async ({ ctx }): Promise => { if (!ctx.staffUser?.id) { throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); } + const slots = await getActiveSlotsInDb() + + /* + // Old implementation - direct DB queries: const slots = await db.query.deliverySlotInfo.findMany({ where: eq(deliverySlotInfo.isActive, true), }); + */ return { slots, count: slots.length, - }; + } }), getSlotById: protectedProcedure .input(getSlotByIdSchema) - .query(async ({ input, ctx }) => { + .query(async ({ input, ctx }): Promise => { if (!ctx.staffUser?.id) { throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); } const { id } = input; - const slot = await db.query.deliverySlotInfo.findFirst({ - where: eq(deliverySlotInfo.id, id), - with: { - productSlots: { - with: { - product: { - columns: { - id: true, - name: true, - images: true, - }, - }, - }, - }, - vendorSnippets: true, - }, - }); + const slot = await getSlotByIdWithRelationsInDb(id) + + /* + // Old implementation - direct DB queries: + const slot = await db.query.deliverySlotInfo.findFirst({ + where: eq(deliverySlotInfo.id, id), + with: { + productSlots: { + with: { + product: { + columns: { + id: true, + name: true, + images: true, + }, + }, + }, + }, + vendorSnippets: true, + }, + }); + */ if (!slot) { - throw new ApiError("Slot not found", 404); + throw new ApiError('Slot not found', 404) } return { slot: { ...slot, - deliverySequence: slot.deliverySequence as number[], - groupIds: slot.groupIds as number[], - products: slot.productSlots.map((ps) => ps.product), - vendorSnippets: slot.vendorSnippets?.map(snippet => ({ + vendorSnippets: slot.vendorSnippets.map(snippet => ({ ...snippet, - accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}` + accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`, })), }, - }; + } }), updateSlot: protectedProcedure .input(updateSlotSchema) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { if (!ctx.staffUser?.id) { throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); } @@ -377,6 +445,18 @@ export const slotsRouter = router({ throw new ApiError("Delivery time and orders close time are required", 400); } + const result = await updateSlotWithRelationsInDb({ + id, + deliveryTime, + freezeTime, + isActive, + productIds, + vendorSnippets: snippets, + groupIds, + }) + + /* + // Old implementation - direct DB queries: // Filter groupIds to only include valid (existing) groups let validGroupIds = groupIds; if (groupIds && groupIds.length > 0) { @@ -456,11 +536,16 @@ export const slotsRouter = router({ message: "Slot updated successfully", }; }); + */ + + if (!result) { + throw new ApiError('Slot not found', 404) + } // Reinitialize stores to reflect changes (outside transaction) scheduleStoreInitialization() - return result; + return result } catch(e) { console.log(e) @@ -470,13 +555,17 @@ export const slotsRouter = router({ deleteSlot: protectedProcedure .input(deleteSlotSchema) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { if (!ctx.staffUser?.id) { throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); } const { id } = input; + const deletedSlot = await deleteSlotByIdInDb(id) + + /* + // Old implementation - direct DB queries: const [deletedSlot] = await db .update(deliverySlotInfo) .set({ isActive: false }) @@ -486,18 +575,23 @@ export const slotsRouter = router({ if (!deletedSlot) { throw new ApiError("Slot not found", 404); } + */ + + if (!deletedSlot) { + throw new ApiError('Slot not found', 404) + } // Reinitialize stores to reflect changes scheduleStoreInitialization() return { - message: "Slot deleted successfully", - }; + message: 'Slot deleted successfully', + } }), getDeliverySequence: protectedProcedure .input(getDeliverySequenceSchema) - .query(async ({ input, ctx }) => { + .query(async ({ input, ctx }): Promise => { const { id } = input; const slotId = parseInt(id); @@ -507,7 +601,7 @@ export const slotsRouter = router({ const cached = await redisClient.get(cacheKey); if (cached) { const parsed = JSON.parse(cached); - const validated = cachedSequenceSchema.parse(parsed) as CachedDeliverySequence; + const validated = cachedSequenceSchema.parse(parsed); console.log('sending cached response') return { deliverySequence: validated }; @@ -518,6 +612,10 @@ export const slotsRouter = router({ } // Fallback to DB + const slot = await getSlotDeliverySequenceInDb(slotId) + + /* + // Old implementation - direct DB queries: const slot = await db.query.deliverySlotInfo.findFirst({ where: eq(deliverySlotInfo.id, slotId), }); @@ -526,6 +624,13 @@ export const slotsRouter = router({ throw new ApiError("Slot not found", 404); } + const sequence = cachedSequenceSchema.parse(slot.deliverySequence || {}); + */ + + if (!slot) { + throw new ApiError('Slot not found', 404) + } + const sequence = (slot.deliverySequence || {}) as CachedDeliverySequence; // Cache the validated result @@ -536,18 +641,22 @@ export const slotsRouter = router({ console.warn('Redis cache write failed:', cacheError); } - return { deliverySequence: sequence }; + return { deliverySequence: sequence } }), updateDeliverySequence: protectedProcedure .input(updateDeliverySequenceSchema) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { if (!ctx.staffUser?.id) { throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); } const { id, deliverySequence } = input; + const updatedSlot = await updateSlotDeliverySequenceInDb(id, deliverySequence) + + /* + // Old implementation - direct DB queries: const [updatedSlot] = await db .update(deliverySlotInfo) .set({ deliverySequence }) @@ -560,6 +669,11 @@ export const slotsRouter = router({ if (!updatedSlot) { throw new ApiError("Slot not found", 404); } + */ + + if (!updatedSlot) { + throw new ApiError('Slot not found', 404) + } // Cache the updated sequence const cacheKey = getSlotSequenceKey(id); @@ -572,8 +686,8 @@ export const slotsRouter = router({ return { slot: updatedSlot, - message: "Delivery sequence updated successfully", - }; + message: 'Delivery sequence updated successfully', + } }), updateSlotCapacity: protectedProcedure @@ -581,13 +695,17 @@ export const slotsRouter = router({ slotId: z.number(), isCapacityFull: z.boolean(), })) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { if (!ctx.staffUser?.id) { throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); } const { slotId, isCapacityFull } = input; + const result = await updateSlotCapacityInDb(slotId, isCapacityFull) + + /* + // Old implementation - direct DB queries: const [updatedSlot] = await db .update(deliverySlotInfo) .set({ isCapacityFull }) @@ -606,5 +724,14 @@ export const slotsRouter = router({ slot: updatedSlot, message: `Slot ${isCapacityFull ? 'marked as full capacity' : 'capacity reset'}`, }; + */ + + if (!result) { + throw new ApiError('Slot not found', 404) + } + + scheduleStoreInitialization() + + return result }), }); diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/vendor-snippets.ts b/apps/backend/src/trpc/apis/admin-apis/apis/vendor-snippets.ts index 4a307d3..9d8a80b 100644 --- a/apps/backend/src/trpc/apis/admin-apis/apis/vendor-snippets.ts +++ b/apps/backend/src/trpc/apis/admin-apis/apis/vendor-snippets.ts @@ -1,10 +1,33 @@ import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index' -import { z } from 'zod'; -import dayjs from 'dayjs'; -import { db } from '@/src/db/db_index' -import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, users, orderStatus } from '@/src/db/schema' -import { eq, and, inArray, isNotNull, gt, sql, asc, ne } from 'drizzle-orm'; +import { z } from 'zod' +import dayjs from 'dayjs' import { appUrl } from '@/src/lib/env-exporter' +import { + checkVendorSnippetExists as checkVendorSnippetExistsInDb, + getVendorSnippetById as getVendorSnippetByIdInDb, + getVendorSnippetByCode as getVendorSnippetByCodeInDb, + getAllVendorSnippets as getAllVendorSnippetsInDb, + createVendorSnippet as createVendorSnippetInDb, + updateVendorSnippet as updateVendorSnippetInDb, + deleteVendorSnippet as deleteVendorSnippetInDb, + getProductsByIds as getProductsByIdsInDb, + getVendorSlotById as getVendorSlotByIdInDb, + getVendorOrdersBySlotId as getVendorOrdersBySlotIdInDb, + getVendorOrders as getVendorOrdersInDb, + updateVendorOrderItemPackaging as updateVendorOrderItemPackagingInDb, + getSlotsAfterDate as getSlotsAfterDateInDb, +} from '@/src/dbService' +import type { + AdminVendorSnippet, + AdminVendorSnippetWithProducts, + AdminVendorSnippetWithSlot, + AdminVendorSnippetDeleteResult, + AdminVendorSnippetOrdersResult, + AdminVendorSnippetOrdersWithSlotResult, + AdminVendorOrderSummary, + AdminUpcomingSlotsResult, + AdminVendorUpdatePackagingResult, +} from '@packages/shared' const createSnippetSchema = z.object({ snippetCode: z.string().min(1, "Snippet code is required"), @@ -26,7 +49,7 @@ const updateSnippetSchema = z.object({ export const vendorSnippetsRouter = router({ create: protectedProcedure .input(createSnippetSchema) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { const { snippetCode, slotId, productIds, validTill, isPermanent } = input; // Get staff user ID from auth middleware @@ -35,6 +58,33 @@ export const vendorSnippetsRouter = router({ throw new Error("Unauthorized"); } + if(slotId) { + const slot = await getVendorSlotByIdInDb(slotId) + if (!slot) { + throw new Error("Invalid slot ID") + } + } + + const products = await getProductsByIdsInDb(productIds) + if (products.length !== productIds.length) { + throw new Error("One or more invalid product IDs") + } + + const existingSnippet = await checkVendorSnippetExistsInDb(snippetCode) + if (existingSnippet) { + throw new Error("Snippet code already exists") + } + + const result = await createVendorSnippetInDb({ + snippetCode, + slotId, + productIds, + isPermanent, + validTill: validTill ? new Date(validTill) : undefined, + }) + + /* + // Old implementation - direct DB queries: // Validate slot exists if(slotId) { const slot = await db.query.deliverySlotInfo.findFirst({ @@ -70,13 +120,32 @@ export const vendorSnippetsRouter = router({ }).returning(); return result[0]; + */ + + return result }), getAll: protectedProcedure - .query(async () => { + .query(async (): Promise => { console.log('from the vendor snipptes methods') try { + const result = await getAllVendorSnippetsInDb() + + const snippetsWithProducts = await Promise.all( + result.map(async (snippet) => { + const products = await getProductsByIdsInDb(snippet.productIds) + + return { + ...snippet, + accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`, + products, + } + }) + ) + + /* + // Old implementation - direct DB queries: const result = await db.query.vendorSnippets.findMany({ with: { slot: true, @@ -100,18 +169,25 @@ export const vendorSnippetsRouter = router({ ); return snippetsWithProducts; + */ + + return snippetsWithProducts } catch(e) { console.log(e) } - return []; + return [] }), getById: protectedProcedure .input(z.object({ id: z.number().int().positive() })) - .query(async ({ input }) => { + .query(async ({ input }): Promise => { const { id } = input; + const result = await getVendorSnippetByIdInDb(id) + + /* + // Old implementation - direct DB queries: const result = await db.query.vendorSnippets.findFirst({ where: eq(vendorSnippets.id, id), with: { @@ -124,14 +200,57 @@ export const vendorSnippetsRouter = router({ } return result; + */ + + if (!result) { + throw new Error('Vendor snippet not found') + } + + return result }), update: protectedProcedure .input(updateSnippetSchema) - .mutation(async ({ input }) => { + .mutation(async ({ input }): Promise => { const { id, updates } = input; - // Check if snippet exists + const existingSnippet = await getVendorSnippetByIdInDb(id) + if (!existingSnippet) { + throw new Error('Vendor snippet not found') + } + + if (updates.slotId) { + const slot = await getVendorSlotByIdInDb(updates.slotId) + if (!slot) { + throw new Error('Invalid slot ID') + } + } + + if (updates.productIds) { + const products = await getProductsByIdsInDb(updates.productIds) + if (products.length !== updates.productIds.length) { + throw new Error('One or more invalid product IDs') + } + } + + if (updates.snippetCode && updates.snippetCode !== existingSnippet.snippetCode) { + const duplicateSnippet = await checkVendorSnippetExistsInDb(updates.snippetCode) + if (duplicateSnippet) { + throw new Error('Snippet code already exists') + } + } + + const updateData = { + ...updates, + validTill: updates.validTill !== undefined + ? (updates.validTill ? new Date(updates.validTill) : null) + : undefined, + } + + const result = await updateVendorSnippetInDb(id, updateData) + + /* + // Old implementation - direct DB queries: const existingSnippet = await db.query.vendorSnippets.findFirst({ where: eq(vendorSnippets.id, id), }); @@ -184,13 +303,24 @@ export const vendorSnippetsRouter = router({ } return result[0]; + */ + + if (!result) { + throw new Error('Failed to update vendor snippet') + } + + return result }), delete: protectedProcedure .input(z.object({ id: z.number().int().positive() })) - .mutation(async ({ input }) => { + .mutation(async ({ input }): Promise => { const { id } = input; + const result = await deleteVendorSnippetInDb(id) + + /* + // Old implementation - direct DB queries: const result = await db.delete(vendorSnippets) .where(eq(vendorSnippets.id, id)) .returning(); @@ -200,15 +330,26 @@ export const vendorSnippetsRouter = router({ } return { message: "Vendor snippet deleted successfully" }; + */ + + if (!result) { + throw new Error('Vendor snippet not found') + } + + return { message: 'Vendor snippet deleted successfully' } }), getOrdersBySnippet: publicProcedure .input(z.object({ snippetCode: z.string().min(1, "Snippet code is required") })) - .query(async ({ input }) => { + .query(async ({ input }): Promise => { const { snippetCode } = input; + const snippet = await getVendorSnippetByCodeInDb(snippetCode) + + /* + // Old implementation - direct DB queries: // Find the snippet const snippet = await db.query.vendorSnippets.findFirst({ where: eq(vendorSnippets.snippetCode, snippetCode), @@ -242,6 +383,21 @@ export const vendorSnippetsRouter = router({ }, orderBy: (orders, { desc }) => [desc(orders.createdAt)], }); + */ + + if (!snippet) { + throw new Error('Vendor snippet not found') + } + + if (snippet.validTill && new Date(snippet.validTill) < new Date()) { + throw new Error('Vendor snippet has expired') + } + + if (!snippet.slotId) { + throw new Error('Vendor snippet not associated with a slot') + } + + const matchingOrders = await getVendorOrdersBySlotIdInDb(snippet.slotId) // Filter orders that contain at least one of the snippet's products const filteredOrders = matchingOrders.filter(order => { @@ -273,11 +429,11 @@ export const vendorSnippetsRouter = router({ const orderTotal = products.reduce((sum, p) => sum + p.subtotal, 0); - return { - orderId: `ORD${order.id}`, - orderDate: order.createdAt.toISOString(), - customerName: order.user.name, - totalAmount: orderTotal, + return { + orderId: `ORD${order.id}`, + orderDate: order.createdAt.toISOString(), + customerName: order.user.name || '', + totalAmount: orderTotal, slotInfo: order.slot ? { time: order.slot.deliveryTime.toISOString(), sequence: order.slot.deliverySequence, @@ -300,11 +456,15 @@ export const vendorSnippetsRouter = router({ createdAt: snippet.createdAt.toISOString(), isPermanent: snippet.isPermanent, }, - }; + } }), getVendorOrders: protectedProcedure - .query(async () => { + .query(async (): Promise => { + const vendorOrders = await getVendorOrdersInDb() + + /* + // Old implementation - direct DB queries: const vendorOrders = await db.query.orders.findMany({ with: { user: true, @@ -320,10 +480,11 @@ export const vendorSnippetsRouter = router({ }, orderBy: (orders, { desc }) => [desc(orders.createdAt)], }); + */ return vendorOrders.map(order => ({ id: order.id, - status: 'pending', // Default status since orders table may not have status field + status: 'pending', orderDate: order.createdAt.toISOString(), totalQuantity: order.orderItems.reduce((sum, item) => sum + parseFloat(item.quantity || '0'), 0), products: order.orderItems.map(item => ({ @@ -331,12 +492,16 @@ export const vendorSnippetsRouter = router({ quantity: parseFloat(item.quantity || '0'), unit: item.product.unit?.shortNotation || 'unit', })), - })); + })) }), getUpcomingSlots: publicProcedure - .query(async () => { + .query(async (): Promise => { const threeHoursAgo = dayjs().subtract(3, 'hour').toDate(); + const slots = await getSlotsAfterDateInDb(threeHoursAgo) + + /* + // Old implementation - direct DB queries: const slots = await db.query.deliverySlotInfo.findMany({ where: and( eq(deliverySlotInfo.isActive, true), @@ -344,6 +509,7 @@ export const vendorSnippetsRouter = router({ ), orderBy: asc(deliverySlotInfo.deliveryTime), }); + */ return { success: true, @@ -353,7 +519,7 @@ export const vendorSnippetsRouter = router({ freezeTime: slot.freezeTime.toISOString(), deliverySequence: slot.deliverySequence, })), - }; + } }), getOrdersBySnippetAndSlot: publicProcedure @@ -361,9 +527,14 @@ export const vendorSnippetsRouter = router({ snippetCode: z.string().min(1, "Snippet code is required"), slotId: z.number().int().positive("Valid slot ID is required"), })) - .query(async ({ input }) => { + .query(async ({ input }): Promise => { const { snippetCode, slotId } = input; + const snippet = await getVendorSnippetByCodeInDb(snippetCode) + const slot = await getVendorSlotByIdInDb(slotId) + + /* + // Old implementation - direct DB queries: // Find the snippet const snippet = await db.query.vendorSnippets.findFirst({ where: eq(vendorSnippets.snippetCode, snippetCode), @@ -401,6 +572,17 @@ export const vendorSnippetsRouter = router({ }, orderBy: (orders, { desc }) => [desc(orders.createdAt)], }); + */ + + if (!snippet) { + throw new Error('Vendor snippet not found') + } + + if (!slot) { + throw new Error('Slot not found') + } + + const matchingOrders = await getVendorOrdersBySlotIdInDb(slotId) // Filter orders that contain at least one of the snippet's products const filteredOrders = matchingOrders.filter(order => { @@ -435,7 +617,7 @@ export const vendorSnippetsRouter = router({ return { orderId: `ORD${order.id}`, orderDate: order.createdAt.toISOString(), - customerName: order.user.name, + customerName: order.user.name || '', totalAmount: orderTotal, slotInfo: order.slot ? { time: order.slot.deliveryTime.toISOString(), @@ -465,7 +647,7 @@ export const vendorSnippetsRouter = router({ freezeTime: slot.freezeTime.toISOString(), deliverySequence: slot.deliverySequence, }, - }; + } }), updateOrderItemPackaging: publicProcedure @@ -473,7 +655,7 @@ export const vendorSnippetsRouter = router({ orderItemId: z.number().int().positive("Valid order item ID required"), is_packaged: z.boolean() })) - .mutation(async ({ input, ctx }) => { + .mutation(async ({ input, ctx }): Promise => { const { orderItemId, is_packaged } = input; // Get staff user ID from auth middleware @@ -482,6 +664,10 @@ export const vendorSnippetsRouter = router({ // throw new Error("Unauthorized"); // } + const result = await updateVendorOrderItemPackagingInDb(orderItemId, is_packaged) + + /* + // Old implementation - direct DB queries: // Check if order item exists and get related data const orderItem = await db.query.orderItems.findFirst({ where: eq(orderItems.id, orderItemId), @@ -527,5 +713,12 @@ export const vendorSnippetsRouter = router({ orderItemId, is_packaged }; + */ + + if (!result.success) { + throw new Error(result.message) + } + + return result }), -}); \ No newline at end of file +}); diff --git a/packages/db_helper_postgres/index.ts b/packages/db_helper_postgres/index.ts index 18be345..a17b5e3 100644 --- a/packages/db_helper_postgres/index.ts +++ b/packages/db_helper_postgres/index.ts @@ -68,9 +68,13 @@ export { // Product getAllProducts, getProductById, + deleteProduct, createProduct, updateProduct, toggleProductOutOfStock, + updateSlotProducts, + getSlotProductIds, + getSlotsProductIds, getAllUnits, getAllProductTags, getProductReviews, @@ -81,19 +85,18 @@ export { deleteProductGroup, addProductToGroup, removeProductFromGroup, + updateProductPrices, } from './src/admin-apis/product'; export { // Slots - getAllSlots, - getSlotById, - createSlot, - updateSlot, - deleteSlot, - getSlotProducts, - addProductToSlot, - removeProductFromSlot, - clearSlotProducts, + getActiveSlotsWithProducts, + getActiveSlots, + getSlotsAfterDate, + getSlotByIdWithRelations, + createSlotWithRelations, + updateSlotWithRelations, + deleteSlotById, updateSlotCapacity, getSlotDeliverySequence, updateSlotDeliverySequence, @@ -159,6 +162,7 @@ export { getOrderItemsByOrderIds, getOrderStatusByOrderIds, updateVendorOrderItemPackaging, + getVendorOrders, } from './src/admin-apis/vendor-snippets'; // Note: User API helpers are available in their respective files diff --git a/packages/db_helper_postgres/src/admin-apis/order.ts b/packages/db_helper_postgres/src/admin-apis/order.ts index 8cc8cf0..9b3c315 100644 --- a/packages/db_helper_postgres/src/admin-apis/order.ts +++ b/packages/db_helper_postgres/src/admin-apis/order.ts @@ -26,6 +26,33 @@ import type { RefundStatus, PaymentStatus, } from '@packages/shared' +import type { InferSelectModel } from 'drizzle-orm' + +const isPaymentStatus = (value: string): value is PaymentStatus => + value === 'pending' || value === 'success' || value === 'cod' || value === 'failed' + +const isRefundStatus = (value: string): value is RefundStatus => + value === 'success' || value === 'pending' || value === 'failed' || value === 'none' || value === 'na' || value === 'processed' + +type OrderStatusRow = InferSelectModel + +const mapOrderStatusRecord = (record: OrderStatusRow): AdminOrderStatusRecord => ({ + id: record.id, + orderTime: record.orderTime, + userId: record.userId, + orderId: record.orderId, + isPackaged: record.isPackaged, + isDelivered: record.isDelivered, + isCancelled: record.isCancelled, + cancelReason: record.cancelReason ?? null, + isCancelledByAdmin: record.isCancelledByAdmin ?? null, + paymentStatus: isPaymentStatus(record.paymentStatus) ? record.paymentStatus : 'pending', + cancellationUserNotes: record.cancellationUserNotes ?? null, + cancellationAdminNotes: record.cancellationAdminNotes ?? null, + cancellationReviewed: record.cancellationReviewed, + cancellationReviewedAt: record.cancellationReviewedAt ?? null, + refundCouponId: record.refundCouponId ?? null, +}) export async function updateOrderNotes(orderId: number, adminNotes: string | null): Promise { const [result] = await db @@ -147,9 +174,7 @@ export async function getOrderDetails(orderId: number): Promise item.is_packaged) || false, isDelivered: statusRecord?.isDelivered || false, isCod: order.isCod, - paymentMode: (order.isCod ? 'COD' : 'Online') as 'COD' | 'Online', - paymentStatus: (statusRecord?.paymentStatus || 'pending') as PaymentStatus, + paymentMode, + paymentStatus: isPaymentStatus(statusRecord?.paymentStatus || 'pending') + ? statusRecord?.paymentStatus || 'pending' + : 'pending', slotId: order.slotId, adminNotes: order.adminNotes, userNotes: order.userNotes, diff --git a/packages/db_helper_postgres/src/admin-apis/product.ts b/packages/db_helper_postgres/src/admin-apis/product.ts index 13c4b51..28f305d 100644 --- a/packages/db_helper_postgres/src/admin-apis/product.ts +++ b/packages/db_helper_postgres/src/admin-apis/product.ts @@ -1,67 +1,265 @@ -import { db } from '../db/db_index'; -import { productInfo, units, specialDeals, productSlots, productTags, productReviews, productGroupInfo, productGroupMembership } from '../db/schema'; -import { eq, and, inArray, desc, sql, asc } from 'drizzle-orm'; +import { db } from '../db/db_index' +import { + productInfo, + units, + specialDeals, + productSlots, + 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, + AdminProductTagWithProducts, + AdminProductReview, + AdminProductWithDetails, + AdminProductWithRelations, + AdminSpecialDeal, + AdminUnit, + AdminUpdateSlotProductsResult, + Store, +} from '@packages/shared' -export async function getAllProducts(): Promise { - return await db.query.productInfo.findMany({ +type ProductRow = InferSelectModel +type UnitRow = InferSelectModel +type StoreRow = InferSelectModel +type SpecialDealRow = InferSelectModel +type ProductTagInfoRow = InferSelectModel +type ProductTagRow = InferSelectModel +type ProductGroupRow = InferSelectModel +type ProductGroupMembershipRow = InferSelectModel +type ProductReviewRow = 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), + 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) => ({ + 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 { - return await db.query.productInfo.findFirst({ +export async function getProductById(id: number): Promise { + const product = await db.query.productInfo.findFirst({ where: eq(productInfo.id, id), with: { unit: true, - store: true, - productSlots: { - with: { - slot: true, - }, - }, - specialDeals: true, - productTags: { - with: { - tag: 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 createProduct(input: any): Promise { - const [product] = await db.insert(productInfo).values(input).returning(); - return product; +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) } -export async function updateProduct(id: number, updates: any): Promise { +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(); - return product; + .returning() + if (!product) { + return null + } + + return mapProduct(product) } -export async function toggleProductOutOfStock(id: number, isOutOfStock: boolean): Promise { - const [product] = await db.update(productInfo) - .set({ isOutOfStock }) +export async function toggleProductOutOfStock(id: number): Promise { + const product = await db.query.productInfo.findFirst({ + where: eq(productInfo.id, id), + }) + + if (!product) { + return null + } + + const [updatedProduct] = await db + .update(productInfo) + .set({ + isOutOfStock: !product.isOutOfStock, + }) .where(eq(productInfo.id, id)) - .returning(); - return product; + .returning() + + if (!updatedProduct) { + return null + } + + return mapProduct(updatedProduct) } -export async function getAllUnits(): Promise { - return await db.query.units.findMany({ - orderBy: units.name, - }); +export async function updateSlotProducts(slotId: string, productIds: string[]): Promise { + const currentAssociations = await db.query.productSlots.findMany({ + where: eq(productSlots.slotId, parseInt(slotId)), + columns: { + productId: true, + }, + }) + + const currentProductIds = currentAssociations.map((assoc) => assoc.productId) + const newProductIds = productIds.map((id) => parseInt(id)) + + const productsToAdd = newProductIds.filter((id) => !currentProductIds.includes(id)) + const productsToRemove = currentProductIds.filter((id) => !newProductIds.includes(id)) + + if (productsToRemove.length > 0) { + await db.delete(productSlots).where( + and( + eq(productSlots.slotId, parseInt(slotId)), + inArray(productSlots.productId, productsToRemove) + ) + ) + } + + if (productsToAdd.length > 0) { + const newAssociations = productsToAdd.map((productId) => ({ + productId, + slotId: parseInt(slotId), + })) + + await db.insert(productSlots).values(newAssociations) + } + + return { + message: 'Slot products updated successfully', + added: productsToAdd.length, + removed: productsToRemove.length, + } } -export async function getAllProductTags(): Promise { - return await db.query.productTags.findMany({ +export async function getSlotProductIds(slotId: string): Promise { + const associations = await db.query.productSlots.findMany({ + where: eq(productSlots.slotId, parseInt(slotId)), + columns: { + productId: true, + }, + }) + + return associations.map((assoc) => assoc.productId) +} + +export async function getAllUnits(): Promise { + const allUnits = await db.query.units.findMany({ + orderBy: units.shortNotation, + }) + + return allUnits.map(mapUnit) +} + +export async function getAllProductTags(): Promise { + const tags = await db.query.productTagInfo.findMany({ with: { products: { with: { @@ -69,56 +267,242 @@ export async function getAllProductTags(): Promise { }, }, }, - }); + }) + + 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 getProductReviews(productId: number): Promise { - return await db.query.productReviews.findMany({ - where: eq(productReviews.productId, productId), - with: { - user: true, +export async function getSlotsProductIds(slotIds: number[]): Promise> { + if (slotIds.length === 0) { + return {} + } + + const associations = await db.query.productSlots.findMany({ + where: inArray(productSlots.slotId, slotIds), + columns: { + slotId: true, + productId: true, }, - orderBy: desc(productReviews.createdAt), - }); + }) + + const result: Record = {} + for (const assoc of associations) { + if (!result[assoc.slotId]) { + result[assoc.slotId] = [] + } + result[assoc.slotId].push(assoc.productId) + } + + slotIds.forEach((slotId) => { + if (!result[slotId]) { + result[slotId] = [] + } + }) + + return result } -export async function respondToReview(reviewId: number, adminResponse: string): Promise { - await db.update(productReviews) - .set({ adminResponse }) - .where(eq(productReviews.id, reviewId)); +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 getAllProductGroups(): Promise { - return await db.query.productGroupInfo.findMany({ +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: { - products: { + 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(name: string): Promise { - const [group] = await db.insert(productGroupInfo).values({ name }).returning(); - return group; +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, name: string): Promise { - const [group] = await db.update(productGroupInfo) - .set({ name }) +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(); - return group; + .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(productGroupInfo).where(eq(productGroupInfo.id, id)); +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 addProductToGroup(groupId: number, productId: number): Promise { - await db.insert(productGroupMembership).values({ groupId, productId }); + await db.insert(productGroupMembership).values({ groupId, productId }) } export async function removeProductFromGroup(groupId: number, productId: number): Promise { @@ -126,5 +510,49 @@ export async function removeProductFromGroup(groupId: number, productId: number) .where(and( eq(productGroupMembership.groupId, groupId), eq(productGroupMembership.productId, productId) - )); + )) +} + +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: [] } } diff --git a/packages/db_helper_postgres/src/admin-apis/slots.ts b/packages/db_helper_postgres/src/admin-apis/slots.ts index d6a70cd..a218257 100644 --- a/packages/db_helper_postgres/src/admin-apis/slots.ts +++ b/packages/db_helper_postgres/src/admin-apis/slots.ts @@ -1,95 +1,351 @@ -import { db } from '../db/db_index'; -import { deliverySlotInfo, productSlots, productInfo } from '../db/schema'; -import { eq, and, inArray, desc } from 'drizzle-orm'; +import { db } from '../db/db_index' +import { + deliverySlotInfo, + productSlots, + productInfo, + vendorSnippets, + productGroupInfo, +} from '../db/schema' +import { and, asc, desc, eq, gt, inArray } from 'drizzle-orm' +import type { + AdminDeliverySlot, + AdminSlotWithProducts, + AdminSlotWithProductsAndSnippetsBase, + AdminSlotCreateResult, + AdminSlotUpdateResult, + AdminVendorSnippet, + AdminSlotProductSummary, + AdminUpdateSlotCapacityResult, +} from '@packages/shared' -export async function getAllSlots(): Promise { - return await db.query.deliverySlotInfo.findMany({ - orderBy: desc(deliverySlotInfo.createdAt), - with: { - productSlots: { - with: { - product: true, - }, - }, - }, - }); +type SlotSnippetInput = { + name: string + productIds: number[] + validTill?: string } -export async function getSlotById(id: number): Promise { - return await db.query.deliverySlotInfo.findFirst({ +const getStringArray = (value: unknown): string[] | null => { + if (!Array.isArray(value)) return null + return value.map((item) => String(item)) +} + +const getNumberArray = (value: unknown): number[] => { + if (!Array.isArray(value)) return [] + return value.map((item) => Number(item)) +} + +const mapDeliverySlot = (slot: typeof deliverySlotInfo.$inferSelect): AdminDeliverySlot => ({ + id: slot.id, + deliveryTime: slot.deliveryTime, + freezeTime: slot.freezeTime, + isActive: slot.isActive, + isFlash: slot.isFlash, + isCapacityFull: slot.isCapacityFull, + deliverySequence: slot.deliverySequence, + groupIds: slot.groupIds, +}) + +const mapSlotProductSummary = (product: { id: number; name: string; images: unknown }): AdminSlotProductSummary => ({ + id: product.id, + name: product.name, + images: getStringArray(product.images), +}) + +const mapVendorSnippet = (snippet: typeof vendorSnippets.$inferSelect): AdminVendorSnippet => ({ + id: snippet.id, + snippetCode: snippet.snippetCode, + slotId: snippet.slotId ?? null, + productIds: snippet.productIds, + isPermanent: snippet.isPermanent, + validTill: snippet.validTill ?? null, + createdAt: snippet.createdAt, +}) + +export async function getActiveSlotsWithProducts(): Promise { + const slots = await db.query.deliverySlotInfo + .findMany({ + where: eq(deliverySlotInfo.isActive, true), + orderBy: desc(deliverySlotInfo.deliveryTime), + with: { + productSlots: { + with: { + product: { + columns: { + id: true, + name: true, + images: true, + }, + }, + }, + }, + }, + }) + + return slots.map((slot) => ({ + ...mapDeliverySlot(slot), + deliverySequence: getNumberArray(slot.deliverySequence), + products: slot.productSlots.map((ps) => mapSlotProductSummary(ps.product)), + })) +} + +export async function getActiveSlots(): Promise { + const slots = await db.query.deliverySlotInfo.findMany({ + where: eq(deliverySlotInfo.isActive, true), + }) + + return slots.map(mapDeliverySlot) +} + +export async function getSlotsAfterDate(afterDate: Date): Promise { + const slots = await db.query.deliverySlotInfo.findMany({ + where: and( + eq(deliverySlotInfo.isActive, true), + gt(deliverySlotInfo.deliveryTime, afterDate) + ), + orderBy: asc(deliverySlotInfo.deliveryTime), + }) + + return slots.map(mapDeliverySlot) +} + +export async function getSlotByIdWithRelations(id: number): Promise { + const slot = await db.query.deliverySlotInfo.findFirst({ where: eq(deliverySlotInfo.id, id), with: { productSlots: { with: { - product: true, + product: { + columns: { + id: true, + name: true, + images: true, + }, + }, }, }, + vendorSnippets: true, }, - }); + }) + + if (!slot) { + return null + } + + return { + ...mapDeliverySlot(slot), + deliverySequence: getNumberArray(slot.deliverySequence), + groupIds: getNumberArray(slot.groupIds), + products: slot.productSlots.map((ps) => mapSlotProductSummary(ps.product)), + vendorSnippets: slot.vendorSnippets.map(mapVendorSnippet), + } } -export async function createSlot(input: any): Promise { - const [slot] = await db.insert(deliverySlotInfo).values(input).returning(); - return slot; +export async function createSlotWithRelations(input: { + deliveryTime: string + freezeTime: string + isActive?: boolean + productIds?: number[] + vendorSnippets?: SlotSnippetInput[] + groupIds?: number[] +}): Promise { + const { deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input + + const result = await db.transaction(async (tx) => { + const [newSlot] = await tx + .insert(deliverySlotInfo) + .values({ + deliveryTime: new Date(deliveryTime), + freezeTime: new Date(freezeTime), + isActive: isActive !== undefined ? isActive : true, + groupIds: groupIds !== undefined ? groupIds : [], + }) + .returning() + + if (productIds && productIds.length > 0) { + const associations = productIds.map((productId) => ({ + productId, + slotId: newSlot.id, + })) + await tx.insert(productSlots).values(associations) + } + + let createdSnippets: AdminVendorSnippet[] = [] + if (snippets && snippets.length > 0) { + for (const snippet of snippets) { + const products = await tx.query.productInfo.findMany({ + where: inArray(productInfo.id, snippet.productIds), + }) + if (products.length !== snippet.productIds.length) { + throw new Error(`One or more invalid product IDs in snippet "${snippet.name}"`) + } + + const existingSnippet = await tx.query.vendorSnippets.findFirst({ + where: eq(vendorSnippets.snippetCode, snippet.name), + }) + if (existingSnippet) { + throw new Error(`Snippet name "${snippet.name}" already exists`) + } + + const [createdSnippet] = await tx.insert(vendorSnippets).values({ + snippetCode: snippet.name, + slotId: newSlot.id, + productIds: snippet.productIds, + validTill: snippet.validTill ? new Date(snippet.validTill) : undefined, + }).returning() + + createdSnippets.push(mapVendorSnippet(createdSnippet)) + } + } + + return { + slot: mapDeliverySlot(newSlot), + createdSnippets, + message: 'Slot created successfully', + } + }) + + return result } -export async function updateSlot(id: number, updates: any): Promise { - const [slot] = await db.update(deliverySlotInfo) - .set(updates) +export async function updateSlotWithRelations(input: { + id: number + deliveryTime: string + freezeTime: string + isActive?: boolean + productIds?: number[] + vendorSnippets?: SlotSnippetInput[] + groupIds?: number[] +}): Promise { + const { id, deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input + + let validGroupIds = groupIds + if (groupIds && groupIds.length > 0) { + const existingGroups = await db.query.productGroupInfo.findMany({ + where: inArray(productGroupInfo.id, groupIds), + columns: { id: true }, + }) + validGroupIds = existingGroups.map((group) => group.id) + } + + const result = await db.transaction(async (tx) => { + const [updatedSlot] = await tx + .update(deliverySlotInfo) + .set({ + deliveryTime: new Date(deliveryTime), + freezeTime: new Date(freezeTime), + isActive: isActive !== undefined ? isActive : true, + groupIds: validGroupIds !== undefined ? validGroupIds : [], + }) + .where(eq(deliverySlotInfo.id, id)) + .returning() + + if (!updatedSlot) { + return null + } + + if (productIds !== undefined) { + await tx.delete(productSlots).where(eq(productSlots.slotId, id)) + + if (productIds.length > 0) { + const associations = productIds.map((productId) => ({ + productId, + slotId: id, + })) + await tx.insert(productSlots).values(associations) + } + } + + let createdSnippets: AdminVendorSnippet[] = [] + if (snippets && snippets.length > 0) { + for (const snippet of snippets) { + const products = await tx.query.productInfo.findMany({ + where: inArray(productInfo.id, snippet.productIds), + }) + if (products.length !== snippet.productIds.length) { + throw new Error(`One or more invalid product IDs in snippet "${snippet.name}"`) + } + + const existingSnippet = await tx.query.vendorSnippets.findFirst({ + where: eq(vendorSnippets.snippetCode, snippet.name), + }) + if (existingSnippet) { + throw new Error(`Snippet name "${snippet.name}" already exists`) + } + + const [createdSnippet] = await tx.insert(vendorSnippets).values({ + snippetCode: snippet.name, + slotId: id, + productIds: snippet.productIds, + validTill: snippet.validTill ? new Date(snippet.validTill) : undefined, + }).returning() + + createdSnippets.push(mapVendorSnippet(createdSnippet)) + } + } + + return { + slot: mapDeliverySlot(updatedSlot), + createdSnippets, + message: 'Slot updated successfully', + } + }) + + return result +} + +export async function deleteSlotById(id: number): Promise { + const [deletedSlot] = await db + .update(deliverySlotInfo) + .set({ isActive: false }) .where(eq(deliverySlotInfo.id, id)) - .returning(); - return slot; + .returning() + + if (!deletedSlot) { + return null + } + + return mapDeliverySlot(deletedSlot) } -export async function deleteSlot(id: number): Promise { - await db.delete(deliverySlotInfo).where(eq(deliverySlotInfo.id, id)); -} - -export async function getSlotProducts(slotId: number): Promise { - return await db.query.productSlots.findMany({ - where: eq(productSlots.slotId, slotId), - with: { - product: true, - }, - }); -} - -export async function addProductToSlot(slotId: number, productId: number): Promise { - await db.insert(productSlots).values({ slotId, productId }); -} - -export async function removeProductFromSlot(slotId: number, productId: number): Promise { - await db.delete(productSlots) - .where(and( - eq(productSlots.slotId, slotId), - eq(productSlots.productId, productId) - )); -} - -export async function clearSlotProducts(slotId: number): Promise { - await db.delete(productSlots).where(eq(productSlots.slotId, slotId)); -} - -export async function updateSlotCapacity(slotId: number, maxCapacity: number): Promise { - const [slot] = await db.update(deliverySlotInfo) - .set({ maxCapacity }) - .where(eq(deliverySlotInfo.id, slotId)) - .returning(); - return slot; -} - -export async function getSlotDeliverySequence(slotId: number): Promise { +export async function getSlotDeliverySequence(slotId: number): Promise { const slot = await db.query.deliverySlotInfo.findFirst({ where: eq(deliverySlotInfo.id, slotId), - columns: { - deliverySequence: true, - }, - }); - return slot?.deliverySequence || null; + }) + + if (!slot) { + return null + } + + return mapDeliverySlot(slot) } -export async function updateSlotDeliverySequence(slotId: number, sequence: any): Promise { - await db.update(deliverySlotInfo) +export async function updateSlotDeliverySequence(slotId: number, sequence: unknown) { + const [updatedSlot] = await db + .update(deliverySlotInfo) .set({ deliverySequence: sequence }) - .where(eq(deliverySlotInfo.id, slotId)); + .where(eq(deliverySlotInfo.id, slotId)) + .returning({ + id: deliverySlotInfo.id, + deliverySequence: deliverySlotInfo.deliverySequence, + }) + + return updatedSlot || null +} + +export async function updateSlotCapacity(slotId: number, isCapacityFull: boolean): Promise { + const [updatedSlot] = await db + .update(deliverySlotInfo) + .set({ isCapacityFull }) + .where(eq(deliverySlotInfo.id, slotId)) + .returning() + + if (!updatedSlot) { + return null + } + + return { + success: true, + slot: mapDeliverySlot(updatedSlot), + message: `Slot ${isCapacityFull ? 'marked as full capacity' : 'capacity reset'}`, + } } diff --git a/packages/db_helper_postgres/src/admin-apis/staff-user.ts b/packages/db_helper_postgres/src/admin-apis/staff-user.ts index 2b51b9f..dabef01 100644 --- a/packages/db_helper_postgres/src/admin-apis/staff-user.ts +++ b/packages/db_helper_postgres/src/admin-apis/staff-user.ts @@ -6,7 +6,7 @@ export interface StaffUser { id: number; name: string; password: string; - staffRoleId: number; + staffRoleId: number | null; createdAt: Date; } diff --git a/packages/db_helper_postgres/src/admin-apis/store.ts b/packages/db_helper_postgres/src/admin-apis/store.ts index d326f67..9e19262 100644 --- a/packages/db_helper_postgres/src/admin-apis/store.ts +++ b/packages/db_helper_postgres/src/admin-apis/store.ts @@ -9,7 +9,7 @@ export interface Store { imageUrl: string | null; owner: number; createdAt: Date; - updatedAt: Date; + // updatedAt: Date; } export async function getAllStores(): Promise { @@ -68,7 +68,7 @@ export async function createStore( imageUrl: newStore.imageUrl, owner: newStore.owner, createdAt: newStore.createdAt, - updatedAt: newStore.updatedAt, + // updatedAt: newStore.updatedAt, }; } @@ -88,7 +88,7 @@ export async function updateStore( .update(storeInfo) .set({ ...input, - updatedAt: new Date(), + // updatedAt: new Date(), }) .where(eq(storeInfo.id, id)) .returning(); @@ -118,7 +118,7 @@ export async function updateStore( imageUrl: updatedStore.imageUrl, owner: updatedStore.owner, createdAt: updatedStore.createdAt, - updatedAt: updatedStore.updatedAt, + // updatedAt: updatedStore.updatedAt, }; } diff --git a/packages/db_helper_postgres/src/admin-apis/vendor-snippets.ts b/packages/db_helper_postgres/src/admin-apis/vendor-snippets.ts index 8cec6a6..5bd6fcc 100644 --- a/packages/db_helper_postgres/src/admin-apis/vendor-snippets.ts +++ b/packages/db_helper_postgres/src/admin-apis/vendor-snippets.ts @@ -1,86 +1,152 @@ -import { db } from '../db/db_index'; -import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, orderStatus } from '../db/schema'; -import { eq, and, inArray, gt, sql, asc } from 'drizzle-orm'; +import { db } from '../db/db_index' +import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, orderStatus } from '../db/schema' +import { desc, eq, inArray } from 'drizzle-orm' +import type { InferSelectModel } from 'drizzle-orm' +import type { + AdminDeliverySlot, + AdminVendorSnippet, + AdminVendorSnippetWithSlot, + AdminVendorSnippetProduct, + AdminVendorUpdatePackagingResult, +} from '@packages/shared' + +type VendorSnippetRow = InferSelectModel +type DeliverySlotRow = InferSelectModel +type ProductRow = InferSelectModel + +const mapVendorSnippet = (snippet: VendorSnippetRow): AdminVendorSnippet => ({ + id: snippet.id, + snippetCode: snippet.snippetCode, + slotId: snippet.slotId ?? null, + productIds: snippet.productIds, + isPermanent: snippet.isPermanent, + validTill: snippet.validTill ?? null, + createdAt: snippet.createdAt, +}) + +const mapDeliverySlot = (slot: DeliverySlotRow): AdminDeliverySlot => ({ + id: slot.id, + deliveryTime: slot.deliveryTime, + freezeTime: slot.freezeTime, + isActive: slot.isActive, + isFlash: slot.isFlash, + isCapacityFull: slot.isCapacityFull, + deliverySequence: slot.deliverySequence, + groupIds: slot.groupIds, +}) + +const mapProductSummary = (product:{id:number, name: string}): AdminVendorSnippetProduct => ({ + id: product.id, + name: product.name, +}) export async function checkVendorSnippetExists(snippetCode: string): Promise { const existingSnippet = await db.query.vendorSnippets.findFirst({ where: eq(vendorSnippets.snippetCode, snippetCode), - }); - return !!existingSnippet; + }) + return !!existingSnippet } -export async function getVendorSnippetById(id: number): Promise { - return await db.query.vendorSnippets.findFirst({ +export async function getVendorSnippetById(id: number): Promise { + const snippet = await db.query.vendorSnippets.findFirst({ where: eq(vendorSnippets.id, id), with: { slot: true, }, - }); + }) + + if (!snippet) { + return null + } + + return { + ...mapVendorSnippet(snippet), + slot: snippet.slot ? mapDeliverySlot(snippet.slot) : null, + } } -export async function getVendorSnippetByCode(snippetCode: string): Promise { - return await db.query.vendorSnippets.findFirst({ +export async function getVendorSnippetByCode(snippetCode: string): Promise { + const snippet = await db.query.vendorSnippets.findFirst({ where: eq(vendorSnippets.snippetCode, snippetCode), - }); + }) + + return snippet ? mapVendorSnippet(snippet) : null } -export async function getAllVendorSnippets(): Promise { - return await db.query.vendorSnippets.findMany({ +export async function getAllVendorSnippets(): Promise { + const snippets = await db.query.vendorSnippets.findMany({ with: { slot: true, }, orderBy: (vendorSnippets, { desc }) => [desc(vendorSnippets.createdAt)], - }); + }) + + return snippets.map((snippet) => ({ + ...mapVendorSnippet(snippet), + slot: snippet.slot ? mapDeliverySlot(snippet.slot) : null, + })) } -export interface CreateVendorSnippetInput { - snippetCode: string; - slotId?: number; - productIds: number[]; - isPermanent: boolean; - validTill?: Date; -} - -export async function createVendorSnippet(input: CreateVendorSnippetInput): Promise { +export async function createVendorSnippet(input: { + snippetCode: string + slotId?: number + productIds: number[] + isPermanent: boolean + validTill?: Date +}): Promise { const [result] = await db.insert(vendorSnippets).values({ snippetCode: input.snippetCode, slotId: input.slotId, productIds: input.productIds, isPermanent: input.isPermanent, validTill: input.validTill, - }).returning(); + }).returning() - return result; + return mapVendorSnippet(result) } -export async function updateVendorSnippet(id: number, updates: any): Promise { +export async function updateVendorSnippet(id: number, updates: { + snippetCode?: string + slotId?: number | null + productIds?: number[] + isPermanent?: boolean + validTill?: Date | null +}): Promise { const [result] = await db.update(vendorSnippets) .set(updates) .where(eq(vendorSnippets.id, id)) - .returning(); + .returning() - return result; + return result ? mapVendorSnippet(result) : null } -export async function deleteVendorSnippet(id: number): Promise { - await db.delete(vendorSnippets) - .where(eq(vendorSnippets.id, id)); +export async function deleteVendorSnippet(id: number): Promise { + const [result] = await db.delete(vendorSnippets) + .where(eq(vendorSnippets.id, id)) + .returning() + + return result ? mapVendorSnippet(result) : null } -export async function getProductsByIds(productIds: number[]): Promise { - return await db.query.productInfo.findMany({ +export async function getProductsByIds(productIds: number[]): Promise { + const products = await db.query.productInfo.findMany({ where: inArray(productInfo.id, productIds), columns: { id: true, name: true }, - }); + }) + + const prods = products.map(mapProductSummary) + return prods; } -export async function getVendorSlotById(slotId: number): Promise { - return await db.query.deliverySlotInfo.findFirst({ +export async function getVendorSlotById(slotId: number): Promise { + const slot = await db.query.deliverySlotInfo.findFirst({ where: eq(deliverySlotInfo.id, slotId), - }); + }) + + return slot ? mapDeliverySlot(slot) : null } -export async function getVendorOrdersBySlotId(slotId: number): Promise { +export async function getVendorOrdersBySlotId(slotId: number) { return await db.query.orders.findMany({ where: eq(orders.slotId, slotId), with: { @@ -98,10 +164,28 @@ export async function getVendorOrdersBySlotId(slotId: number): Promise { slot: true, }, orderBy: (orders, { desc }) => [desc(orders.createdAt)], - }); + }) } -export async function getOrderItemsByOrderIds(orderIds: number[]): Promise { +export async function getVendorOrders() { + return await db.query.orders.findMany({ + with: { + user: true, + orderItems: { + with: { + product: { + with: { + unit: true, + }, + }, + }, + }, + }, + orderBy: (orders, { desc }) => [desc(orders.createdAt)], + }) +} + +export async function getOrderItemsByOrderIds(orderIds: number[]) { return await db.query.orderItems.findMany({ where: inArray(orderItems.orderId, orderIds), with: { @@ -111,20 +195,56 @@ export async function getOrderItemsByOrderIds(orderIds: number[]): Promise { +export async function getOrderStatusByOrderIds(orderIds: number[]) { return await db.query.orderStatus.findMany({ where: inArray(orderStatus.orderId, orderIds), - }); + }) } -export async function updateVendorOrderItemPackaging(orderItemId: number, isPackaged: boolean, isPackageVerified: boolean): Promise { - await db.update(orderItems) +export async function updateVendorOrderItemPackaging( + orderItemId: number, + isPackaged: boolean +): Promise { + const orderItem = await db.query.orderItems.findFirst({ + where: eq(orderItems.id, orderItemId), + with: { + order: { + with: { + slot: true, + }, + }, + }, + }) + + if (!orderItem) { + return { success: false, message: 'Order item not found' } + } + + if (!orderItem.order.slotId) { + return { success: false, message: 'Order item not associated with a vendor slot' } + } + + const snippetExists = await db.query.vendorSnippets.findFirst({ + where: eq(vendorSnippets.slotId, orderItem.order.slotId), + }) + + if (!snippetExists) { + return { success: false, message: "No vendor snippet found for this order's slot" } + } + + const [updatedItem] = await db.update(orderItems) .set({ is_packaged: isPackaged, - is_package_verified: isPackageVerified, }) - .where(eq(orderItems.id, orderItemId)); + .where(eq(orderItems.id, orderItemId)) + .returning({ id: orderItems.id }) + + if (!updatedItem) { + return { success: false, message: 'Failed to update packaging status' } + } + + return { success: true, orderItemId, is_packaged: isPackaged } } diff --git a/packages/shared/types/admin.ts b/packages/shared/types/admin.ts index c79880d..3e8cecf 100644 --- a/packages/shared/types/admin.ts +++ b/packages/shared/types/admin.ts @@ -78,7 +78,7 @@ export interface Store { imageUrl: string | null; owner: number; createdAt: Date; - updatedAt: Date; + // updatedAt: Date; } export interface StaffUser { @@ -347,3 +347,367 @@ export interface AdminCancelOrderResult { userId?: number; error?: AdminCancelOrderError; } + +export interface AdminUnit { + id: number; + shortNotation: string; + fullName: string; +} + +export interface AdminProduct { + id: number; + name: string; + shortDescription: string | null; + longDescription: string | null; + unitId: number; + price: string; + marketPrice: string | null; + images: string[] | null; + isOutOfStock: boolean; + isSuspended: boolean; + isFlashAvailable: boolean; + flashPrice: string | null; + createdAt: Date; + incrementStep: number; + productQuantity: number; + storeId: number | null; +} + +export interface AdminProductWithRelations extends AdminProduct { + unit: AdminUnit; + store: Store | null; +} + +export interface AdminProductTagInfo { + id: number; + tagName: string; + tagDescription: string | null; + imageUrl: string | null; + isDashboardTag: boolean; + relatedStores: unknown; + createdAt: Date; +} + +export interface AdminProductTagAssignment { + productId: number; + tagId: number; + assignedAt: Date; + product: AdminProduct; +} + +export interface AdminProductTagWithProducts extends AdminProductTagInfo { + products: AdminProductTagAssignment[]; +} + +export interface AdminSpecialDeal { + id: number; + productId: number; + quantity: string; + price: string; + validTill: Date; +} + +export interface AdminProductWithDetails extends AdminProduct { + unit: AdminUnit; + deals: AdminSpecialDeal[]; + tags: AdminProductTagInfo[]; +} + +export interface AdminProductListResponse { + products: AdminProductWithRelations[]; + count: number; +} + +export interface AdminProductResponse { + product: AdminProductWithDetails; +} + +export interface AdminDeleteProductResult { + message: string; +} + +export interface AdminToggleOutOfStockResult { + product: AdminProduct; + message: string; +} + +export interface AdminUpdateSlotProductsResult { + message: string; + added: number; + removed: number; +} + +export interface AdminSlotProductIdsResult { + productIds: number[]; +} + +export type AdminSlotsProductIdsResult = Record; + +export interface AdminProductReview { + id: number; + reviewBody: string; + ratings: number; + imageUrls: unknown; + reviewTime: Date; + adminResponse: string | null; + adminResponseImages: unknown; + userName: string | null; +} + +export interface AdminProductReviewWithSignedUrls extends AdminProductReview { + signedImageUrls: string[]; + signedAdminImageUrls: string[]; +} + +export interface AdminProductReviewsResult { + reviews: AdminProductReviewWithSignedUrls[]; + hasMore: boolean; +} + +export interface AdminProductReviewResponse { + success: boolean; + review: AdminProductReview; +} + +export interface AdminProductGroup { + id: number; + groupName: string; + description: string | null; + createdAt: Date; + products: AdminProduct[]; + productCount: number; +} + +export interface AdminProductGroupsResult { + groups: AdminProductGroup[]; +} + +export interface AdminProductGroupResponse { + group: AdminProductGroupInfo; + message: string; +} + +export interface AdminProductGroupInfo { + id: number; + groupName: string; + description: string | null; + createdAt: Date; +} + +export interface AdminUpdateProductPricesResult { + message: string; + updatedCount: number; +} + +export interface AdminDeliverySlot { + id: number; + deliveryTime: Date; + freezeTime: Date; + isActive: boolean; + isFlash: boolean; + isCapacityFull: boolean; + deliverySequence: unknown; + groupIds: unknown; +} + +export interface AdminSlotProductSummary { + id: number; + name: string; + images: string[] | null; +} + +export interface AdminVendorSnippet { + id: number; + snippetCode: string; + slotId: number | null; + productIds: number[]; + isPermanent: boolean; + validTill: Date | null; + createdAt: Date; +} + +export interface AdminVendorSnippetWithAccess extends AdminVendorSnippet { + accessUrl: string; +} + +export interface AdminVendorSnippetWithSlot extends AdminVendorSnippet { + slot: AdminDeliverySlot | null; +} + +export interface AdminVendorSnippetProduct { + id: number; + name: string; +} + +export interface AdminVendorSnippetWithProducts extends AdminVendorSnippetWithSlot { + accessUrl: string; + products: AdminVendorSnippetProduct[]; +} + +export interface AdminSlotWithProducts extends AdminDeliverySlot { + deliverySequence: number[]; + products: AdminSlotProductSummary[]; +} + +export interface AdminSlotWithProductsAndSnippets extends AdminSlotWithProducts { + groupIds: number[]; + vendorSnippets: AdminVendorSnippetWithAccess[]; +} + +export interface AdminSlotWithProductsAndSnippetsBase extends AdminSlotWithProducts { + groupIds: number[]; + vendorSnippets: AdminVendorSnippet[]; +} + +export interface AdminSlotsResult { + slots: AdminSlotWithProducts[]; + count: number; +} + +export interface AdminSlotsListResult { + slots: AdminDeliverySlot[]; + count: number; +} + +export interface AdminSlotResult { + slot: AdminSlotWithProductsAndSnippets; +} + +export interface AdminSlotCreateResult { + slot: AdminDeliverySlot; + createdSnippets: AdminVendorSnippet[]; + message: string; +} + +export interface AdminSlotUpdateResult { + slot: AdminDeliverySlot; + createdSnippets: AdminVendorSnippet[]; + message: string; +} + +export interface AdminSlotDeleteResult { + message: string; +} + +export type AdminDeliverySequence = Record; + +export interface AdminDeliverySequenceResult { + deliverySequence: AdminDeliverySequence; +} + +export interface AdminUpdateDeliverySequenceResult { + slot: { + id: number; + deliverySequence: unknown; + }; + message: string; +} + +export interface AdminUpdateSlotCapacityResult { + success: boolean; + slot: AdminDeliverySlot; + message: string; +} + +export interface AdminVendorSnippetCreateInput { + snippetCode: string; + slotId?: number; + productIds: number[]; + validTill?: string; + isPermanent: boolean; +} + +export interface AdminVendorSnippetUpdateInput { + snippetCode?: string; + slotId?: number; + productIds?: number[]; + validTill?: string | null; + isPermanent?: boolean; +} + +export interface AdminVendorSnippetDeleteResult { + message: string; +} + +export interface AdminVendorSnippetOrderProduct { + orderItemId: number; + productId: number; + productName: string; + quantity: number; + productSize: number; + price: number; + unit: string; + subtotal: number; + is_packaged: boolean; + is_package_verified: boolean; +} + +export interface AdminVendorSnippetOrderSummary { + orderId: string; + orderDate: string; + customerName: string; + totalAmount: number; + slotInfo: { + time: string; + sequence: unknown; + } | null; + products: AdminVendorSnippetOrderProduct[]; + matchedProducts: number[]; + snippetCode: string; +} + +export interface AdminVendorSnippetOrdersResult { + success: boolean; + data: AdminVendorSnippetOrderSummary[]; + snippet: { + id: number; + snippetCode: string; + slotId: number | null; + productIds: number[]; + validTill?: string; + createdAt: string; + isPermanent: boolean; + }; +} + +export interface AdminVendorSnippetOrdersWithSlotResult extends AdminVendorSnippetOrdersResult { + selectedSlot: { + id: number; + deliveryTime: string; + freezeTime: string; + deliverySequence: unknown; + }; +} + +export interface AdminVendorOrderSummary { + id: number; + status: string; + orderDate: string; + totalQuantity: number; + products: { + name: string; + quantity: number; + unit: string; + }[]; +} + +export interface AdminUpcomingSlotsResult { + success: boolean; + data: { + id: number; + deliveryTime: string; + freezeTime: string; + deliverySequence: unknown; + }[]; +} + +export type AdminVendorUpdatePackagingResult = + | { + success: true; + orderItemId: number; + is_packaged: boolean; + } + | { + success: false; + message: string; + }