This commit is contained in:
shafi54 2026-03-25 19:30:01 +05:30
parent 306244e8df
commit 3c836e274d
12 changed files with 2099 additions and 332 deletions

View file

@ -92,12 +92,17 @@ export {
getOrderItemsByOrderIds, getOrderItemsByOrderIds,
getOrderStatusByOrderIds, getOrderStatusByOrderIds,
updateVendorOrderItemPackaging, updateVendorOrderItemPackaging,
getVendorOrders,
// Product methods // Product methods
getAllProducts, getAllProducts,
getProductById, getProductById,
deleteProduct,
createProduct, createProduct,
updateProduct, updateProduct,
toggleProductOutOfStock, toggleProductOutOfStock,
updateSlotProducts,
getSlotProductIds,
getSlotsProductIds,
getAllUnits, getAllUnits,
getAllProductTags, getAllProductTags,
getProductReviews, getProductReviews,
@ -108,16 +113,15 @@ export {
deleteProductGroup, deleteProductGroup,
addProductToGroup, addProductToGroup,
removeProductFromGroup, removeProductFromGroup,
updateProductPrices,
// Slots methods // Slots methods
getAllSlots, getActiveSlotsWithProducts,
getSlotById, getActiveSlots,
createSlot, getSlotsAfterDate,
updateSlot, getSlotByIdWithRelations,
deleteSlot, createSlotWithRelations,
getSlotProducts, updateSlotWithRelations,
addProductToSlot, deleteSlotById,
removeProductFromSlot,
clearSlotProducts,
updateSlotCapacity, updateSlotCapacity,
getSlotDeliverySequence, getSlotDeliverySequence,
updateSlotDeliverySequence, updateSlotDeliverySequence,
@ -164,6 +168,58 @@ export type {
AdminGetAllOrdersResultWithUserId, AdminGetAllOrdersResultWithUserId,
AdminRebalanceSlotsResult, AdminRebalanceSlotsResult,
AdminCancelOrderResult, 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'; } from '@packages/shared';
export type { export type {

View file

@ -1,24 +1,47 @@
import { router, protectedProcedure } from '@/src/trpc/trpc-index' import { router, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; 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 { ApiError } from '@/src/lib/api-error' import { ApiError } from '@/src/lib/api-error'
import { imageUploadS3, generateSignedUrlsFromS3Urls, getOriginalUrlFromSignedUrl, claimUploadUrl } from '@/src/lib/s3-client' import { generateSignedUrlsFromS3Urls, claimUploadUrl } from '@/src/lib/s3-client'
import { deleteS3Image } from '@/src/lib/delete-image'
import type { SpecialDeal } from '@/src/db/types'
import { scheduleStoreInitialization } from '@/src/stores/store-initializer' 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({ export const productRouter = router({
getProducts: protectedProcedure getProducts: protectedProcedure
.query(async ({ ctx }) => { .query(async (): Promise<AdminProductListResponse> => {
const products = await getAllProductsInDb()
/*
// Old implementation - direct DB query:
const products = await db.query.productInfo.findMany({ const products = await db.query.productInfo.findMany({
orderBy: productInfo.name, orderBy: productInfo.name,
with: { with: {
@ -26,28 +49,32 @@ export const productRouter = router({
store: true, store: true,
}, },
}); });
*/
// Generate signed URLs for all product images
const productsWithSignedUrls = await Promise.all( const productsWithSignedUrls = await Promise.all(
products.map(async (product) => ({ products.map(async (product) => ({
...product, ...product,
images: await generateSignedUrlsFromS3Urls((product.images as string[]) || []), images: await generateSignedUrlsFromS3Urls((product.images as string[]) || []),
})) }))
); )
return { return {
products: productsWithSignedUrls, products: productsWithSignedUrls,
count: productsWithSignedUrls.length, count: productsWithSignedUrls.length,
}; }
}), }),
getProductById: protectedProcedure getProductById: protectedProcedure
.input(z.object({ .input(z.object({
id: z.number(), id: z.number(),
})) }))
.query(async ({ input, ctx }) => { .query(async ({ input }): Promise<AdminProductResponse> => {
const { id } = input; const { id } = input;
const product = await getProductByIdInDb(id)
/*
// Old implementation - direct DB queries:
const product = await db.query.productInfo.findFirst({ const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id), where: eq(productInfo.id, id),
with: { with: {
@ -84,15 +111,33 @@ export const productRouter = router({
return { return {
product: productWithSignedUrls, 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 deleteProduct: protectedProcedure
.input(z.object({ .input(z.object({
id: z.number(), id: z.number(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }): Promise<AdminDeleteProductResult> => {
const { id } = input; const { id } = input;
const deletedProduct = await deleteProductInDb(id)
/*
// Old implementation - direct DB query:
const [deletedProduct] = await db const [deletedProduct] = await db
.delete(productInfo) .delete(productInfo)
.where(eq(productInfo.id, id)) .where(eq(productInfo.id, id))
@ -101,22 +146,31 @@ export const productRouter = router({
if (!deletedProduct) { if (!deletedProduct) {
throw new ApiError("Product not found", 404); throw new ApiError("Product not found", 404);
} }
*/
if (!deletedProduct) {
throw new ApiError('Product not found', 404)
}
// Reinitialize stores to reflect changes // Reinitialize stores to reflect changes
scheduleStoreInitialization() scheduleStoreInitialization()
return { return {
message: "Product deleted successfully", message: 'Product deleted successfully',
}; }
}), }),
toggleOutOfStock: protectedProcedure toggleOutOfStock: protectedProcedure
.input(z.object({ .input(z.object({
id: z.number(), id: z.number(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }): Promise<AdminToggleOutOfStockResult> => {
const { id } = input; const { id } = input;
const updatedProduct = await toggleProductOutOfStockInDb(id)
/*
// Old implementation - direct DB queries:
const product = await db.query.productInfo.findFirst({ const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id), where: eq(productInfo.id, id),
}); });
@ -132,14 +186,18 @@ export const productRouter = router({
}) })
.where(eq(productInfo.id, id)) .where(eq(productInfo.id, id))
.returning(); .returning();
*/
if (!updatedProduct) {
throw new ApiError('Product not found', 404)
}
// Reinitialize stores to reflect changes
scheduleStoreInitialization() scheduleStoreInitialization()
return { return {
product: updatedProduct, product: updatedProduct,
message: `Product marked as ${updatedProduct.isOutOfStock ? 'out of stock' : 'in stock'}`, message: `Product marked as ${updatedProduct.isOutOfStock ? 'out of stock' : 'in stock'}`,
}; }
}), }),
updateSlotProducts: protectedProcedure updateSlotProducts: protectedProcedure
@ -147,13 +205,17 @@ export const productRouter = router({
slotId: z.string(), slotId: z.string(),
productIds: z.array(z.string()), productIds: z.array(z.string()),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }): Promise<AdminUpdateSlotProductsResult> => {
const { slotId, productIds } = input; const { slotId, productIds } = input;
if (!Array.isArray(productIds)) { if (!Array.isArray(productIds)) {
throw new ApiError("productIds must be an array", 400); throw new ApiError("productIds must be an array", 400);
} }
const result = await updateSlotProductsInDb(slotId, productIds)
/*
// Old implementation - direct DB queries:
// Get current associations // Get current associations
const currentAssociations = await db.query.productSlots.findMany({ const currentAssociations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, parseInt(slotId)), where: eq(productSlots.slotId, parseInt(slotId)),
@ -197,15 +259,28 @@ export const productRouter = router({
added: productsToAdd.length, added: productsToAdd.length,
removed: productsToRemove.length, removed: productsToRemove.length,
}; };
*/
scheduleStoreInitialization()
return {
message: 'Slot products updated successfully',
added: result.added,
removed: result.removed,
}
}), }),
getSlotProductIds: protectedProcedure getSlotProductIds: protectedProcedure
.input(z.object({ .input(z.object({
slotId: z.string(), slotId: z.string(),
})) }))
.query(async ({ input, ctx }) => { .query(async ({ input }): Promise<AdminSlotProductIdsResult> => {
const { slotId } = input; const { slotId } = input;
const productIds = await getSlotProductIdsInDb(slotId)
/*
// Old implementation - direct DB queries:
const associations = await db.query.productSlots.findMany({ const associations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, parseInt(slotId)), where: eq(productSlots.slotId, parseInt(slotId)),
columns: { columns: {
@ -218,19 +293,28 @@ export const productRouter = router({
return { return {
productIds, productIds,
}; };
*/
return {
productIds,
}
}), }),
getSlotsProductIds: protectedProcedure getSlotsProductIds: protectedProcedure
.input(z.object({ .input(z.object({
slotIds: z.array(z.number()), slotIds: z.array(z.number()),
})) }))
.query(async ({ input, ctx }) => { .query(async ({ input }): Promise<AdminSlotsProductIdsResult> => {
const { slotIds } = input; const { slotIds } = input;
if (!Array.isArray(slotIds)) { if (!Array.isArray(slotIds)) {
throw new ApiError("slotIds must be an array", 400); throw new ApiError("slotIds must be an array", 400);
} }
const result = await getSlotsProductIdsInDb(slotIds)
/*
// Old implementation - direct DB queries:
if (slotIds.length === 0) { if (slotIds.length === 0) {
return {}; return {};
} }
@ -261,6 +345,9 @@ export const productRouter = router({
}); });
return result; return result;
*/
return result
}), }),
getProductReviews: protectedProcedure getProductReviews: protectedProcedure
@ -269,9 +356,13 @@ export const productRouter = router({
limit: z.number().int().min(1).max(50).optional().default(10), limit: z.number().int().min(1).max(50).optional().default(10),
offset: z.number().int().min(0).optional().default(0), offset: z.number().int().min(0).optional().default(0),
})) }))
.query(async ({ input }) => { .query(async ({ input }): Promise<AdminProductReviewsResult> => {
const { productId, limit, offset } = input; const { productId, limit, offset } = input;
const { reviews, totalCount } = await getProductReviewsInDb(productId, limit, offset)
/*
// Old implementation - direct DB queries:
const reviews = await db const reviews = await db
.select({ .select({
id: productReviews.id, id: productReviews.id,
@ -309,6 +400,19 @@ export const productRouter = router({
const hasMore = offset + limit < totalCount; const hasMore = offset + limit < totalCount;
return { reviews: reviewsWithSignedUrls, hasMore }; 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 respondToReview: protectedProcedure
@ -318,9 +422,13 @@ export const productRouter = router({
adminResponseImages: z.array(z.string()).optional().default([]), adminResponseImages: z.array(z.string()).optional().default([]),
uploadUrls: z.array(z.string()).optional().default([]), uploadUrls: z.array(z.string()).optional().default([]),
})) }))
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<AdminProductReviewResponse> => {
const { reviewId, adminResponse, adminResponseImages, uploadUrls } = input; const { reviewId, adminResponse, adminResponseImages, uploadUrls } = input;
const updatedReview = await respondToReviewInDb(reviewId, adminResponse, adminResponseImages)
/*
// Old implementation - direct DB queries:
const [updatedReview] = await db const [updatedReview] = await db
.update(productReviews) .update(productReviews)
.set({ .set({
@ -341,10 +449,25 @@ export const productRouter = router({
} }
return { success: true, review: updatedReview }; 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 getGroups: protectedProcedure
.query(async ({ ctx }) => { .query(async (): Promise<AdminProductGroupsResult> => {
const groups = await getAllProductGroupsInDb()
/*
// Old implementation - direct DB queries:
const groups = await db.query.productGroupInfo.findMany({ const groups = await db.query.productGroupInfo.findMany({
with: { with: {
memberships: { memberships: {
@ -355,14 +478,18 @@ export const productRouter = router({
}, },
orderBy: desc(productGroupInfo.createdAt), orderBy: desc(productGroupInfo.createdAt),
}); });
*/
return { return {
groups: groups.map(group => ({ groups: groups.map(group => ({
...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, productCount: group.memberships.length,
})), })),
}; }
}), }),
createGroup: protectedProcedure createGroup: protectedProcedure
@ -371,9 +498,13 @@ export const productRouter = router({
description: z.string().optional(), description: z.string().optional(),
product_ids: z.array(z.number()).default([]), product_ids: z.array(z.number()).default([]),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }): Promise<AdminProductGroupResponse> => {
const { group_name, description, product_ids } = input; 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 const [newGroup] = await db
.insert(productGroupInfo) .insert(productGroupInfo)
.values({ .values({
@ -398,6 +529,14 @@ export const productRouter = router({
group: newGroup, group: newGroup,
message: 'Group created successfully', message: 'Group created successfully',
}; };
*/
scheduleStoreInitialization()
return {
group: newGroup,
message: 'Group created successfully',
}
}), }),
updateGroup: protectedProcedure updateGroup: protectedProcedure
@ -407,9 +546,13 @@ export const productRouter = router({
description: z.string().optional(), description: z.string().optional(),
product_ids: z.array(z.number()).optional(), product_ids: z.array(z.number()).optional(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }): Promise<AdminProductGroupResponse> => {
const { id, group_name, description, product_ids } = input; 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 = {}; const updateData: any = {};
if (group_name !== undefined) updateData.groupName = group_name; if (group_name !== undefined) updateData.groupName = group_name;
if (description !== undefined) updateData.description = description; if (description !== undefined) updateData.description = description;
@ -446,15 +589,31 @@ export const productRouter = router({
group: updatedGroup, group: updatedGroup,
message: 'Group updated successfully', message: 'Group updated successfully',
}; };
*/
if (!updatedGroup) {
throw new ApiError('Group not found', 404)
}
scheduleStoreInitialization()
return {
group: updatedGroup,
message: 'Group updated successfully',
}
}), }),
deleteGroup: protectedProcedure deleteGroup: protectedProcedure
.input(z.object({ .input(z.object({
id: z.number(), id: z.number(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }): Promise<AdminDeleteProductResult> => {
const { id } = input; const { id } = input;
const deletedGroup = await deleteProductGroupInDb(id)
/*
// Old implementation - direct DB queries:
// Delete memberships first // Delete memberships first
await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id)); await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id));
@ -474,6 +633,17 @@ export const productRouter = router({
return { return {
message: 'Group deleted successfully', message: 'Group deleted successfully',
}; };
*/
if (!deletedGroup) {
throw new ApiError('Group not found', 404)
}
scheduleStoreInitialization()
return {
message: 'Group deleted successfully',
}
}), }),
updateProductPrices: protectedProcedure updateProductPrices: protectedProcedure
@ -486,9 +656,17 @@ export const productRouter = router({
isFlashAvailable: z.boolean().optional(), isFlashAvailable: z.boolean().optional(),
})), })),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input }): Promise<AdminUpdateProductPricesResult> => {
const { updates } = input; 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) { if (updates.length === 0) {
throw new ApiError('No updates provided', 400); throw new ApiError('No updates provided', 400);
} }
@ -531,5 +709,17 @@ export const productRouter = router({
message: `Updated prices for ${updates.length} product(s)`, message: `Updated prices for ${updates.length} product(s)`,
updatedCount: updates.length, 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,
}
}), }),
}); });

