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,
getOrderStatusByOrderIds,
updateVendorOrderItemPackaging,
getVendorOrders,
// Product methods
getAllProducts,
getProductById,
deleteProduct,
createProduct,
updateProduct,
toggleProductOutOfStock,
updateSlotProducts,
getSlotProductIds,
getSlotsProductIds,
getAllUnits,
getAllProductTags,
getProductReviews,
@ -108,16 +113,15 @@ export {
deleteProductGroup,
addProductToGroup,
removeProductFromGroup,
updateProductPrices,
// Slots methods
getAllSlots,
getSlotById,
createSlot,
updateSlot,
deleteSlot,
getSlotProducts,
addProductToSlot,
removeProductFromSlot,
clearSlotProducts,
getActiveSlotsWithProducts,
getActiveSlots,
getSlotsAfterDate,
getSlotByIdWithRelations,
createSlotWithRelations,
updateSlotWithRelations,
deleteSlotById,
updateSlotCapacity,
getSlotDeliverySequence,
updateSlotDeliverySequence,
@ -164,6 +168,58 @@ export type {
AdminGetAllOrdersResultWithUserId,
AdminRebalanceSlotsResult,
AdminCancelOrderResult,
AdminUnit,
AdminProduct,
AdminProductWithRelations,
AdminProductWithDetails,
AdminProductTagInfo,
AdminProductTagWithProducts,
AdminProductListResponse,
AdminProductResponse,
AdminDeleteProductResult,
AdminToggleOutOfStockResult,
AdminUpdateSlotProductsResult,
AdminSlotProductIdsResult,
AdminSlotsProductIdsResult,
AdminProductReview,
AdminProductReviewWithSignedUrls,
AdminProductReviewsResult,
AdminProductReviewResponse,
AdminProductGroup,
AdminProductGroupsResult,
AdminProductGroupResponse,
AdminProductGroupInfo,
AdminUpdateProductPricesResult,
AdminDeliverySlot,
AdminSlotProductSummary,
AdminSlotWithProducts,
AdminSlotWithProductsAndSnippets,
AdminSlotWithProductsAndSnippetsBase,
AdminSlotsResult,
AdminSlotsListResult,
AdminSlotResult,
AdminSlotCreateResult,
AdminSlotUpdateResult,
AdminSlotDeleteResult,
AdminDeliverySequence,
AdminDeliverySequenceResult,
AdminUpdateDeliverySequenceResult,
AdminUpdateSlotCapacityResult,
AdminVendorSnippet,
AdminVendorSnippetWithAccess,
AdminVendorSnippetWithSlot,
AdminVendorSnippetProduct,
AdminVendorSnippetWithProducts,
AdminVendorSnippetCreateInput,
AdminVendorSnippetUpdateInput,
AdminVendorSnippetDeleteResult,
AdminVendorSnippetOrderProduct,
AdminVendorSnippetOrderSummary,
AdminVendorSnippetOrdersResult,
AdminVendorSnippetOrdersWithSlotResult,
AdminVendorOrderSummary,
AdminUpcomingSlotsResult,
AdminVendorUpdatePackagingResult,
} from '@packages/shared';
export type {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,67 +1,265 @@
import { db } from '../db/db_index';
import { productInfo, units, specialDeals, productSlots, productTags, productReviews, productGroupInfo, productGroupMembership } from '../db/schema';
import { eq, and, inArray, desc, sql, asc } from 'drizzle-orm';
import { db } from '../db/db_index'
import {
productInfo,
units,
specialDeals,
productSlots,
productTags,
productReviews,
productGroupInfo,
productGroupMembership,
productTagInfo,
users,
storeInfo,
} from '../db/schema'
import { and, desc, eq, inArray, sql } from 'drizzle-orm'
import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'
import type {
AdminProduct,
AdminProductGroupInfo,
AdminProductTagWithProducts,
AdminProductReview,
AdminProductWithDetails,
AdminProductWithRelations,
AdminSpecialDeal,
AdminUnit,
AdminUpdateSlotProductsResult,
Store,
} from '@packages/shared'
export async function getAllProducts(): Promise<any[]> {
return await db.query.productInfo.findMany({
type ProductRow = InferSelectModel<typeof productInfo>
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,
with: {
unit: true,
store: true,
},
});
})
return products.map((product) => ({
...mapProduct(product),
unit: mapUnit(product.unit),
store: product.store ? mapStore(product.store) : null,
}))
}
export async function getProductById(id: number): Promise<any | null> {
return await db.query.productInfo.findFirst({
export async function getProductById(id: number): Promise<AdminProductWithDetails | null> {
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
with: {
unit: true,
store: true,
productSlots: {
with: {
slot: true,
},
},
specialDeals: true,
productTags: {
with: {
tag: true,
},
},
},
});
})
if (!product) {
return null
}
const deals = await db.query.specialDeals.findMany({
where: eq(specialDeals.productId, id),
orderBy: specialDeals.quantity,
})
const productTagsData = await db.query.productTags.findMany({
where: eq(productTags.productId, id),
with: {
tag: true,
},
})
return {
...mapProduct(product),
unit: mapUnit(product.unit),
deals: deals.map(mapSpecialDeal),
tags: productTagsData.map((tag) => mapTagInfo(tag.tag)),
}
}
export async function createProduct(input: any): Promise<any> {
const [product] = await db.insert(productInfo).values(input).returning();
return product;
export async function deleteProduct(id: number): Promise<AdminProduct | null> {
const [deletedProduct] = await db
.delete(productInfo)
.where(eq(productInfo.id, id))
.returning()
if (!deletedProduct) {
return null
}
return mapProduct(deletedProduct)
}
export async function updateProduct(id: number, updates: any): Promise<any> {
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)
.set(updates)
.where(eq(productInfo.id, id))
.returning();
return product;
.returning()
if (!product) {
return null
}
return mapProduct(product)
}
export async function toggleProductOutOfStock(id: number, isOutOfStock: boolean): Promise<any> {
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))
.returning();
return product;
.returning()
if (!updatedProduct) {
return null
}
return mapProduct(updatedProduct)
}
export async function getAllUnits(): Promise<any[]> {
return await db.query.units.findMany({
orderBy: units.name,
});
export async function updateSlotProducts(slotId: string, productIds: string[]): Promise<AdminUpdateSlotProductsResult> {
const currentAssociations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, parseInt(slotId)),
columns: {
productId: true,
},
})
const currentProductIds = currentAssociations.map((assoc) => assoc.productId)
const newProductIds = productIds.map((id) => parseInt(id))
const productsToAdd = newProductIds.filter((id) => !currentProductIds.includes(id))
const productsToRemove = currentProductIds.filter((id) => !newProductIds.includes(id))
if (productsToRemove.length > 0) {
await db.delete(productSlots).where(
and(
eq(productSlots.slotId, parseInt(slotId)),
inArray(productSlots.productId, productsToRemove)
)
)
}
if (productsToAdd.length > 0) {
const newAssociations = productsToAdd.map((productId) => ({
productId,
slotId: parseInt(slotId),
}))
await db.insert(productSlots).values(newAssociations)
}
return {
message: 'Slot products updated successfully',
added: productsToAdd.length,
removed: productsToRemove.length,
}
}
export async function getAllProductTags(): Promise<any[]> {
return await db.query.productTags.findMany({
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: {
products: {
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[]> {
return await db.query.productReviews.findMany({
where: eq(productReviews.productId, productId),
with: {
user: true,
export async function getSlotsProductIds(slotIds: number[]): Promise<Record<number, number[]>> {
if (slotIds.length === 0) {
return {}
}
const associations = await db.query.productSlots.findMany({
where: inArray(productSlots.slotId, slotIds),
columns: {
slotId: true,
productId: true,
},
orderBy: desc(productReviews.createdAt),
});
})
const result: Record<number, number[]> = {}
for (const assoc of associations) {
if (!result[assoc.slotId]) {
result[assoc.slotId] = []
}
result[assoc.slotId].push(assoc.productId)
}
slotIds.forEach((slotId) => {
if (!result[slotId]) {
result[slotId] = []
}
})
return result
}
export async function respondToReview(reviewId: number, adminResponse: string): Promise<void> {
await db.update(productReviews)
.set({ adminResponse })
.where(eq(productReviews.id, reviewId));
export async function getProductReviews(productId: number, limit: number, offset: number) {
const reviews = await db
.select({
id: productReviews.id,
reviewBody: productReviews.reviewBody,
ratings: productReviews.ratings,
imageUrls: productReviews.imageUrls,
reviewTime: productReviews.reviewTime,
adminResponse: productReviews.adminResponse,
adminResponseImages: productReviews.adminResponseImages,
userName: users.name,
})
.from(productReviews)
.innerJoin(users, eq(productReviews.userId, users.id))
.where(eq(productReviews.productId, productId))
.orderBy(desc(productReviews.reviewTime))
.limit(limit)
.offset(offset)
const totalCountResult = await db
.select({ count: sql`count(*)` })
.from(productReviews)
.where(eq(productReviews.productId, productId))
const totalCount = Number(totalCountResult[0].count)
const mappedReviews: AdminProductReview[] = reviews.map((review) => ({
id: review.id,
reviewBody: review.reviewBody,
ratings: review.ratings,
imageUrls: review.imageUrls,
reviewTime: review.reviewTime,
adminResponse: review.adminResponse ?? null,
adminResponseImages: review.adminResponseImages,
userName: review.userName ?? null,
}))
return {
reviews: mappedReviews,
totalCount,
}
}
export async function getAllProductGroups(): Promise<any[]> {
return await db.query.productGroupInfo.findMany({
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: {
products: {
memberships: {
with: {
product: true,
},
},
},
});
orderBy: desc(productGroupInfo.createdAt),
})
return groups.map((group) => ({
id: group.id,
groupName: group.groupName,
description: group.description ?? null,
createdAt: group.createdAt,
products: group.memberships.map((membership) => mapProduct(membership.product)),
productCount: group.memberships.length,
memberships: group.memberships
}))
}
export async function createProductGroup(name: string): Promise<any> {
const [group] = await db.insert(productGroupInfo).values({ name }).returning();
return group;
export async function createProductGroup(
groupName: string,
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)
}
return {
id: newGroup.id,
groupName: newGroup.groupName,
description: newGroup.description ?? null,
createdAt: newGroup.createdAt,
}
}
export async function updateProductGroup(id: number, name: string): Promise<any> {
const [group] = await db.update(productGroupInfo)
.set({ name })
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))
.returning();
return group;
.returning()
if (!updatedGroup) {
return null
}
if (productIds !== undefined) {
await db.delete(productGroupMembership).where(eq(productGroupMembership.groupId, id))
if (productIds.length > 0) {
const memberships = productIds.map((productId) => ({
productId,
groupId: id,
}))
await db.insert(productGroupMembership).values(memberships)
}
}
return {
id: updatedGroup.id,
groupName: updatedGroup.groupName,
description: updatedGroup.description ?? null,
createdAt: updatedGroup.createdAt,
}
}
export async function deleteProductGroup(id: number): Promise<void> {
await db.delete(productGroupInfo).where(eq(productGroupInfo.id, id));
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> {
await db.insert(productGroupMembership).values({ groupId, productId });
await db.insert(productGroupMembership).values({ groupId, productId })
}
export async function removeProductFromGroup(groupId: number, productId: number): Promise<void> {
@ -126,5 +510,49 @@ export async function removeProductFromGroup(groupId: number, productId: number)
.where(and(
eq(productGroupMembership.groupId, groupId),
eq(productGroupMembership.productId, productId)
));
))
}
export async function updateProductPrices(updates: Array<{
productId: number
price?: number
marketPrice?: number | null
flashPrice?: number | null
isFlashAvailable?: boolean
}>) {
if (updates.length === 0) {
return { updatedCount: 0, invalidIds: [] }
}
const productIds = updates.map((update) => update.productId)
const existingProducts = await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
columns: { id: true },
})
const existingIds = new Set(existingProducts.map((product) => product.id))
const invalidIds = productIds.filter((id) => !existingIds.has(id))
if (invalidIds.length > 0) {
return { updatedCount: 0, invalidIds }
}
const updatePromises = updates.map((update) => {
const { productId, price, marketPrice, flashPrice, isFlashAvailable } = update
const updateData: Partial<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 { deliverySlotInfo, productSlots, productInfo } from '../db/schema';
import { eq, and, inArray, desc } from 'drizzle-orm';
import { db } from '../db/db_index'
import {
deliverySlotInfo,
productSlots,
productInfo,
vendorSnippets,
productGroupInfo,
} from '../db/schema'
import { and, asc, desc, eq, gt, inArray } from 'drizzle-orm'
import type {
AdminDeliverySlot,
AdminSlotWithProducts,
AdminSlotWithProductsAndSnippetsBase,
AdminSlotCreateResult,
AdminSlotUpdateResult,
AdminVendorSnippet,
AdminSlotProductSummary,
AdminUpdateSlotCapacityResult,
} from '@packages/shared'
export async function getAllSlots(): Promise<any[]> {
return await db.query.deliverySlotInfo.findMany({
orderBy: desc(deliverySlotInfo.createdAt),
with: {
productSlots: {
with: {
product: true,
},
},
},
});
type SlotSnippetInput = {
name: string
productIds: number[]
validTill?: string
}
export async function getSlotById(id: number): Promise<any | null> {
return await db.query.deliverySlotInfo.findFirst({
const getStringArray = (value: unknown): string[] | null => {
if (!Array.isArray(value)) return null
return value.map((item) => String(item))
}
const getNumberArray = (value: unknown): number[] => {
if (!Array.isArray(value)) return []
return value.map((item) => Number(item))
}
const mapDeliverySlot = (slot: typeof deliverySlotInfo.$inferSelect): AdminDeliverySlot => ({
id: slot.id,
deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime,
isActive: slot.isActive,
isFlash: slot.isFlash,
isCapacityFull: slot.isCapacityFull,
deliverySequence: slot.deliverySequence,
groupIds: slot.groupIds,
})
const mapSlotProductSummary = (product: { id: number; name: string; images: unknown }): AdminSlotProductSummary => ({
id: product.id,
name: product.name,
images: getStringArray(product.images),
})
const mapVendorSnippet = (snippet: typeof vendorSnippets.$inferSelect): AdminVendorSnippet => ({
id: snippet.id,
snippetCode: snippet.snippetCode,
slotId: snippet.slotId ?? null,
productIds: snippet.productIds,
isPermanent: snippet.isPermanent,
validTill: snippet.validTill ?? null,
createdAt: snippet.createdAt,
})
export async function getActiveSlotsWithProducts(): Promise<AdminSlotWithProducts[]> {
const slots = await db.query.deliverySlotInfo
.findMany({
where: eq(deliverySlotInfo.isActive, true),
orderBy: desc(deliverySlotInfo.deliveryTime),
with: {
productSlots: {
with: {
product: {
columns: {
id: true,
name: true,
images: true,
},
},
},
},
},
})
return slots.map((slot) => ({
...mapDeliverySlot(slot),
deliverySequence: getNumberArray(slot.deliverySequence),
products: slot.productSlots.map((ps) => mapSlotProductSummary(ps.product)),
}))
}
export async function getActiveSlots(): Promise<AdminDeliverySlot[]> {
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),
with: {
productSlots: {
with: {
product: true,
product: {
columns: {
id: true,
name: true,
images: true,
},
},
},
},
vendorSnippets: true,
},
});
})
if (!slot) {
return null
}
return {
...mapDeliverySlot(slot),
deliverySequence: getNumberArray(slot.deliverySequence),
groupIds: getNumberArray(slot.groupIds),
products: slot.productSlots.map((ps) => mapSlotProductSummary(ps.product)),
vendorSnippets: slot.vendorSnippets.map(mapVendorSnippet),
}
}
export async function createSlot(input: any): Promise<any> {
const [slot] = await db.insert(deliverySlotInfo).values(input).returning();
return slot;
export async function createSlotWithRelations(input: {
deliveryTime: string
freezeTime: string
isActive?: boolean
productIds?: number[]
vendorSnippets?: SlotSnippetInput[]
groupIds?: number[]
}): Promise<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 updateSlot(id: number, updates: any): Promise<any> {
const [slot] = await db.update(deliverySlotInfo)
.set(updates)
export async function updateSlotWithRelations(input: {
id: number
deliveryTime: string
freezeTime: string
isActive?: boolean
productIds?: number[]
vendorSnippets?: SlotSnippetInput[]
groupIds?: number[]
}): Promise<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))
.returning()
if (!updatedSlot) {
return null
}
if (productIds !== undefined) {
await tx.delete(productSlots).where(eq(productSlots.slotId, id))
if (productIds.length > 0) {
const associations = productIds.map((productId) => ({
productId,
slotId: id,
}))
await tx.insert(productSlots).values(associations)
}
}
let createdSnippets: AdminVendorSnippet[] = []
if (snippets && snippets.length > 0) {
for (const snippet of snippets) {
const products = await tx.query.productInfo.findMany({
where: inArray(productInfo.id, snippet.productIds),
})
if (products.length !== snippet.productIds.length) {
throw new Error(`One or more invalid product IDs in snippet "${snippet.name}"`)
}
const existingSnippet = await tx.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippet.name),
})
if (existingSnippet) {
throw new Error(`Snippet name "${snippet.name}" already exists`)
}
const [createdSnippet] = await tx.insert(vendorSnippets).values({
snippetCode: snippet.name,
slotId: id,
productIds: snippet.productIds,
validTill: snippet.validTill ? new Date(snippet.validTill) : undefined,
}).returning()
createdSnippets.push(mapVendorSnippet(createdSnippet))
}
}
return {
slot: mapDeliverySlot(updatedSlot),
createdSnippets,
message: 'Slot updated successfully',
}
})
return result
}
export async function deleteSlotById(id: number): Promise<AdminDeliverySlot | null> {
const [deletedSlot] = await db
.update(deliverySlotInfo)
.set({ isActive: false })
.where(eq(deliverySlotInfo.id, id))
.returning();
return slot;
.returning()
if (!deletedSlot) {
return null
}
return mapDeliverySlot(deletedSlot)
}
export async function deleteSlot(id: number): Promise<void> {
await db.delete(deliverySlotInfo).where(eq(deliverySlotInfo.id, id));
}
export async function getSlotProducts(slotId: number): Promise<any[]> {
return await db.query.productSlots.findMany({
where: eq(productSlots.slotId, slotId),
with: {
product: true,
},
});
}
export async function addProductToSlot(slotId: number, productId: number): Promise<void> {
await db.insert(productSlots).values({ slotId, productId });
}
export async function removeProductFromSlot(slotId: number, productId: number): Promise<void> {
await db.delete(productSlots)
.where(and(
eq(productSlots.slotId, slotId),
eq(productSlots.productId, productId)
));
}
export async function clearSlotProducts(slotId: number): Promise<void> {
await db.delete(productSlots).where(eq(productSlots.slotId, slotId));
}
export async function updateSlotCapacity(slotId: number, maxCapacity: number): Promise<any> {
const [slot] = await db.update(deliverySlotInfo)
.set({ maxCapacity })
.where(eq(deliverySlotInfo.id, slotId))
.returning();
return slot;
}
export async function getSlotDeliverySequence(slotId: number): Promise<any | null> {
export async function getSlotDeliverySequence(slotId: number): Promise<AdminDeliverySlot | null> {
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId),
columns: {
deliverySequence: true,
},
});
return slot?.deliverySequence || null;
})
if (!slot) {
return null
}
return mapDeliverySlot(slot)
}
export async function updateSlotDeliverySequence(slotId: number, sequence: any): Promise<void> {
await db.update(deliverySlotInfo)
export async function updateSlotDeliverySequence(slotId: number, sequence: unknown) {
const [updatedSlot] = await db
.update(deliverySlotInfo)
.set({ deliverySequence: sequence })
.where(eq(deliverySlotInfo.id, slotId));
.where(eq(deliverySlotInfo.id, slotId))
.returning({
id: deliverySlotInfo.id,
deliverySequence: deliverySlotInfo.deliverySequence,
})
return updatedSlot || null
}
export async function updateSlotCapacity(slotId: number, isCapacityFull: boolean): Promise<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;
name: string;
password: string;
staffRoleId: number;
staffRoleId: number | null;
createdAt: Date;
}