View file

@ -1,14 +1,38 @@
import { router, protectedProcedure } from "@/src/trpc/trpc-index" import { router, protectedProcedure } from "@/src/trpc/trpc-index"
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { z } from "zod"; 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 { ApiError } from "@/src/lib/api-error"
import { appUrl } from "@/src/lib/env-exporter" import { appUrl } from "@/src/lib/env-exporter"
import redisClient from "@/src/lib/redis-client" import redisClient from "@/src/lib/redis-client"
import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters" import { getSlotSequenceKey } from "@/src/lib/redisKeyGetters"
import { scheduleStoreInitialization } from '@/src/stores/store-initializer' 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 { interface CachedDeliverySequence {
@ -64,11 +88,15 @@ const updateDeliverySequenceSchema = z.object({
export const slotsRouter = router({ export const slotsRouter = router({
// Exact replica of GET /av/slots // Exact replica of GET /av/slots
getAll: protectedProcedure.query(async ({ ctx }) => { getAll: protectedProcedure.query(async ({ ctx }): Promise<AdminSlotsResult> => {
if (!ctx.staffUser?.id) { if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
} }
const slots = await getActiveSlotsWithProductsInDb()
/*
// Old implementation - direct DB queries:
const slots = await db.query.deliverySlotInfo const slots = await db.query.deliverySlotInfo
.findMany({ .findMany({
where: eq(deliverySlotInfo.isActive, true), where: eq(deliverySlotInfo.isActive, true),
@ -94,17 +122,18 @@ export const slotsRouter = router({
products: slot.productSlots.map((ps) => ps.product), products: slot.productSlots.map((ps) => ps.product),
})) }))
); );
*/
return { return {
slots, slots,
count: slots.length, count: slots.length,
}; }
}), }),
// Exact replica of POST /av/products/slots/product-ids // Exact replica of POST /av/products/slots/product-ids
getSlotsProductIds: protectedProcedure getSlotsProductIds: protectedProcedure
.input(z.object({ slotIds: z.array(z.number()) })) .input(z.object({ slotIds: z.array(z.number()) }))
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }): Promise<AdminSlotsProductIdsResult> => {
if (!ctx.staffUser?.id) { if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); 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) { if (slotIds.length === 0) {
return {}; return {};
} }
@ -148,6 +181,9 @@ export const slotsRouter = router({
}); });
return result; return result;
*/
return result
}), }),
// Exact replica of PUT /av/products/slots/:slotId/products // Exact replica of PUT /av/products/slots/:slotId/products
@ -158,7 +194,7 @@ export const slotsRouter = router({
productIds: z.array(z.number()), productIds: z.array(z.number()),
}) })
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<AdminUpdateSlotProductsResult> => {
if (!ctx.staffUser?.id) { if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); 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 // Get current associations
const currentAssociations = await db.query.productSlots.findMany({ const currentAssociations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, slotId), where: eq(productSlots.slotId, slotId),
@ -223,11 +263,20 @@ export const slotsRouter = router({
added: productsToAdd.length, added: productsToAdd.length,
removed: productsToRemove.length, removed: productsToRemove.length,
}; };
*/
scheduleStoreInitialization()
return {
message: result.message,
added: result.added,
removed: result.removed,
}
}), }),
createSlot: protectedProcedure createSlot: protectedProcedure
.input(createSlotSchema) .input(createSlotSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<AdminSlotCreateResult> => {
if (!ctx.staffUser?.id) { if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); 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); 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) => { const result = await db.transaction(async (tx) => {
// Create slot // Create slot
const [newSlot] = await tx const [newSlot] = await tx
@ -297,37 +357,47 @@ export const slotsRouter = router({
message: "Slot created successfully", message: "Slot created successfully",
}; };
}); });
*/
// Reinitialize stores to reflect changes (outside transaction) // Reinitialize stores to reflect changes (outside transaction)
scheduleStoreInitialization() scheduleStoreInitialization()
return result; return result
}), }),
getSlots: protectedProcedure.query(async ({ ctx }) => { getSlots: protectedProcedure.query(async ({ ctx }): Promise<AdminSlotsListResult> => {
if (!ctx.staffUser?.id) { if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
} }
const slots = await getActiveSlotsInDb()
/*
// Old implementation - direct DB queries:
const slots = await db.query.deliverySlotInfo.findMany({ const slots = await db.query.deliverySlotInfo.findMany({
where: eq(deliverySlotInfo.isActive, true), where: eq(deliverySlotInfo.isActive, true),
}); });
*/
return { return {
slots, slots,
count: slots.length, count: slots.length,
}; }
}), }),
getSlotById: protectedProcedure getSlotById: protectedProcedure
.input(getSlotByIdSchema) .input(getSlotByIdSchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }): Promise<AdminSlotResult> => {
if (!ctx.staffUser?.id) { if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
} }
const { id } = input; const { id } = input;
const slot = await getSlotByIdWithRelationsInDb(id)
/*
// Old implementation - direct DB queries:
const slot = await db.query.deliverySlotInfo.findFirst({ const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, id), where: eq(deliverySlotInfo.id, id),
with: { with: {
@ -345,28 +415,26 @@ export const slotsRouter = router({
vendorSnippets: true, vendorSnippets: true,
}, },
}); });
*/
if (!slot) { if (!slot) {
throw new ApiError("Slot not found", 404); throw new ApiError('Slot not found', 404)
} }
return { return {
slot: { slot: {
...slot, ...slot,
deliverySequence: slot.deliverySequence as number[], vendorSnippets: slot.vendorSnippets.map(snippet => ({
groupIds: slot.groupIds as number[],
products: slot.productSlots.map((ps) => ps.product),
vendorSnippets: slot.vendorSnippets?.map(snippet => ({
...snippet, ...snippet,
accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}` accessUrl: `${appUrl}/vendor-order-list?id=${snippet.snippetCode}`,
})), })),
}, },
}; }
}), }),
updateSlot: protectedProcedure updateSlot: protectedProcedure
.input(updateSlotSchema) .input(updateSlotSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<AdminSlotUpdateResult> => {
if (!ctx.staffUser?.id) { if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); 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); 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 // Filter groupIds to only include valid (existing) groups
let validGroupIds = groupIds; let validGroupIds = groupIds;
if (groupIds && groupIds.length > 0) { if (groupIds && groupIds.length > 0) {
@ -456,11 +536,16 @@ export const slotsRouter = router({
message: "Slot updated successfully", message: "Slot updated successfully",
}; };
}); });
*/
if (!result) {
throw new ApiError('Slot not found', 404)
}
// Reinitialize stores to reflect changes (outside transaction) // Reinitialize stores to reflect changes (outside transaction)
scheduleStoreInitialization() scheduleStoreInitialization()
return result; return result
} }
catch(e) { catch(e) {
console.log(e) console.log(e)
@ -470,13 +555,17 @@ export const slotsRouter = router({
deleteSlot: protectedProcedure deleteSlot: protectedProcedure
.input(deleteSlotSchema) .input(deleteSlotSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<AdminSlotDeleteResult> => {
if (!ctx.staffUser?.id) { if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
} }
const { id } = input; const { id } = input;
const deletedSlot = await deleteSlotByIdInDb(id)
/*
// Old implementation - direct DB queries:
const [deletedSlot] = await db const [deletedSlot] = await db
.update(deliverySlotInfo) .update(deliverySlotInfo)
.set({ isActive: false }) .set({ isActive: false })
@ -486,18 +575,23 @@ export const slotsRouter = router({
if (!deletedSlot) { if (!deletedSlot) {
throw new ApiError("Slot not found", 404); throw new ApiError("Slot not found", 404);
} }
*/
if (!deletedSlot) {
throw new ApiError('Slot not found', 404)
}
// Reinitialize stores to reflect changes // Reinitialize stores to reflect changes
scheduleStoreInitialization() scheduleStoreInitialization()
return { return {
message: "Slot deleted successfully", message: 'Slot deleted successfully',
}; }
}), }),
getDeliverySequence: protectedProcedure getDeliverySequence: protectedProcedure
.input(getDeliverySequenceSchema) .input(getDeliverySequenceSchema)
.query(async ({ input, ctx }) => { .query(async ({ input, ctx }): Promise<AdminDeliverySequenceResult> => {
const { id } = input; const { id } = input;
const slotId = parseInt(id); const slotId = parseInt(id);
@ -507,7 +601,7 @@ export const slotsRouter = router({
const cached = await redisClient.get(cacheKey); const cached = await redisClient.get(cacheKey);
if (cached) { if (cached) {
const parsed = JSON.parse(cached); const parsed = JSON.parse(cached);
const validated = cachedSequenceSchema.parse(parsed) as CachedDeliverySequence; const validated = cachedSequenceSchema.parse(parsed);
console.log('sending cached response') console.log('sending cached response')
return { deliverySequence: validated }; return { deliverySequence: validated };
@ -518,6 +612,10 @@ export const slotsRouter = router({
} }
// Fallback to DB // Fallback to DB
const slot = await getSlotDeliverySequenceInDb(slotId)
/*
// Old implementation - direct DB queries:
const slot = await db.query.deliverySlotInfo.findFirst({ const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId), where: eq(deliverySlotInfo.id, slotId),
}); });
@ -526,6 +624,13 @@ export const slotsRouter = router({
throw new ApiError("Slot not found", 404); 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; const sequence = (slot.deliverySequence || {}) as CachedDeliverySequence;
// Cache the validated result // Cache the validated result
@ -536,18 +641,22 @@ export const slotsRouter = router({
console.warn('Redis cache write failed:', cacheError); console.warn('Redis cache write failed:', cacheError);
} }
return { deliverySequence: sequence }; return { deliverySequence: sequence }
}), }),
updateDeliverySequence: protectedProcedure updateDeliverySequence: protectedProcedure
.input(updateDeliverySequenceSchema) .input(updateDeliverySequenceSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<AdminUpdateDeliverySequenceResult> => {
if (!ctx.staffUser?.id) { if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
} }
const { id, deliverySequence } = input; const { id, deliverySequence } = input;
const updatedSlot = await updateSlotDeliverySequenceInDb(id, deliverySequence)
/*
// Old implementation - direct DB queries:
const [updatedSlot] = await db const [updatedSlot] = await db
.update(deliverySlotInfo) .update(deliverySlotInfo)
.set({ deliverySequence }) .set({ deliverySequence })
@ -560,6 +669,11 @@ export const slotsRouter = router({
if (!updatedSlot) { if (!updatedSlot) {
throw new ApiError("Slot not found", 404); throw new ApiError("Slot not found", 404);
} }
*/
if (!updatedSlot) {
throw new ApiError('Slot not found', 404)
}
// Cache the updated sequence // Cache the updated sequence
const cacheKey = getSlotSequenceKey(id); const cacheKey = getSlotSequenceKey(id);
@ -572,8 +686,8 @@ export const slotsRouter = router({
return { return {
slot: updatedSlot, slot: updatedSlot,
message: "Delivery sequence updated successfully", message: 'Delivery sequence updated successfully',
}; }
}), }),
updateSlotCapacity: protectedProcedure updateSlotCapacity: protectedProcedure
@ -581,13 +695,17 @@ export const slotsRouter = router({
slotId: z.number(), slotId: z.number(),
isCapacityFull: z.boolean(), isCapacityFull: z.boolean(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<AdminUpdateSlotCapacityResult> => {
if (!ctx.staffUser?.id) { if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" }); throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
} }
const { slotId, isCapacityFull } = input; const { slotId, isCapacityFull } = input;
const result = await updateSlotCapacityInDb(slotId, isCapacityFull)
/*
// Old implementation - direct DB queries:
const [updatedSlot] = await db const [updatedSlot] = await db
.update(deliverySlotInfo) .update(deliverySlotInfo)
.set({ isCapacityFull }) .set({ isCapacityFull })
@ -606,5 +724,14 @@ export const slotsRouter = router({
slot: updatedSlot, slot: updatedSlot,
message: `Slot ${isCapacityFull ? 'marked as full capacity' : 'capacity reset'}`, message: `Slot ${isCapacityFull ? 'marked as full capacity' : 'capacity reset'}`,
}; };
*/
if (!result) {
throw new ApiError('Slot not found', 404)
}
scheduleStoreInitialization()
return result
}), }),
}); });

View file

@ -1,10 +1,33 @@
import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index' import { router, publicProcedure, protectedProcedure } from '@/src/trpc/trpc-index'
import { z } from 'zod'; import { z } from 'zod'
import dayjs from 'dayjs'; 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 { appUrl } from '@/src/lib/env-exporter' 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({ const createSnippetSchema = z.object({
snippetCode: z.string().min(1, "Snippet code is required"), snippetCode: z.string().min(1, "Snippet code is required"),
@ -26,7 +49,7 @@ const updateSnippetSchema = z.object({
export const vendorSnippetsRouter = router({ export const vendorSnippetsRouter = router({
create: protectedProcedure create: protectedProcedure
.input(createSnippetSchema) .input(createSnippetSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<AdminVendorSnippet> => {
const { snippetCode, slotId, productIds, validTill, isPermanent } = input; const { snippetCode, slotId, productIds, validTill, isPermanent } = input;
// Get staff user ID from auth middleware // Get staff user ID from auth middleware
@ -35,6 +58,33 @@ export const vendorSnippetsRouter = router({
throw new Error("Unauthorized"); 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 // Validate slot exists
if(slotId) { if(slotId) {
const slot = await db.query.deliverySlotInfo.findFirst({ const slot = await db.query.deliverySlotInfo.findFirst({
@ -70,13 +120,32 @@ export const vendorSnippetsRouter = router({
}).returning(); }).returning();
return result[0]; return result[0];
*/
return result
}), }),
getAll: protectedProcedure getAll: protectedProcedure
.query(async () => { .query(async (): Promise<AdminVendorSnippetWithProducts[]> => {
console.log('from the vendor snipptes methods') console.log('from the vendor snipptes methods')
try { 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({ const result = await db.query.vendorSnippets.findMany({
with: { with: {
slot: true, slot: true,
@ -100,18 +169,25 @@ export const vendorSnippetsRouter = router({
); );
return snippetsWithProducts; return snippetsWithProducts;
*/
return snippetsWithProducts
} }
catch(e) { catch(e) {
console.log(e) console.log(e)
} }
return []; return []
}), }),
getById: protectedProcedure getById: protectedProcedure
.input(z.object({ id: z.number().int().positive() })) .input(z.object({ id: z.number().int().positive() }))
.query(async ({ input }) => { .query(async ({ input }): Promise<AdminVendorSnippetWithSlot> => {
const { id } = input; const { id } = input;
const result = await getVendorSnippetByIdInDb(id)
/*
// Old implementation - direct DB queries:
const result = await db.query.vendorSnippets.findFirst({ const result = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.id, id), where: eq(vendorSnippets.id, id),
with: { with: {
@ -124,14 +200,57 @@ export const vendorSnippetsRouter = router({
} }
return result; return result;
*/
if (!result) {
throw new Error('Vendor snippet not found')
}
return result
}), }),
update: protectedProcedure update: protectedProcedure
.input(updateSnippetSchema) .input(updateSnippetSchema)
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<AdminVendorSnippet> => {
const { id, updates } = input; 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({ const existingSnippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.id, id), where: eq(vendorSnippets.id, id),
}); });
@ -184,13 +303,24 @@ export const vendorSnippetsRouter = router({
} }
return result[0]; return result[0];
*/
if (!result) {
throw new Error('Failed to update vendor snippet')
}
return result
}), }),
delete: protectedProcedure delete: protectedProcedure
.input(z.object({ id: z.number().int().positive() })) .input(z.object({ id: z.number().int().positive() }))
.mutation(async ({ input }) => { .mutation(async ({ input }): Promise<AdminVendorSnippetDeleteResult> => {
const { id } = input; const { id } = input;
const result = await deleteVendorSnippetInDb(id)
/*
// Old implementation - direct DB queries:
const result = await db.delete(vendorSnippets) const result = await db.delete(vendorSnippets)
.where(eq(vendorSnippets.id, id)) .where(eq(vendorSnippets.id, id))
.returning(); .returning();
@ -200,15 +330,26 @@ export const vendorSnippetsRouter = router({
} }
return { message: "Vendor snippet deleted successfully" }; return { message: "Vendor snippet deleted successfully" };
*/
if (!result) {
throw new Error('Vendor snippet not found')
}
return { message: 'Vendor snippet deleted successfully' }
}), }),
getOrdersBySnippet: publicProcedure getOrdersBySnippet: publicProcedure
.input(z.object({ .input(z.object({
snippetCode: z.string().min(1, "Snippet code is required") snippetCode: z.string().min(1, "Snippet code is required")
})) }))
.query(async ({ input }) => { .query(async ({ input }): Promise<AdminVendorSnippetOrdersResult> => {
const { snippetCode } = input; const { snippetCode } = input;
const snippet = await getVendorSnippetByCodeInDb(snippetCode)
/*
// Old implementation - direct DB queries:
// Find the snippet // Find the snippet
const snippet = await db.query.vendorSnippets.findFirst({ const snippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode), where: eq(vendorSnippets.snippetCode, snippetCode),
@ -242,6 +383,21 @@ export const vendorSnippetsRouter = router({
}, },
orderBy: (orders, { desc }) => [desc(orders.createdAt)], 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 // Filter orders that contain at least one of the snippet's products
const filteredOrders = matchingOrders.filter(order => { const filteredOrders = matchingOrders.filter(order => {
@ -276,7 +432,7 @@ export const vendorSnippetsRouter = router({
return { return {
orderId: `ORD${order.id}`, orderId: `ORD${order.id}`,
orderDate: order.createdAt.toISOString(), orderDate: order.createdAt.toISOString(),
customerName: order.user.name, customerName: order.user.name || '',
totalAmount: orderTotal, totalAmount: orderTotal,
slotInfo: order.slot ? { slotInfo: order.slot ? {
time: order.slot.deliveryTime.toISOString(), time: order.slot.deliveryTime.toISOString(),
@ -300,11 +456,15 @@ export const vendorSnippetsRouter = router({
createdAt: snippet.createdAt.toISOString(), createdAt: snippet.createdAt.toISOString(),
isPermanent: snippet.isPermanent, isPermanent: snippet.isPermanent,
}, },
}; }
}), }),
getVendorOrders: protectedProcedure getVendorOrders: protectedProcedure
.query(async () => { .query(async (): Promise<AdminVendorOrderSummary[]> => {
const vendorOrders = await getVendorOrdersInDb()
/*
// Old implementation - direct DB queries:
const vendorOrders = await db.query.orders.findMany({ const vendorOrders = await db.query.orders.findMany({
with: { with: {
user: true, user: true,
@ -320,10 +480,11 @@ export const vendorSnippetsRouter = router({
}, },
orderBy: (orders, { desc }) => [desc(orders.createdAt)], orderBy: (orders, { desc }) => [desc(orders.createdAt)],
}); });
*/
return vendorOrders.map(order => ({ return vendorOrders.map(order => ({
id: order.id, id: order.id,
status: 'pending', // Default status since orders table may not have status field status: 'pending',
orderDate: order.createdAt.toISOString(), orderDate: order.createdAt.toISOString(),
totalQuantity: order.orderItems.reduce((sum, item) => sum + parseFloat(item.quantity || '0'), 0), totalQuantity: order.orderItems.reduce((sum, item) => sum + parseFloat(item.quantity || '0'), 0),
products: order.orderItems.map(item => ({ products: order.orderItems.map(item => ({
@ -331,12 +492,16 @@ export const vendorSnippetsRouter = router({
quantity: parseFloat(item.quantity || '0'), quantity: parseFloat(item.quantity || '0'),
unit: item.product.unit?.shortNotation || 'unit', unit: item.product.unit?.shortNotation || 'unit',
})), })),
})); }))
}), }),
getUpcomingSlots: publicProcedure getUpcomingSlots: publicProcedure
.query(async () => { .query(async (): Promise<AdminUpcomingSlotsResult> => {
const threeHoursAgo = dayjs().subtract(3, 'hour').toDate(); const threeHoursAgo = dayjs().subtract(3, 'hour').toDate();
const slots = await getSlotsAfterDateInDb(threeHoursAgo)
/*
// Old implementation - direct DB queries:
const slots = await db.query.deliverySlotInfo.findMany({ const slots = await db.query.deliverySlotInfo.findMany({
where: and( where: and(
eq(deliverySlotInfo.isActive, true), eq(deliverySlotInfo.isActive, true),
@ -344,6 +509,7 @@ export const vendorSnippetsRouter = router({
), ),
orderBy: asc(deliverySlotInfo.deliveryTime), orderBy: asc(deliverySlotInfo.deliveryTime),
}); });
*/
return { return {
success: true, success: true,
@ -353,7 +519,7 @@ export const vendorSnippetsRouter = router({
freezeTime: slot.freezeTime.toISOString(), freezeTime: slot.freezeTime.toISOString(),
deliverySequence: slot.deliverySequence, deliverySequence: slot.deliverySequence,
})), })),
}; }
}), }),
getOrdersBySnippetAndSlot: publicProcedure getOrdersBySnippetAndSlot: publicProcedure
@ -361,9 +527,14 @@ export const vendorSnippetsRouter = router({
snippetCode: z.string().min(1, "Snippet code is required"), snippetCode: z.string().min(1, "Snippet code is required"),
slotId: z.number().int().positive("Valid slot ID is required"), slotId: z.number().int().positive("Valid slot ID is required"),
})) }))
.query(async ({ input }) => { .query(async ({ input }): Promise<AdminVendorSnippetOrdersWithSlotResult> => {
const { snippetCode, slotId } = input; const { snippetCode, slotId } = input;
const snippet = await getVendorSnippetByCodeInDb(snippetCode)
const slot = await getVendorSlotByIdInDb(slotId)
/*
// Old implementation - direct DB queries:
// Find the snippet // Find the snippet
const snippet = await db.query.vendorSnippets.findFirst({ const snippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode), where: eq(vendorSnippets.snippetCode, snippetCode),
@ -401,6 +572,17 @@ export const vendorSnippetsRouter = router({
}, },
orderBy: (orders, { desc }) => [desc(orders.createdAt)], 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 // Filter orders that contain at least one of the snippet's products
const filteredOrders = matchingOrders.filter(order => { const filteredOrders = matchingOrders.filter(order => {
@ -435,7 +617,7 @@ export const vendorSnippetsRouter = router({
return { return {
orderId: `ORD${order.id}`, orderId: `ORD${order.id}`,
orderDate: order.createdAt.toISOString(), orderDate: order.createdAt.toISOString(),
customerName: order.user.name, customerName: order.user.name || '',
totalAmount: orderTotal, totalAmount: orderTotal,
slotInfo: order.slot ? { slotInfo: order.slot ? {
time: order.slot.deliveryTime.toISOString(), time: order.slot.deliveryTime.toISOString(),
@ -465,7 +647,7 @@ export const vendorSnippetsRouter = router({
freezeTime: slot.freezeTime.toISOString(), freezeTime: slot.freezeTime.toISOString(),
deliverySequence: slot.deliverySequence, deliverySequence: slot.deliverySequence,
}, },
}; }
}), }),
updateOrderItemPackaging: publicProcedure updateOrderItemPackaging: publicProcedure
@ -473,7 +655,7 @@ export const vendorSnippetsRouter = router({
orderItemId: z.number().int().positive("Valid order item ID required"), orderItemId: z.number().int().positive("Valid order item ID required"),
is_packaged: z.boolean() is_packaged: z.boolean()
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }): Promise<AdminVendorUpdatePackagingResult> => {
const { orderItemId, is_packaged } = input; const { orderItemId, is_packaged } = input;
// Get staff user ID from auth middleware // Get staff user ID from auth middleware
@ -482,6 +664,10 @@ export const vendorSnippetsRouter = router({
// throw new Error("Unauthorized"); // 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 // Check if order item exists and get related data
const orderItem = await db.query.orderItems.findFirst({ const orderItem = await db.query.orderItems.findFirst({
where: eq(orderItems.id, orderItemId), where: eq(orderItems.id, orderItemId),
@ -527,5 +713,12 @@ export const vendorSnippetsRouter = router({
orderItemId, orderItemId,
is_packaged is_packaged
}; };
*/
if (!result.success) {
throw new Error(result.message)
}
return result
}), }),
}); });

View file

@ -68,9 +68,13 @@ export {
// Product // Product
getAllProducts, getAllProducts,
getProductById, getProductById,
deleteProduct,
createProduct, createProduct,
updateProduct, updateProduct,
toggleProductOutOfStock, toggleProductOutOfStock,
updateSlotProducts,
getSlotProductIds,
getSlotsProductIds,
getAllUnits, getAllUnits,
getAllProductTags, getAllProductTags,
getProductReviews, getProductReviews,
@ -81,19 +85,18 @@ export {
deleteProductGroup, deleteProductGroup,
addProductToGroup, addProductToGroup,
removeProductFromGroup, removeProductFromGroup,
updateProductPrices,
} from './src/admin-apis/product'; } from './src/admin-apis/product';
export { export {
// Slots // Slots
getAllSlots, getActiveSlotsWithProducts,
getSlotById, getActiveSlots,
createSlot, getSlotsAfterDate,
updateSlot, getSlotByIdWithRelations,
deleteSlot, createSlotWithRelations,
getSlotProducts, updateSlotWithRelations,
addProductToSlot, deleteSlotById,
removeProductFromSlot,
clearSlotProducts,
updateSlotCapacity, updateSlotCapacity,
getSlotDeliverySequence, getSlotDeliverySequence,
updateSlotDeliverySequence, updateSlotDeliverySequence,
@ -159,6 +162,7 @@ export {
getOrderItemsByOrderIds, getOrderItemsByOrderIds,
getOrderStatusByOrderIds, getOrderStatusByOrderIds,
updateVendorOrderItemPackaging, updateVendorOrderItemPackaging,
getVendorOrders,
} from './src/admin-apis/vendor-snippets'; } from './src/admin-apis/vendor-snippets';
// Note: User API helpers are available in their respective files // Note: User API helpers are available in their respective files

View file

@ -26,6 +26,33 @@ import type {
RefundStatus, RefundStatus,
PaymentStatus, PaymentStatus,
} from '@packages/shared' } 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<typeof orderStatus>
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<AdminOrderRow | null> { export async function updateOrderNotes(orderId: number, adminNotes: string | null): Promise<AdminOrderRow | null> {
const [result] = await db const [result] = await db
@ -147,9 +174,7 @@ export async function getOrderDetails(orderId: number): Promise<AdminOrderDetail
} }
const statusRecord = orderData.orderStatus?.[0] const statusRecord = orderData.orderStatus?.[0]
const orderStatusRecord = statusRecord const orderStatusRecord = statusRecord ? mapOrderStatusRecord(statusRecord) : null
? (statusRecord as AdminOrderStatusRecord)
: null
let status: 'pending' | 'delivered' | 'cancelled' = 'pending' let status: 'pending' | 'delivered' | 'cancelled' = 'pending'
if (orderStatusRecord?.isCancelled) { if (orderStatusRecord?.isCancelled) {
status = 'cancelled' status = 'cancelled'
@ -158,8 +183,8 @@ export async function getOrderDetails(orderId: number): Promise<AdminOrderDetail
} }
const refund = orderData.refunds?.[0] const refund = orderData.refunds?.[0]
const refundStatus = refund?.refundStatus const refundStatus = refund?.refundStatus && isRefundStatus(refund.refundStatus)
? (refund.refundStatus as RefundStatus) ? refund.refundStatus
: null : null
const refundRecord: AdminRefundRecord | null = refund const refundRecord: AdminRefundRecord | null = refund
? { ? {
@ -351,6 +376,8 @@ export async function getSlotOrders(slotId: string): Promise<AdminGetSlotOrdersR
isPackageVerified: item.is_package_verified, isPackageVerified: item.is_package_verified,
})) }))
const paymentMode: 'COD' | 'Online' = order.isCod ? 'COD' : 'Online'
return { return {
id: order.id, id: order.id,
readableId: order.id, readableId: order.id,
@ -370,8 +397,10 @@ export async function getSlotOrders(slotId: string): Promise<AdminGetSlotOrdersR
isPackaged: order.orderItems.every((item) => item.is_packaged) || false, isPackaged: order.orderItems.every((item) => item.is_packaged) || false,
isDelivered: statusRecord?.isDelivered || false, isDelivered: statusRecord?.isDelivered || false,
isCod: order.isCod, isCod: order.isCod,
paymentMode: (order.isCod ? 'COD' : 'Online') as 'COD' | 'Online', paymentMode,
paymentStatus: (statusRecord?.paymentStatus || 'pending') as PaymentStatus, paymentStatus: isPaymentStatus(statusRecord?.paymentStatus || 'pending')
? statusRecord?.paymentStatus || 'pending'
: 'pending',
slotId: order.slotId, slotId: order.slotId,
adminNotes: order.adminNotes, adminNotes: order.adminNotes,
userNotes: order.userNotes, userNotes: order.userNotes,

View file

@ -1,67 +1,265 @@
import { db } from '../db/db_index'; import { db } from '../db/db_index'
import { productInfo, units, specialDeals, productSlots, productTags, productReviews, productGroupInfo, productGroupMembership } from '../db/schema'; import {
import { eq, and, inArray, desc, sql, asc } from 'drizzle-orm'; 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<any[]> { type ProductRow = InferSelectModel<typeof productInfo>
return await db.query.productInfo.findMany({ type UnitRow = InferSelectModel<typeof units>
type StoreRow = InferSelectModel<typeof storeInfo>
type SpecialDealRow = InferSelectModel<typeof specialDeals>
type ProductTagInfoRow = InferSelectModel<typeof productTagInfo>
type ProductTagRow = InferSelectModel<typeof productTags>
type ProductGroupRow = InferSelectModel<typeof productGroupInfo>
type ProductGroupMembershipRow = InferSelectModel<typeof productGroupMembership>
type ProductReviewRow = InferSelectModel<typeof productReviews>
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<AdminProductWithRelations[]> {
const products = await db.query.productInfo.findMany({
orderBy: productInfo.name, orderBy: productInfo.name,
with: { with: {
unit: true, unit: true,
store: 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<any | null> { export async function getProductById(id: number): Promise<AdminProductWithDetails | null> {
return await db.query.productInfo.findFirst({ const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id), where: eq(productInfo.id, id),
with: { with: {
unit: true, unit: true,
store: true,
productSlots: {
with: {
slot: true,
}, },
}, })
specialDeals: true,
productTags: { 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: { with: {
tag: true, 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<any> { export async function deleteProduct(id: number): Promise<AdminProduct | null> {
const [product] = await db.insert(productInfo).values(input).returning(); const [deletedProduct] = await db
return product; .delete(productInfo)
.where(eq(productInfo.id, id))
.returning()
if (!deletedProduct) {
return null
} }
export async function updateProduct(id: number, updates: any): Promise<any> { return mapProduct(deletedProduct)
}
type ProductInfoInsert = InferInsertModel<typeof productInfo>
type ProductInfoUpdate = Partial<ProductInfoInsert>
export async function createProduct(input: ProductInfoInsert): Promise<AdminProduct> {
const [product] = await db.insert(productInfo).values(input).returning()
return mapProduct(product)
}
export async function updateProduct(id: number, updates: ProductInfoUpdate): Promise<AdminProduct | null> {
const [product] = await db.update(productInfo) const [product] = await db.update(productInfo)
.set(updates) .set(updates)
.where(eq(productInfo.id, id)) .where(eq(productInfo.id, id))
.returning(); .returning()
return product; if (!product) {
return null
} }
export async function toggleProductOutOfStock(id: number, isOutOfStock: boolean): Promise<any> { return mapProduct(product)
const [product] = await db.update(productInfo) }
.set({ isOutOfStock })
export async function toggleProductOutOfStock(id: number): Promise<AdminProduct | null> {
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)) .where(eq(productInfo.id, id))
.returning(); .returning()
return product;
if (!updatedProduct) {
return null
} }
export async function getAllUnits(): Promise<any[]> { return mapProduct(updatedProduct)
return await db.query.units.findMany({
orderBy: units.name,
});
} }
export async function getAllProductTags(): Promise<any[]> { export async function updateSlotProducts(slotId: string, productIds: string[]): Promise<AdminUpdateSlotProductsResult> {
return await db.query.productTags.findMany({ 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 getSlotProductIds(slotId: string): Promise<number[]> {
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<AdminUnit[]> {
const allUnits = await db.query.units.findMany({
orderBy: units.shortNotation,
})
return allUnits.map(mapUnit)
}
export async function getAllProductTags(): Promise<AdminProductTagWithProducts[]> {
const tags = await db.query.productTagInfo.findMany({
with: { with: {
products: { products: {
with: { with: {
@ -69,56 +267,242 @@ export async function getAllProductTags(): Promise<any[]> {
}, },
}, },
}, },
}); })
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<any[]> { export async function getSlotsProductIds(slotIds: number[]): Promise<Record<number, number[]>> {
return await db.query.productReviews.findMany({ if (slotIds.length === 0) {
where: eq(productReviews.productId, productId), return {}
with: { }
user: true,
const associations = await db.query.productSlots.findMany({
where: inArray(productSlots.slotId, slotIds),
columns: {
slotId: true,
productId: true,
}, },
orderBy: desc(productReviews.createdAt), })
});
const result: Record<number, number[]> = {}
for (const assoc of associations) {
if (!result[assoc.slotId]) {
result[assoc.slotId] = []
}
result[assoc.slotId].push(assoc.productId)
} }
export async function respondToReview(reviewId: number, adminResponse: string): Promise<void> { slotIds.forEach((slotId) => {
await db.update(productReviews) if (!result[slotId]) {
.set({ adminResponse }) result[slotId] = []
.where(eq(productReviews.id, reviewId)); }
})
return result
} }
export async function getAllProductGroups(): Promise<any[]> { export async function getProductReviews(productId: number, limit: number, offset: number) {
return await db.query.productGroupInfo.findMany({ 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<AdminProductReview | null> {
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: { with: {
products: { memberships: {
with: { with: {
product: true, 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<any> { export async function createProductGroup(
const [group] = await db.insert(productGroupInfo).values({ name }).returning(); groupName: string,
return group; description: string | undefined,
productIds: number[]
): Promise<AdminProductGroupInfo> {
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)
} }
export async function updateProductGroup(id: number, name: string): Promise<any> { return {
const [group] = await db.update(productGroupInfo) id: newGroup.id,
.set({ name }) 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<AdminProductGroupInfo | null> {
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)) .where(eq(productGroupInfo.id, id))
.returning(); .returning()
return group;
if (!updatedGroup) {
return null
} }
export async function deleteProductGroup(id: number): Promise<void> { if (productIds !== undefined) {
await db.delete(productGroupInfo).where(eq(productGroupInfo.id, id)); 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<AdminProductGroupInfo | null> {
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<void> { export async function addProductToGroup(groupId: number, productId: number): Promise<void> {
await db.insert(productGroupMembership).values({ groupId, productId }); await db.insert(productGroupMembership).values({ groupId, productId })
} }
export async function removeProductFromGroup(groupId: number, productId: number): Promise<void> { export async function removeProductFromGroup(groupId: number, productId: number): Promise<void> {
@ -126,5 +510,49 @@ export async function removeProductFromGroup(groupId: number, productId: number)
.where(and( .where(and(
eq(productGroupMembership.groupId, groupId), eq(productGroupMembership.groupId, groupId),
eq(productGroupMembership.productId, productId) 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<Pick<ProductInfoInsert, 'price' | 'marketPrice' | 'flashPrice' | 'isFlashAvailable'>> = {}
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: [] }
} }

View file

@ -1,95 +1,351 @@
import { db } from '../db/db_index'; import { db } from '../db/db_index'
import { deliverySlotInfo, productSlots, productInfo } from '../db/schema'; import {
import { eq, and, inArray, desc } from 'drizzle-orm'; 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<any[]> { type SlotSnippetInput = {
return await db.query.deliverySlotInfo.findMany({ name: string
orderBy: desc(deliverySlotInfo.createdAt), productIds: number[]
validTill?: string
}
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<AdminSlotWithProducts[]> {
const slots = await db.query.deliverySlotInfo
.findMany({
where: eq(deliverySlotInfo.isActive, true),
orderBy: desc(deliverySlotInfo.deliveryTime),
with: { with: {
productSlots: { productSlots: {
with: { with: {
product: true, 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 getSlotById(id: number): Promise<any | null> { export async function getActiveSlots(): Promise<AdminDeliverySlot[]> {
return await db.query.deliverySlotInfo.findFirst({ const slots = await db.query.deliverySlotInfo.findMany({
where: eq(deliverySlotInfo.isActive, true),
})
return slots.map(mapDeliverySlot)
}
export async function getSlotsAfterDate(afterDate: Date): Promise<AdminDeliverySlot[]> {
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<AdminSlotWithProductsAndSnippetsBase | null> {
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, id), where: eq(deliverySlotInfo.id, id),
with: { with: {
productSlots: { productSlots: {
with: { with: {
product: true, product: {
columns: {
id: true,
name: true,
images: true,
}, },
}, },
}, },
}); },
vendorSnippets: true,
},
})
if (!slot) {
return null
} }
export async function createSlot(input: any): Promise<any> { return {
const [slot] = await db.insert(deliverySlotInfo).values(input).returning(); ...mapDeliverySlot(slot),
return 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 updateSlot(id: number, updates: any): Promise<any> { export async function createSlotWithRelations(input: {
const [slot] = await db.update(deliverySlotInfo) deliveryTime: string
.set(updates) freezeTime: string
isActive?: boolean
productIds?: number[]
vendorSnippets?: SlotSnippetInput[]
groupIds?: number[]
}): Promise<AdminSlotCreateResult> {
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 updateSlotWithRelations(input: {
id: number
deliveryTime: string
freezeTime: string
isActive?: boolean
productIds?: number[]
vendorSnippets?: SlotSnippetInput[]
groupIds?: number[]
}): Promise<AdminSlotUpdateResult | null> {
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)) .where(eq(deliverySlotInfo.id, id))
.returning(); .returning()
return slot;
if (!updatedSlot) {
return null
} }
export async function deleteSlot(id: number): Promise<void> { if (productIds !== undefined) {
await db.delete(deliverySlotInfo).where(eq(deliverySlotInfo.id, id)); 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)
}
} }
export async function getSlotProducts(slotId: number): Promise<any[]> { let createdSnippets: AdminVendorSnippet[] = []
return await db.query.productSlots.findMany({ if (snippets && snippets.length > 0) {
where: eq(productSlots.slotId, slotId), for (const snippet of snippets) {
with: { const products = await tx.query.productInfo.findMany({
product: true, 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}"`)
} }
export async function addProductToSlot(slotId: number, productId: number): Promise<void> { const existingSnippet = await tx.query.vendorSnippets.findFirst({
await db.insert(productSlots).values({ slotId, productId }); where: eq(vendorSnippets.snippetCode, snippet.name),
})
if (existingSnippet) {
throw new Error(`Snippet name "${snippet.name}" already exists`)
} }
export async function removeProductFromSlot(slotId: number, productId: number): Promise<void> { const [createdSnippet] = await tx.insert(vendorSnippets).values({
await db.delete(productSlots) snippetCode: snippet.name,
.where(and( slotId: id,
eq(productSlots.slotId, slotId), productIds: snippet.productIds,
eq(productSlots.productId, productId) validTill: snippet.validTill ? new Date(snippet.validTill) : undefined,
)); }).returning()
createdSnippets.push(mapVendorSnippet(createdSnippet))
}
} }
export async function clearSlotProducts(slotId: number): Promise<void> { return {
await db.delete(productSlots).where(eq(productSlots.slotId, slotId)); slot: mapDeliverySlot(updatedSlot),
createdSnippets,
message: 'Slot updated successfully',
}
})
return result
} }
export async function updateSlotCapacity(slotId: number, maxCapacity: number): Promise<any> { export async function deleteSlotById(id: number): Promise<AdminDeliverySlot | null> {
const [slot] = await db.update(deliverySlotInfo) const [deletedSlot] = await db
.set({ maxCapacity }) .update(deliverySlotInfo)
.where(eq(deliverySlotInfo.id, slotId)) .set({ isActive: false })
.returning(); .where(eq(deliverySlotInfo.id, id))
return slot; .returning()
if (!deletedSlot) {
return null
} }
export async function getSlotDeliverySequence(slotId: number): Promise<any | null> { return mapDeliverySlot(deletedSlot)
}
export async function getSlotDeliverySequence(slotId: number): Promise<AdminDeliverySlot | null> {
const slot = await db.query.deliverySlotInfo.findFirst({ const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId), where: eq(deliverySlotInfo.id, slotId),
columns: { })
deliverySequence: true,
}, if (!slot) {
}); return null
return slot?.deliverySequence || null;
} }
export async function updateSlotDeliverySequence(slotId: number, sequence: any): Promise<void> { return mapDeliverySlot(slot)
await db.update(deliverySlotInfo) }
.set({ deliverySequence: sequence })
.where(eq(deliverySlotInfo.id, slotId)); export async function updateSlotDeliverySequence(slotId: number, sequence: unknown) {
const [updatedSlot] = await db
.update(deliverySlotInfo)
.set({ deliverySequence: sequence })
.where(eq(deliverySlotInfo.id, slotId))
.returning({
id: deliverySlotInfo.id,
deliverySequence: deliverySlotInfo.deliverySequence,
})
return updatedSlot || null
}
export async function updateSlotCapacity(slotId: number, isCapacityFull: boolean): Promise<AdminUpdateSlotCapacityResult | null> {
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'}`,
}
} }