View file

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

View file

@ -1,86 +1,152 @@
import { db } from '../db/db_index';
import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, orderStatus } from '../db/schema';
import { eq, and, inArray, gt, sql, asc } from 'drizzle-orm';
import { db } from '../db/db_index'
import { vendorSnippets, deliverySlotInfo, productInfo, orders, orderItems, orderStatus } from '../db/schema'
import { desc, eq, inArray } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm'
import type {
AdminDeliverySlot,
AdminVendorSnippet,
AdminVendorSnippetWithSlot,
AdminVendorSnippetProduct,
AdminVendorUpdatePackagingResult,
} from '@packages/shared'
type VendorSnippetRow = InferSelectModel<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> {
const existingSnippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode),
});
return !!existingSnippet;
})
return !!existingSnippet
}
export async function getVendorSnippetById(id: number): Promise<any | null> {
return await db.query.vendorSnippets.findFirst({
export async function getVendorSnippetById(id: number): Promise<AdminVendorSnippetWithSlot | null> {
const snippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.id, id),
with: {
slot: true,
},
});
})
if (!snippet) {
return null
}
return {
...mapVendorSnippet(snippet),
slot: snippet.slot ? mapDeliverySlot(snippet.slot) : null,
}
}
export async function getVendorSnippetByCode(snippetCode: string): Promise<any | null> {
return await db.query.vendorSnippets.findFirst({
export async function getVendorSnippetByCode(snippetCode: string): Promise<AdminVendorSnippet | null> {
const snippet = await db.query.vendorSnippets.findFirst({
where: eq(vendorSnippets.snippetCode, snippetCode),
});
})
return snippet ? mapVendorSnippet(snippet) : null
}
export async function getAllVendorSnippets(): Promise<any[]> {
return await db.query.vendorSnippets.findMany({
export async function getAllVendorSnippets(): Promise<AdminVendorSnippetWithSlot[]> {
const snippets = await db.query.vendorSnippets.findMany({
with: {
slot: true,
},
orderBy: (vendorSnippets, { desc }) => [desc(vendorSnippets.createdAt)],
});
})
return snippets.map((snippet) => ({
...mapVendorSnippet(snippet),
slot: snippet.slot ? mapDeliverySlot(snippet.slot) : null,
}))
}
export interface CreateVendorSnippetInput {
snippetCode: string;
slotId?: number;
productIds: number[];
isPermanent: boolean;
validTill?: Date;
}
export async function createVendorSnippet(input: CreateVendorSnippetInput): Promise<any> {
export async function createVendorSnippet(input: {
snippetCode: string
slotId?: number
productIds: number[]
isPermanent: boolean
validTill?: Date
}): Promise<AdminVendorSnippet> {
const [result] = await db.insert(vendorSnippets).values({
snippetCode: input.snippetCode,
slotId: input.slotId,
productIds: input.productIds,
isPermanent: input.isPermanent,
validTill: input.validTill,
}).returning();
}).returning()
return result;
return mapVendorSnippet(result)
}
export async function updateVendorSnippet(id: number, updates: any): Promise<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)
.set(updates)
.where(eq(vendorSnippets.id, id))
.returning();
.returning()
return result;
return result ? mapVendorSnippet(result) : null
}
export async function deleteVendorSnippet(id: number): Promise<void> {
await db.delete(vendorSnippets)
.where(eq(vendorSnippets.id, id));
export async function deleteVendorSnippet(id: number): Promise<AdminVendorSnippet | null> {
const [result] = await db.delete(vendorSnippets)
.where(eq(vendorSnippets.id, id))
.returning()
return result ? mapVendorSnippet(result) : null
}
export async function getProductsByIds(productIds: number[]): Promise<any[]> {
return await db.query.productInfo.findMany({
export async function getProductsByIds(productIds: number[]): Promise<AdminVendorSnippetProduct[]> {
const products = await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
columns: { id: true, name: true },
});
})
const prods = products.map(mapProductSummary)
return prods;
}
export async function getVendorSlotById(slotId: number): Promise<any | null> {
return await db.query.deliverySlotInfo.findFirst({
export async function getVendorSlotById(slotId: number): Promise<AdminDeliverySlot | null> {
const slot = await db.query.deliverySlotInfo.findFirst({
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({
where: eq(orders.slotId, slotId),
with: {
@ -98,10 +164,28 @@ export async function getVendorOrdersBySlotId(slotId: number): Promise<any[]> {
slot: true,
},
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({
where: inArray(orderItems.orderId, orderIds),
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({
where: inArray(orderStatus.orderId, orderIds),
});
})
}
export async function updateVendorOrderItemPackaging(orderItemId: number, isPackaged: boolean, isPackageVerified: boolean): Promise<void> {
await db.update(orderItems)
export async function updateVendorOrderItemPackaging(
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({
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;
owner: number;
createdAt: Date;
updatedAt: Date;
// updatedAt: Date;
}
export interface StaffUser {
@ -347,3 +347,367 @@ export interface AdminCancelOrderResult {
userId?: number;
error?: AdminCancelOrderError;
}
export interface AdminUnit {
id: number;
shortNotation: string;
fullName: string;
}
export interface AdminProduct {
id: number;
name: string;
shortDescription: string | null;
longDescription: string | null;
unitId: number;
price: string;
marketPrice: string | null;
images: string[] | null;
isOutOfStock: boolean;
isSuspended: boolean;
isFlashAvailable: boolean;
flashPrice: string | null;
createdAt: Date;
incrementStep: number;
productQuantity: number;
storeId: number | null;
}
export interface AdminProductWithRelations extends AdminProduct {
unit: AdminUnit;
store: Store | null;
}
export interface AdminProductTagInfo {
id: number;
tagName: string;
tagDescription: string | null;
imageUrl: string | null;
isDashboardTag: boolean;
relatedStores: unknown;
createdAt: Date;
}
export interface AdminProductTagAssignment {
productId: number;
tagId: number;
assignedAt: Date;
product: AdminProduct;
}
export interface AdminProductTagWithProducts extends AdminProductTagInfo {
products: AdminProductTagAssignment[];
}
export interface AdminSpecialDeal {
id: number;
productId: number;
quantity: string;
price: string;
validTill: Date;
}
export interface AdminProductWithDetails extends AdminProduct {
unit: AdminUnit;
deals: AdminSpecialDeal[];
tags: AdminProductTagInfo[];
}
export interface AdminProductListResponse {
products: AdminProductWithRelations[];
count: number;
}
export interface AdminProductResponse {
product: AdminProductWithDetails;
}
export interface AdminDeleteProductResult {
message: string;
}
export interface AdminToggleOutOfStockResult {
product: AdminProduct;
message: string;
}
export interface AdminUpdateSlotProductsResult {
message: string;
added: number;
removed: number;
}
export interface AdminSlotProductIdsResult {
productIds: number[];
}
export type AdminSlotsProductIdsResult = Record<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;
}