View file

@ -6,7 +6,7 @@ export interface StaffUser {
id: number; id: number;
name: string; name: string;
password: string; password: string;
staffRoleId: number; staffRoleId: number | null;
createdAt: Date; createdAt: Date;
} }

View file

@ -9,7 +9,7 @@ export interface Store {
imageUrl: string | null; imageUrl: string | null;
owner: number; owner: number;
createdAt: Date; createdAt: Date;
updatedAt: Date; // updatedAt: Date;
} }
export async function getAllStores(): Promise<any[]> { export async function getAllStores(): Promise<any[]> {
@ -68,7 +68,7 @@ export async function createStore(
imageUrl: newStore.imageUrl, imageUrl: newStore.imageUrl,
owner: newStore.owner, owner: newStore.owner,
createdAt: newStore.createdAt, createdAt: newStore.createdAt,
updatedAt: newStore.updatedAt, // updatedAt: newStore.updatedAt,
}; };
} }
@ -88,7 +88,7 @@ export async function updateStore(
.update(storeInfo) .update(storeInfo)
.set({ .set({
...input, ...input,
updatedAt: new Date(), // updatedAt: new Date(),
}) })
.where(eq(storeInfo.id, id)) .where(eq(storeInfo.id, id))
.returning(); .returning();
@ -118,7 +118,7 @@ export async function updateStore(
imageUrl: updatedStore.imageUrl, imageUrl: updatedStore.imageUrl,
owner: updatedStore.owner, owner: updatedStore.owner,
createdAt: updatedStore.createdAt, createdAt: updatedStore.createdAt,
updatedAt: updatedStore.updatedAt, // updatedAt: updatedStore.updatedAt,
}; };
} }

View file

@ -1,86 +1,152 @@
import { db } from '../db/db_index'; import { db } from '../db/db_index'
import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, orderStatus } from '../db/schema'; import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, orderStatus } from '../db/schema'
import { eq, and, inArray, gt, sql, asc } from 'drizzle-orm'; 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<typeof vendorSnippets>
type DeliverySlotRow = InferSelectModel<typeof deliverySlotInfo>
type ProductRow = InferSelectModel<typeof productInfo>
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<boolean> { export async function checkVendorSnippetExists(snippetCode: string): Promise<boolean> {
const existingSnippet = await db.query.vendorSnippets.findFirst({ const existingSnippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode), where: eq(vendorSnippets.snippetCode, snippetCode),
}); })
return !!existingSnippet; return !!existingSnippet
} }
export async function getVendorSnippetById(id: number): Promise<any | null> { export async function getVendorSnippetById(id: number): Promise<AdminVendorSnippetWithSlot | null> {
return await db.query.vendorSnippets.findFirst({ const snippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.id, id), where: eq(vendorSnippets.id, id),
with: { with: {
slot: true, slot: true,
}, },
}); })
if (!snippet) {
return null
} }
export async function getVendorSnippetByCode(snippetCode: string): Promise<any | null> { return {
return await db.query.vendorSnippets.findFirst({ ...mapVendorSnippet(snippet),
slot: snippet.slot ? mapDeliverySlot(snippet.slot) : null,
}
}
export async function getVendorSnippetByCode(snippetCode: string): Promise<AdminVendorSnippet | null> {
const snippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode), where: eq(vendorSnippets.snippetCode, snippetCode),
}); })
return snippet ? mapVendorSnippet(snippet) : null
} }
export async function getAllVendorSnippets(): Promise<any[]> { export async function getAllVendorSnippets(): Promise<AdminVendorSnippetWithSlot[]> {
return await db.query.vendorSnippets.findMany({ const snippets = await db.query.vendorSnippets.findMany({
with: { with: {
slot: true, slot: true,
}, },
orderBy: (vendorSnippets, { desc }) => [desc(vendorSnippets.createdAt)], orderBy: (vendorSnippets, { desc }) => [desc(vendorSnippets.createdAt)],
}); })
return snippets.map((snippet) => ({
...mapVendorSnippet(snippet),
slot: snippet.slot ? mapDeliverySlot(snippet.slot) : null,
}))
} }
export interface CreateVendorSnippetInput { export async function createVendorSnippet(input: {
snippetCode: string; snippetCode: string
slotId?: number; slotId?: number
productIds: number[]; productIds: number[]
isPermanent: boolean; isPermanent: boolean
validTill?: Date; validTill?: Date
} }): Promise<AdminVendorSnippet> {
export async function createVendorSnippet(input: CreateVendorSnippetInput): Promise<any> {
const [result] = await db.insert(vendorSnippets).values({ const [result] = await db.insert(vendorSnippets).values({
snippetCode: input.snippetCode, snippetCode: input.snippetCode,
slotId: input.slotId, slotId: input.slotId,
productIds: input.productIds, productIds: input.productIds,
isPermanent: input.isPermanent, isPermanent: input.isPermanent,
validTill: input.validTill, validTill: input.validTill,
}).returning(); }).returning()
return result; return mapVendorSnippet(result)
} }
export async function updateVendorSnippet(id: number, updates: any): Promise<any> { export async function updateVendorSnippet(id: number, updates: {
snippetCode?: string
slotId?: number | null
productIds?: number[]
isPermanent?: boolean
validTill?: Date | null
}): Promise<AdminVendorSnippet | null> {
const [result] = await db.update(vendorSnippets) const [result] = await db.update(vendorSnippets)
.set(updates) .set(updates)
.where(eq(vendorSnippets.id, id)) .where(eq(vendorSnippets.id, id))
.returning(); .returning()
return result; return result ? mapVendorSnippet(result) : null
} }
export async function deleteVendorSnippet(id: number): Promise<void> { export async function deleteVendorSnippet(id: number): Promise<AdminVendorSnippet | null> {
await db.delete(vendorSnippets) const [result] = await db.delete(vendorSnippets)
.where(eq(vendorSnippets.id, id)); .where(eq(vendorSnippets.id, id))
.returning()
return result ? mapVendorSnippet(result) : null
} }
export async function getProductsByIds(productIds: number[]): Promise<any[]> { export async function getProductsByIds(productIds: number[]): Promise<AdminVendorSnippetProduct[]> {
return await db.query.productInfo.findMany({ const products = await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds), where: inArray(productInfo.id, productIds),
columns: { id: true, name: true }, columns: { id: true, name: true },
}); })
const prods = products.map(mapProductSummary)
return prods;
} }
export async function getVendorSlotById(slotId: number): Promise<any | null> { export async function getVendorSlotById(slotId: number): Promise<AdminDeliverySlot | null> {
return await db.query.deliverySlotInfo.findFirst({ const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId), where: eq(deliverySlotInfo.id, slotId),
}); })
return slot ? mapDeliverySlot(slot) : null
} }
export async function getVendorOrdersBySlotId(slotId: number): Promise<any[]> { export async function getVendorOrdersBySlotId(slotId: number) {
return await db.query.orders.findMany({ return await db.query.orders.findMany({
where: eq(orders.slotId, slotId), where: eq(orders.slotId, slotId),
with: { with: {
@ -98,10 +164,28 @@ export async function getVendorOrdersBySlotId(slotId: number): Promise<any[]> {
slot: true, slot: true,
}, },
orderBy: (orders, { desc }) => [desc(orders.createdAt)], orderBy: (orders, { desc }) => [desc(orders.createdAt)],
}); })
} }
export async function getOrderItemsByOrderIds(orderIds: number[]): Promise<any[]> { 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({ return await db.query.orderItems.findMany({
where: inArray(orderItems.orderId, orderIds), where: inArray(orderItems.orderId, orderIds),
with: { with: {
@ -111,20 +195,56 @@ export async function getOrderItemsByOrderIds(orderIds: number[]): Promise<any[]
}, },
}, },
}, },
}); })
} }
export async function getOrderStatusByOrderIds(orderIds: number[]): Promise<any[]> { export async function getOrderStatusByOrderIds(orderIds: number[]) {
return await db.query.orderStatus.findMany({ return await db.query.orderStatus.findMany({
where: inArray(orderStatus.orderId, orderIds), where: inArray(orderStatus.orderId, orderIds),
}); })
} }
export async function updateVendorOrderItemPackaging(orderItemId: number, isPackaged: boolean, isPackageVerified: boolean): Promise<void> { export async function updateVendorOrderItemPackaging(
await db.update(orderItems) orderItemId: number,
isPackaged: boolean
): Promise<AdminVendorUpdatePackagingResult> {
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({ .set({
is_packaged: isPackaged, 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 }
} }

View file

@ -78,7 +78,7 @@ export interface Store {
imageUrl: string | null; imageUrl: string | null;
owner: number; owner: number;
createdAt: Date; createdAt: Date;
updatedAt: Date; // updatedAt: Date;
} }
export interface StaffUser { export interface StaffUser {
@ -347,3 +347,367 @@ export interface AdminCancelOrderResult {
userId?: number; userId?: number;
error?: AdminCancelOrderError; 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<number, number[]>;
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<string, number[]>;
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;
}