This commit is contained in:
shafi54 2026-05-11 20:57:13 +05:30
parent 5bacc25627
commit 5d7f6b7aab
23 changed files with 655 additions and 459 deletions

View file

@ -42,19 +42,19 @@ async function transformSlotToStoreSlot(slot: SlotWithProductsData): Promise<Slo
freezeTime: slot.freezeTime, freezeTime: slot.freezeTime,
isActive: slot.isActive, isActive: slot.isActive,
isCapacityFull: slot.isCapacityFull, isCapacityFull: slot.isCapacityFull,
products: slot.productSlots.map((productSlot) => ({ products: slot.products.map((product) => ({
id: productSlot.product.id, id: product.id,
name: productSlot.product.name, name: product.name,
productQuantity: productSlot.product.productQuantity, productQuantity: product.productQuantity,
shortDescription: productSlot.product.shortDescription, shortDescription: product.shortDescription,
price: productSlot.product.price.toString(), price: product.price.toString(),
marketPrice: productSlot.product.marketPrice?.toString() || null, marketPrice: product.marketPrice?.toString() || null,
unit: productSlot.product.unit?.shortNotation || null, unit: product.unit?.shortNotation || null,
images: scaffoldAssetUrl( images: scaffoldAssetUrl(
(productSlot.product.images as string[]) || [] (product.images as string[]) || []
), ),
isOutOfStock: productSlot.product.isOutOfStock, isOutOfStock: product.isOutOfStock,
storeId: productSlot.product.storeId, storeId: product.storeId,
nextDeliveryDate: slot.deliveryTime, nextDeliveryDate: slot.deliveryTime,
})), })),
} }
@ -118,19 +118,19 @@ export async function initializeSlotStore(): Promise<void> {
isActive: slot.isActive, isActive: slot.isActive,
isCapacityFull: slot.isCapacityFull, isCapacityFull: slot.isCapacityFull,
products: await Promise.all( products: await Promise.all(
slot.productSlots.map(async (productSlot) => ({ slot.products.map(async (product) => ({
id: productSlot.product.id, id: product.id,
name: productSlot.product.name, name: product.name,
productQuantity: productSlot.product.productQuantity, productQuantity: product.productQuantity,
shortDescription: productSlot.product.shortDescription, shortDescription: product.shortDescription,
price: productSlot.product.price.toString(), price: product.price.toString(),
marketPrice: productSlot.product.marketPrice?.toString() || null, marketPrice: product.marketPrice?.toString() || null,
unit: productSlot.product.unit?.shortNotation || null, unit: product.unit?.shortNotation || null,
images: scaffoldAssetUrl( images: scaffoldAssetUrl(
(productSlot.product.images as string[]) || [] (product.images as string[]) || []
), ),
isOutOfStock: productSlot.product.isOutOfStock, isOutOfStock: product.isOutOfStock,
storeId: productSlot.product.storeId, storeId: product.storeId,
nextDeliveryDate: slot.deliveryTime, nextDeliveryDate: slot.deliveryTime,
})) }))
), ),
@ -229,7 +229,7 @@ export async function getProductSlots(productId: number): Promise<SlotInfo[]> {
const productSlots: SlotInfo[] = [] const productSlots: SlotInfo[] = []
for (const slot of slots) { for (const slot of slots) {
const hasProduct = slot.productSlots.some(ps => ps.product.id === productId) const hasProduct = slot.products.some(p => p.id === productId)
if (hasProduct) { if (hasProduct) {
productSlots.push(extractSlotInfo(slot)) productSlots.push(extractSlotInfo(slot))
} }
@ -272,8 +272,8 @@ export async function getAllProductsSlots(): Promise<Record<number, SlotInfo[]>>
for (const slot of slots) { for (const slot of slots) {
const slotInfo = extractSlotInfo(slot) const slotInfo = extractSlotInfo(slot)
for (const productSlot of slot.productSlots) { for (const product of slot.products) {
const productId = productSlot.product.id const productId = product.id
if (!result[productId]) { if (!result[productId]) {
result[productId] = [] result[productId] = []
} }
@ -322,8 +322,8 @@ export async function getMultipleProductsSlots(
for (const slot of slots) { for (const slot of slots) {
const slotInfo = extractSlotInfo(slot) const slotInfo = extractSlotInfo(slot)
for (const productSlot of slot.productSlots) { for (const product of slot.products) {
const pid = productSlot.product.id const pid = product.id
if (productIdSet.has(pid) && !slot.isCapacityFull) { if (productIdSet.has(pid) && !slot.isCapacityFull) {
result[pid].push(slotInfo) result[pid].push(slotInfo)
} }

View file

@ -18,6 +18,7 @@ bindings = [
[[migrations]] [[migrations]]
tag = "cache-creator-v1" tag = "cache-creator-v1"
new_classes = ["CacheCreator"] new_classes = ["CacheCreator"]
migrations_dir = "../../packages/db_helper_sqlite/drizzle/"
[[queues.producers]] [[queues.producers]]
binding = "NOTIF_QUEUE" binding = "NOTIF_QUEUE"

View file

@ -0,0 +1,21 @@
-- Migration: Replace product_slots junction table with JSONB array in delivery_slot_info
-- Step 1: Add product_ids column to delivery_slot_info
ALTER TABLE delivery_slot_info ADD COLUMN product_ids JSONB DEFAULT '[]';
-- Step 2: Migrate data from product_slots to product_ids JSONB array
UPDATE delivery_slot_info
SET product_ids = COALESCE(
(SELECT jsonb_agg(product_id ORDER BY product_id)
FROM product_slots
WHERE product_slots.slot_id = delivery_slot_info.id),
'[]'::jsonb
);
-- Step 3: Drop the old product_slots table
DROP TABLE IF EXISTS product_slots;
-- Note: After running this migration, update the schema.ts file to:
-- 1. Add productIds: jsonb('product_ids').$defaultFn(() => []) to deliverySlotInfo
-- 2. Remove the productSlots table definition
-- 3. Remove productSlotsRelations
-- 4. Update deliverySlotInfoRelations to remove productSlots reference

View file

@ -3,7 +3,7 @@ import {
productInfo, productInfo,
units, units,
specialDeals, specialDeals,
productSlots, deliverySlotInfo,
productTags, productTags,
productReviews, productReviews,
productGroupInfo, productGroupInfo,
@ -203,37 +203,25 @@ export async function toggleProductOutOfStock(id: number): Promise<AdminProduct
} }
export async function updateSlotProducts(slotId: string, productIds: string[]): Promise<AdminUpdateSlotProductsResult> { export async function updateSlotProducts(slotId: string, productIds: string[]): Promise<AdminUpdateSlotProductsResult> {
const currentAssociations = await db.query.productSlots.findMany({ const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(productSlots.slotId, parseInt(slotId)), where: eq(deliverySlotInfo.id, parseInt(slotId)),
columns: {
productId: true,
},
}) })
const currentProductIds = currentAssociations.map((assoc) => assoc.productId) if (!slot) {
throw new Error(`Slot ${slotId} not found`)
}
const currentProductIds = slot.productIds || []
const newProductIds = productIds.map((id) => parseInt(id)) const newProductIds = productIds.map((id) => parseInt(id))
// Simply update the productIds array
await db.update(deliverySlotInfo)
.set({ productIds: newProductIds })
.where(eq(deliverySlotInfo.id, parseInt(slotId)))
const productsToAdd = newProductIds.filter((id) => !currentProductIds.includes(id)) const productsToAdd = newProductIds.filter((id) => !currentProductIds.includes(id))
const productsToRemove = currentProductIds.filter((id) => !newProductIds.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 { return {
message: 'Slot products updated successfully', message: 'Slot products updated successfully',
added: productsToAdd.length, added: productsToAdd.length,
@ -242,14 +230,11 @@ export async function updateSlotProducts(slotId: string, productIds: string[]):
} }
export async function getSlotProductIds(slotId: string): Promise<number[]> { export async function getSlotProductIds(slotId: string): Promise<number[]> {
const associations = await db.query.productSlots.findMany({ const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(productSlots.slotId, parseInt(slotId)), where: eq(deliverySlotInfo.id, parseInt(slotId)),
columns: {
productId: true,
},
}) })
return associations.map((assoc) => assoc.productId) return slot?.productIds || []
} }
export async function getAllUnits(): Promise<AdminUnit[]> { export async function getAllUnits(): Promise<AdminUnit[]> {
@ -307,20 +292,13 @@ export async function getSlotsProductIds(slotIds: number[]): Promise<Record<numb
return {} return {}
} }
const associations = await db.query.productSlots.findMany({ const slots = await db.query.deliverySlotInfo.findMany({
where: inArray(productSlots.slotId, slotIds), where: inArray(deliverySlotInfo.id, slotIds),
columns: {
slotId: true,
productId: true,
},
}) })
const result: Record<number, number[]> = {} const result: Record<number, number[]> = {}
for (const assoc of associations) { for (const slot of slots) {
if (!result[assoc.slotId]) { result[slot.id] = slot.productIds || []
result[assoc.slotId] = []
}
result[assoc.slotId].push(assoc.productId)
} }
slotIds.forEach((slotId) => { slotIds.forEach((slotId) => {

View file

@ -1,12 +1,11 @@
import { db } from '../db/db_index' import { db } from '../db/db_index'
import { import {
deliverySlotInfo, deliverySlotInfo,
productSlots,
productInfo, productInfo,
vendorSnippets, vendorSnippets,
productGroupInfo, productGroupInfo,
} from '../db/schema' } from '../db/schema'
import { and, asc, desc, eq, gt, inArray, lt } from 'drizzle-orm' import { and, asc, desc, eq, gt, inArray } from 'drizzle-orm'
import type { import type {
AdminDeliverySlot, AdminDeliverySlot,
AdminSlotWithProducts, AdminSlotWithProducts,
@ -67,25 +66,35 @@ export async function getActiveSlotsWithProducts(limit: number = 20): Promise<Ad
where: eq(deliverySlotInfo.isActive, true), where: eq(deliverySlotInfo.isActive, true),
orderBy: desc(deliverySlotInfo.deliveryTime), orderBy: desc(deliverySlotInfo.deliveryTime),
limit, limit,
with: {
productSlots: {
with: {
product: {
columns: {
id: true,
name: true,
images: true,
},
},
},
},
},
}) })
// Get all unique product IDs from all slots
const allProductIds = new Set<number>()
for (const slot of slots) {
for (const productId of (slot.productIds || [])) {
allProductIds.add(productId)
}
}
// Fetch all products in one query
const productIdsArray = Array.from(allProductIds)
const productsData = productIdsArray.length > 0
? await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIdsArray),
columns: { id: true, name: true, images: true },
})
: []
// Create a map for quick lookup
const productMap = new Map(productsData.map(p => [p.id, p]))
return slots.map((slot) => ({ return slots.map((slot) => ({
...mapDeliverySlot(slot), ...mapDeliverySlot(slot),
deliverySequence: getNumberArray(slot.deliverySequence), deliverySequence: getNumberArray(slot.deliverySequence),
products: slot.productSlots.map((ps) => mapSlotProductSummary(ps.product)), products: (slot.productIds || [])
.map(productId => productMap.get(productId))
.filter((p): p is NonNullable<typeof p> => p != null)
.map(product => mapSlotProductSummary(product)),
})) }))
} }
@ -104,8 +113,11 @@ export async function staleSlotsCleanup(): Promise<number> {
// Get the threshold (the 20th most recent slot ID - minimum of the 20) // Get the threshold (the 20th most recent slot ID - minimum of the 20)
const threshold = Math.min(...recentSlots.map((s) => s.id)) const threshold = Math.min(...recentSlots.map((s) => s.id))
// Delete product_slots for all slots older than threshold // Clear productIds for all slots older than threshold
const result = await db.delete(productSlots).where(lt(productSlots.slotId, threshold)) const result = await db
.update(deliverySlotInfo)
.set({ productIds: [] })
.where(eq(deliverySlotInfo.id, threshold))
return result.rowCount || 0 return result.rowCount || 0
} }
@ -134,17 +146,6 @@ export async function getSlotByIdWithRelations(id: number): Promise<AdminSlotWit
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: {
productSlots: {
with: {
product: {
columns: {
id: true,
name: true,
images: true,
},
},
},
},
vendorSnippets: true, vendorSnippets: true,
}, },
}) })
@ -153,11 +154,20 @@ export async function getSlotByIdWithRelations(id: number): Promise<AdminSlotWit
return null return null
} }
// Fetch products for this slot
const productIds = slot.productIds || []
const productsData = productIds.length > 0
? await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
columns: { id: true, name: true, images: true },
})
: []
return { return {
...mapDeliverySlot(slot), ...mapDeliverySlot(slot),
deliverySequence: getNumberArray(slot.deliverySequence), deliverySequence: getNumberArray(slot.deliverySequence),
groupIds: getNumberArray(slot.groupIds), groupIds: getNumberArray(slot.groupIds),
products: slot.productSlots.map((ps) => mapSlotProductSummary(ps.product)), products: productsData.map(product => mapSlotProductSummary(product)),
vendorSnippets: slot.vendorSnippets.map(mapVendorSnippet), vendorSnippets: slot.vendorSnippets.map(mapVendorSnippet),
} }
} }
@ -180,17 +190,10 @@ export async function createSlotWithRelations(input: {
freezeTime: new Date(freezeTime), freezeTime: new Date(freezeTime),
isActive: isActive !== undefined ? isActive : true, isActive: isActive !== undefined ? isActive : true,
groupIds: groupIds !== undefined ? groupIds : [], groupIds: groupIds !== undefined ? groupIds : [],
productIds: productIds !== undefined ? productIds : [],
}) })
.returning() .returning()
if (productIds && productIds.length > 0) {
const associations = productIds.map((productId) => ({
productId,
slotId: newSlot.id,
}))
await tx.insert(productSlots).values(associations)
}
let createdSnippets: AdminVendorSnippet[] = [] let createdSnippets: AdminVendorSnippet[] = []
if (snippets && snippets.length > 0) { if (snippets && snippets.length > 0) {
for (const snippet of snippets) { for (const snippet of snippets) {
@ -257,6 +260,7 @@ export async function updateSlotWithRelations(input: {
freezeTime: new Date(freezeTime), freezeTime: new Date(freezeTime),
isActive: isActive !== undefined ? isActive : true, isActive: isActive !== undefined ? isActive : true,
groupIds: validGroupIds !== undefined ? validGroupIds : [], groupIds: validGroupIds !== undefined ? validGroupIds : [],
...(productIds !== undefined && { productIds }),
}) })
.where(eq(deliverySlotInfo.id, id)) .where(eq(deliverySlotInfo.id, id))
.returning() .returning()
@ -265,18 +269,6 @@ export async function updateSlotWithRelations(input: {
return null 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[] = [] let createdSnippets: AdminVendorSnippet[] = []
if (snippets && snippets.length > 0) { if (snippets && snippets.length > 0) {
for (const snippet of snippets) { for (const snippet of snippets) {

View file

@ -195,6 +195,7 @@ export const deliverySlotInfo = mf.table('delivery_slot_info', {
isCapacityFull: boolean('is_capacity_full').notNull().default(false), isCapacityFull: boolean('is_capacity_full').notNull().default(false),
deliverySequence: jsonb('delivery_sequence').$defaultFn(() => {}), deliverySequence: jsonb('delivery_sequence').$defaultFn(() => {}),
groupIds: jsonb('group_ids').$defaultFn(() => []), groupIds: jsonb('group_ids').$defaultFn(() => []),
productIds: jsonb('product_ids').$defaultFn(() => []),
}); });
export const vendorSnippets = mf.table('vendor_snippets', { export const vendorSnippets = mf.table('vendor_snippets', {
@ -211,13 +212,6 @@ export const vendorSnippetsRelations = relations(vendorSnippets, ({ one }) => ({
slot: one(deliverySlotInfo, { fields: [vendorSnippets.slotId], references: [deliverySlotInfo.id] }), slot: one(deliverySlotInfo, { fields: [vendorSnippets.slotId], references: [deliverySlotInfo.id] }),
})); }));
export const productSlots = mf.table('product_slots', {
productId: integer('product_id').notNull().references(() => productInfo.id),
slotId: integer('slot_id').notNull().references(() => deliverySlotInfo.id),
}, (t) => ({
pk: unique('product_slot_pk').on(t.productId, t.slotId),
}));
export const specialDeals = mf.table('special_deals', { export const specialDeals = mf.table('special_deals', {
id: integer().primaryKey().generatedAlwaysAsIdentity(), id: integer().primaryKey().generatedAlwaysAsIdentity(),
productId: integer('product_id').notNull().references(() => productInfo.id), productId: integer('product_id').notNull().references(() => productInfo.id),
@ -509,7 +503,6 @@ export const unitsRelations = relations(units, ({ many }) => ({
export const productInfoRelations = relations(productInfo, ({ one, many }) => ({ export const productInfoRelations = relations(productInfo, ({ one, many }) => ({
unit: one(units, { fields: [productInfo.unitId], references: [units.id] }), unit: one(units, { fields: [productInfo.unitId], references: [units.id] }),
store: one(storeInfo, { fields: [productInfo.storeId], references: [storeInfo.id] }), store: one(storeInfo, { fields: [productInfo.storeId], references: [storeInfo.id] }),
productSlots: many(productSlots),
specialDeals: many(specialDeals), specialDeals: many(specialDeals),
orderItems: many(orderItems), orderItems: many(orderItems),
cartItems: many(cartItems), cartItems: many(cartItems),
@ -529,16 +522,10 @@ export const productTagsRelations = relations(productTags, ({ one }) => ({
})); }));
export const deliverySlotInfoRelations = relations(deliverySlotInfo, ({ many }) => ({ export const deliverySlotInfoRelations = relations(deliverySlotInfo, ({ many }) => ({
productSlots: many(productSlots),
orders: many(orders), orders: many(orders),
vendorSnippets: many(vendorSnippets), vendorSnippets: many(vendorSnippets),
})); }));
export const productSlotsRelations = relations(productSlots, ({ one }) => ({
product: one(productInfo, { fields: [productSlots.productId], references: [productInfo.id] }),
slot: one(deliverySlotInfo, { fields: [productSlots.slotId], references: [deliverySlotInfo.id] }),
}));
export const specialDealsRelations = relations(specialDeals, ({ one }) => ({ export const specialDealsRelations = relations(specialDeals, ({ one }) => ({
product: one(productInfo, { fields: [specialDeals.productId], references: [productInfo.id] }), product: one(productInfo, { fields: [specialDeals.productId], references: [productInfo.id] }),
})); }));

View file

@ -1,5 +1,5 @@
import { db } from "@/src/db/db_index" import { db } from "@/src/db/db_index"
import { units, productInfo, deliverySlotInfo, productSlots, keyValStore, staffRoles, staffPermissions, staffRolePermissions } from "@/src/db/schema" import { units, productInfo, deliverySlotInfo, keyValStore, staffRoles, staffPermissions, staffRolePermissions } from "@/src/db/schema"
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { minOrderValue, deliveryCharge } from '@/src/lib/env-exporter' import { minOrderValue, deliveryCharge } from '@/src/lib/env-exporter'
import { CONST_KEYS } from '@/src/lib/const-keys' import { CONST_KEYS } from '@/src/lib/const-keys'

View file

@ -5,7 +5,6 @@ import type {
units, units,
productInfo, productInfo,
deliverySlotInfo, deliverySlotInfo,
productSlots,
specialDeals, specialDeals,
orders, orders,
orderItems, orderItems,
@ -21,7 +20,6 @@ export type Address = InferSelectModel<typeof addresses>;
export type Unit = InferSelectModel<typeof units>; export type Unit = InferSelectModel<typeof units>;
export type ProductInfo = InferSelectModel<typeof productInfo>; export type ProductInfo = InferSelectModel<typeof productInfo>;
export type DeliverySlotInfo = InferSelectModel<typeof deliverySlotInfo>; export type DeliverySlotInfo = InferSelectModel<typeof deliverySlotInfo>;
export type ProductSlot = InferSelectModel<typeof productSlots>;
export type SpecialDeal = InferSelectModel<typeof specialDeals>; export type SpecialDeal = InferSelectModel<typeof specialDeals>;
export type Order = InferSelectModel<typeof orders>; export type Order = InferSelectModel<typeof orders>;
export type OrderItem = InferSelectModel<typeof orderItems>; export type OrderItem = InferSelectModel<typeof orderItems>;

View file

@ -1,5 +1,5 @@
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 { productInfo, units, specialDeals, productTags, productReviews, productGroupInfo, productGroupMembership } from '../db/schema';
import { eq, and, inArray, desc, sql, asc } from 'drizzle-orm'; import { eq, and, inArray, desc, sql, asc } from 'drizzle-orm';
export async function getAllProducts(): Promise<any[]> { export async function getAllProducts(): Promise<any[]> {
@ -18,11 +18,6 @@ export async function getProductById(id: number): Promise<any | null> {
with: { with: {
unit: true, unit: true,
store: true, store: true,
productSlots: {
with: {
slot: true,
},
},
specialDeals: true, specialDeals: true,
productTags: { productTags: {
with: { with: {

View file

@ -1,30 +1,44 @@
import { db } from '../db/db_index'; import { db } from '../db/db_index';
import { deliverySlotInfo, productSlots, productInfo, vendorSnippets } from '../db/schema'; import { deliverySlotInfo, productInfo, vendorSnippets } from '../db/schema';
import { eq, and, inArray, desc } from 'drizzle-orm'; import { eq, and, inArray, desc } from 'drizzle-orm';
export async function getAllSlots(): Promise<any[]> { export async function getAllSlots(): Promise<any[]> {
return await db.query.deliverySlotInfo.findMany({ const slots = await db.query.deliverySlotInfo.findMany({
orderBy: desc(deliverySlotInfo.createdAt), orderBy: desc(deliverySlotInfo.createdAt),
with: { with: {
productSlots: {
with: {
product: true,
},
},
vendorSnippets: true, vendorSnippets: true,
}, },
}); });
// Fetch products for all slots
const allProductIds = new Set<number>();
for (const slot of slots) {
for (const productId of (slot.productIds || [])) {
allProductIds.add(productId);
}
}
const productIdsArray = Array.from(allProductIds);
const productsData = productIdsArray.length > 0
? await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIdsArray),
})
: [];
const productMap = new Map(productsData.map(p => [p.id, p]));
return slots.map(slot => ({
...slot,
products: (slot.productIds || [])
.map(productId => productMap.get(productId))
.filter((p): p is NonNullable<typeof p> => p != null),
}));
} }
export async function getSlotById(id: number): Promise<any | null> { export async function getSlotById(id: number): Promise<any | null> {
return await db.query.deliverySlotInfo.findFirst({ const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, id), where: eq(deliverySlotInfo.id, id),
with: { with: {
productSlots: {
with: {
product: true,
},
},
vendorSnippets: { vendorSnippets: {
with: { with: {
slot: true, slot: true,
@ -32,6 +46,23 @@ export async function getSlotById(id: number): Promise<any | null> {
}, },
}, },
}); });
if (!slot) {
return null;
}
// Fetch products for this slot
const productIds = slot.productIds || [];
const productsData = productIds.length > 0
? await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
})
: [];
return {
...slot,
products: productsData,
};
} }
export async function createSlot(input: any): Promise<any> { export async function createSlot(input: any): Promise<any> {
@ -52,28 +83,62 @@ export async function deleteSlot(id: number): Promise<void> {
} }
export async function getSlotProducts(slotId: number): Promise<any[]> { export async function getSlotProducts(slotId: number): Promise<any[]> {
return await db.query.productSlots.findMany({ const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(productSlots.slotId, slotId), where: eq(deliverySlotInfo.id, slotId),
with: { });
product: true,
}, if (!slot) {
return [];
}
const productIds = slot.productIds || [];
if (productIds.length === 0) {
return [];
}
return await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
}); });
} }
export async function addProductToSlot(slotId: number, productId: number): Promise<void> { export async function addProductToSlot(slotId: number, productId: number): Promise<void> {
await db.insert(productSlots).values({ slotId, productId }); const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId),
});
if (!slot) {
throw new Error(`Slot ${slotId} not found`);
}
const currentProductIds = slot.productIds || [];
if (!currentProductIds.includes(productId)) {
await db.update(deliverySlotInfo)
.set({ productIds: [...currentProductIds, productId] })
.where(eq(deliverySlotInfo.id, slotId));
}
} }
export async function removeProductFromSlot(slotId: number, productId: number): Promise<void> { export async function removeProductFromSlot(slotId: number, productId: number): Promise<void> {
await db.delete(productSlots) const slot = await db.query.deliverySlotInfo.findFirst({
.where(and( where: eq(deliverySlotInfo.id, slotId),
eq(productSlots.slotId, slotId), });
eq(productSlots.productId, productId)
)); if (!slot) {
throw new Error(`Slot ${slotId} not found`);
}
const currentProductIds = slot.productIds || [];
const updatedProductIds = currentProductIds.filter(id => id !== productId);
await db.update(deliverySlotInfo)
.set({ productIds: updatedProductIds })
.where(eq(deliverySlotInfo.id, slotId));
} }
export async function clearSlotProducts(slotId: number): Promise<void> { export async function clearSlotProducts(slotId: number): Promise<void> {
await db.delete(productSlots).where(eq(productSlots.slotId, slotId)); await db.update(deliverySlotInfo)
.set({ productIds: [] })
.where(eq(deliverySlotInfo.id, slotId));
} }
export async function updateSlotCapacity(slotId: number, maxCapacity: number): Promise<any> { export async function updateSlotCapacity(slotId: number, maxCapacity: number): Promise<any> {

View file

@ -6,7 +6,6 @@ import {
homeBanners, homeBanners,
productInfo, productInfo,
units, units,
productSlots,
deliverySlotInfo, deliverySlotInfo,
specialDeals, specialDeals,
storeInfo, storeInfo,
@ -112,23 +111,30 @@ export async function getAllStoresForCache(): Promise<StoreBasicData[]> {
} }
export async function getAllDeliverySlotsForCache(): Promise<DeliverySlotData[]> { export async function getAllDeliverySlotsForCache(): Promise<DeliverySlotData[]> {
return db const slots = await db.query.deliverySlotInfo.findMany({
.select({ where: and(
productId: productSlots.productId,
id: deliverySlotInfo.id,
deliveryTime: deliverySlotInfo.deliveryTime,
freezeTime: deliverySlotInfo.freezeTime,
isCapacityFull: deliverySlotInfo.isCapacityFull,
})
.from(productSlots)
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
.where(
and(
eq(deliverySlotInfo.isActive, true), eq(deliverySlotInfo.isActive, true),
eq(deliverySlotInfo.isCapacityFull, false), eq(deliverySlotInfo.isCapacityFull, false),
gt(deliverySlotInfo.deliveryTime, sql`NOW()`) gt(deliverySlotInfo.deliveryTime, sql`NOW()`)
) ),
) })
// Flatten slots with their product IDs
const result: DeliverySlotData[] = []
for (const slot of slots) {
const productIds = slot.productIds || []
for (const productId of productIds) {
result.push({
productId,
id: slot.id,
deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime,
isCapacityFull: slot.isCapacityFull,
})
}
}
return result
} }
export async function getAllSpecialDealsForCache(): Promise<SpecialDealData[]> { export async function getAllSpecialDealsForCache(): Promise<SpecialDealData[]> {
@ -203,8 +209,7 @@ export interface SlotWithProductsData {
freezeTime: Date freezeTime: Date
isActive: boolean isActive: boolean
isCapacityFull: boolean isCapacityFull: boolean
productSlots: Array<{ products: Array<{
product: {
id: number id: number
name: string name: string
productQuantity: number productQuantity: number
@ -216,32 +221,72 @@ export interface SlotWithProductsData {
images: unknown images: unknown
isOutOfStock: boolean isOutOfStock: boolean
storeId: number | null storeId: number | null
}
}> }>
} }
export async function getAllSlotsWithProductsForCache(): Promise<SlotWithProductsData[]> { export async function getAllSlotsWithProductsForCache(): Promise<SlotWithProductsData[]> {
const now = new Date() const now = new Date()
return db.query.deliverySlotInfo.findMany({ // Get all active future slots
const slots = await db.query.deliverySlotInfo.findMany({
where: and( where: and(
eq(deliverySlotInfo.isActive, true), eq(deliverySlotInfo.isActive, true),
gt(deliverySlotInfo.deliveryTime, now), gt(deliverySlotInfo.deliveryTime, now)
), ),
with: { orderBy: asc(deliverySlotInfo.deliveryTime),
productSlots: { })
with: {
product: { // Get all unique product IDs from all slots
const allProductIds = new Set<number>()
for (const slot of slots) {
for (const productId of (slot.productIds || [])) {
allProductIds.add(productId)
}
}
// Fetch all products in one query
const productIdsArray = Array.from(allProductIds)
const productsData = productIdsArray.length > 0
? await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIdsArray),
with: { with: {
unit: true, unit: true,
store: true, store: true,
}, },
}, })
}, : []
},
}, // Create a map for quick lookup
orderBy: asc(deliverySlotInfo.deliveryTime), const productMap = new Map(productsData.map(p => [p.id, p]))
}) as Promise<SlotWithProductsData[]>
// Build the result
return slots.map(slot => ({
id: slot.id,
deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime,
isActive: slot.isActive,
isCapacityFull: slot.isCapacityFull,
products: (slot.productIds || [])
.map(productId => productMap.get(productId))
.filter((p): p is NonNullable<typeof p> => p != null)
.map(product => ({
id: product.id,
name: product.name,
productQuantity: product.productQuantity,
shortDescription: product.shortDescription,
price: String(product.price ?? '0'),
marketPrice: product.marketPrice ? String(product.marketPrice) : null,
unit: product.unit ? { shortNotation: product.unit.shortNotation } : null,
store: product.store ? {
id: product.store.id,
name: product.store.name,
description: product.store.description
} : null,
images: product.images,
isOutOfStock: product.isOutOfStock,
storeId: product.storeId,
})),
})) as SlotWithProductsData[]
} }
// ============================================================================ // ============================================================================

View file

@ -1,5 +1,5 @@
import { db } from '../db/db_index' import { db } from '../db/db_index'
import { productInfo, productReviews, specialDeals, storeInfo, units, users } from '../db/schema' import { deliverySlotInfo, productInfo, productReviews, specialDeals, storeInfo, units, users } from '../db/schema'
import { and, desc, eq, gt, sql } from 'drizzle-orm' import { and, desc, eq, gt, sql } from 'drizzle-orm'
import type { UserProductDetailData, UserProductReview } from '@packages/shared' import type { UserProductDetailData, UserProductReview } from '@packages/shared'
@ -229,20 +229,22 @@ export async function getSuspendedProductIds(): Promise<number[]> {
* This version filters by both isActive AND isCapacityFull * This version filters by both isActive AND isCapacityFull
*/ */
export async function getNextDeliveryDateWithCapacity(productId: number): Promise<Date | null> { export async function getNextDeliveryDateWithCapacity(productId: number): Promise<Date | null> {
const result = await db const slots = await db.query.deliverySlotInfo.findMany({
.select({ deliveryTime: deliverySlotInfo.deliveryTime }) where: and(
.from(productSlots)
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
.where(
and(
eq(productSlots.productId, productId),
eq(deliverySlotInfo.isActive, true), eq(deliverySlotInfo.isActive, true),
eq(deliverySlotInfo.isCapacityFull, false), eq(deliverySlotInfo.isCapacityFull, false),
gt(deliverySlotInfo.deliveryTime, sql`NOW()`) gt(deliverySlotInfo.deliveryTime, sql`NOW()`)
) ),
) orderBy: desc(deliverySlotInfo.deliveryTime),
.orderBy(deliverySlotInfo.deliveryTime) })
.limit(1)
return result[0]?.deliveryTime || null // Find the first slot that contains this product
for (const slot of slots) {
const productIds = slot.productIds || []
if (productIds.includes(productId)) {
return slot.deliveryTime
}
}
return null
} }

View file

@ -0,0 +1,24 @@
-- Migration: Replace product_slots junction table with JSON array in delivery_slot_info
-- Step 1: Add product_ids column to delivery_slot_info
ALTER TABLE `delivery_slot_info` ADD COLUMN `product_ids` text DEFAULT '[]';
-- Step 2: Migrate data from product_slots to product_ids JSON array
-- Use a subquery to aggregate product_ids for each slot
UPDATE `delivery_slot_info`
SET `product_ids` = (
SELECT CASE
WHEN GROUP_CONCAT(`product_id`) IS NULL THEN '[]'
ELSE '[' || GROUP_CONCAT(`product_id`) || ']'
END
FROM `product_slots`
WHERE `product_slots`.`slot_id` = `delivery_slot_info`.`id`
);
-- Step 3: Drop the old product_slots table
DROP TABLE IF EXISTS `product_slots`;
-- Note: After running this migration, update the schema.ts file to:
-- 1. Add productIds: jsonText<number[]>('product_ids').$defaultFn(() => []) to deliverySlotInfo
-- 2. Remove the productSlots table definition
-- 3. Remove productSlotsRelations
-- 4. Update deliverySlotInfoRelations to remove productSlots reference

View file

@ -3,7 +3,7 @@ import {
productInfo, productInfo,
units, units,
specialDeals, specialDeals,
productSlots, deliverySlotInfo,
productTags, productTags,
productReviews, productReviews,
productGroupInfo, productGroupInfo,
@ -229,37 +229,25 @@ export async function toggleProductOutOfStock(id: number): Promise<AdminProduct
} }
export async function updateSlotProducts(slotId: string, productIds: string[]): Promise<AdminUpdateSlotProductsResult> { export async function updateSlotProducts(slotId: string, productIds: string[]): Promise<AdminUpdateSlotProductsResult> {
const currentAssociations = await db.query.productSlots.findMany({ const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(productSlots.slotId, parseInt(slotId)), where: eq(deliverySlotInfo.id, parseInt(slotId)),
columns: { })
productId: true,
},
}) as Array<{ productId: number }>
const currentProductIds = currentAssociations.map((assoc: { productId: number }) => assoc.productId) if (!slot) {
throw new Error(`Slot ${slotId} not found`)
}
const currentProductIds = slot.productIds || []
const newProductIds = productIds.map((id: string) => parseInt(id)) const newProductIds = productIds.map((id: string) => parseInt(id))
// Simply update the productIds array
await db.update(deliverySlotInfo)
.set({ productIds: newProductIds })
.where(eq(deliverySlotInfo.id, parseInt(slotId)))
const productsToAdd = newProductIds.filter((id: number) => !currentProductIds.includes(id)) const productsToAdd = newProductIds.filter((id: number) => !currentProductIds.includes(id))
const productsToRemove = currentProductIds.filter((id: number) => !newProductIds.includes(id)) const productsToRemove = currentProductIds.filter((id: number) => !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 { return {
message: 'Slot products updated successfully', message: 'Slot products updated successfully',
added: productsToAdd.length, added: productsToAdd.length,
@ -268,14 +256,11 @@ export async function updateSlotProducts(slotId: string, productIds: string[]):
} }
export async function getSlotProductIds(slotId: string): Promise<number[]> { export async function getSlotProductIds(slotId: string): Promise<number[]> {
const associations = await db.query.productSlots.findMany({ const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(productSlots.slotId, parseInt(slotId)), where: eq(deliverySlotInfo.id, parseInt(slotId)),
columns: {
productId: true,
},
}) })
return associations.map((assoc: { productId: number }) => assoc.productId) return slot?.productIds || []
} }
export async function getAllUnits(): Promise<AdminUnit[]> { export async function getAllUnits(): Promise<AdminUnit[]> {
@ -433,20 +418,13 @@ export async function getSlotsProductIds(slotIds: number[]): Promise<Record<numb
return {} return {}
} }
const associations = await db.query.productSlots.findMany({ const slots = await db.query.deliverySlotInfo.findMany({
where: inArray(productSlots.slotId, slotIds), where: inArray(deliverySlotInfo.id, slotIds),
columns: { })
slotId: true,
productId: true,
},
}) as Array<{ slotId: number; productId: number }>
const result: Record<number, number[]> = {} const result: Record<number, number[]> = {}
for (const assoc of associations) { for (const slot of slots) {
if (!result[assoc.slotId]) { result[slot.id] = slot.productIds || []
result[assoc.slotId] = []
}
result[assoc.slotId].push(assoc.productId)
} }
slotIds.forEach((slotId) => { slotIds.forEach((slotId) => {

View file

@ -1,12 +1,11 @@
import { db } from '../db/db_index' import { db } from '../db/db_index'
import { import {
deliverySlotInfo, deliverySlotInfo,
productSlots,
productInfo, productInfo,
vendorSnippets, vendorSnippets,
productGroupInfo, productGroupInfo,
} from '../db/schema' } from '../db/schema'
import { and, asc, desc, eq, gt, inArray, lt } from 'drizzle-orm' import { and, asc, desc, eq, gt, inArray } from 'drizzle-orm'
import type { import type {
AdminDeliverySlot, AdminDeliverySlot,
AdminSlotWithProducts, AdminSlotWithProducts,
@ -54,7 +53,6 @@ const chunkArray = <T>(items: T[], size: number): T[][] => {
} }
const PRODUCT_ID_CHUNK_SIZE = 40 const PRODUCT_ID_CHUNK_SIZE = 40
const PRODUCT_SLOT_CHUNK_SIZE = 40
const fetchExistingProductIds = async (tx: any, productIds: number[]) => { const fetchExistingProductIds = async (tx: any, productIds: number[]) => {
const existingIds = new Set<number>() const existingIds = new Set<number>()
@ -103,25 +101,39 @@ export async function getActiveSlotsWithProducts(limit: number = 20): Promise<Ad
where: eq(deliverySlotInfo.isActive, true), where: eq(deliverySlotInfo.isActive, true),
orderBy: desc(deliverySlotInfo.deliveryTime), orderBy: desc(deliverySlotInfo.deliveryTime),
limit, limit,
with: {
productSlots: {
with: {
product: {
columns: {
id: true,
name: true,
images: true,
},
},
},
},
},
}) })
return slots.map((slot: any) => ({ // Get all unique product IDs from all slots
const allProductIds = new Set<number>()
for (const slot of slots) {
for (const productId of (slot.productIds || [])) {
allProductIds.add(productId)
}
}
// Fetch all products in one query
const productIdsArray = Array.from(allProductIds)
const productIdsSet = new Set(productIdsArray)
// const productsData = productIdsArray.length > 0
// ? await db.query.productInfo.findMany({
// where: inArray(productInfo.id, productIdsArray),
// columns: { id: true, name: true, images: true },
// })
// : []
let productsData = await db.query.productInfo.findMany({});
productsData = productsData.filter(item => productIdsSet.has(item.id))
// Create a map for quick lookup
const productMap = new Map(productsData.map(p => [p.id, p]))
return slots.map((slot) => ({
...mapDeliverySlot(slot), ...mapDeliverySlot(slot),
deliverySequence: getNumberArray(slot.deliverySequence), deliverySequence: getNumberArray(slot.deliverySequence),
products: slot.productSlots.map((ps: any) => mapSlotProductSummary(ps.product)), products: (slot.productIds || [])
.map(productId => productMap.get(productId))
.filter((p): p is NonNullable<typeof p> => p != null)
.map(product => mapSlotProductSummary(product)),
})) }))
} }
@ -140,10 +152,13 @@ export async function staleSlotsCleanup(): Promise<number> {
// Get the threshold (the 20th most recent slot ID - minimum of the 20) // Get the threshold (the 20th most recent slot ID - minimum of the 20)
const threshold = Math.min(...recentSlots.map((s) => s.id)) const threshold = Math.min(...recentSlots.map((s) => s.id))
// Delete product_slots for all slots older than threshold // Clear productIds for all slots older than threshold
const result = await db.delete(productSlots).where(lt(productSlots.slotId, threshold)) const result = await db
.update(deliverySlotInfo)
.set({ productIds: [] })
.where(eq(deliverySlotInfo.id, threshold))
return result.changes || 0 return 1
} }
export async function getActiveSlots(): Promise<AdminDeliverySlot[]> { export async function getActiveSlots(): Promise<AdminDeliverySlot[]> {
@ -170,17 +185,6 @@ export async function getSlotByIdWithRelations(id: number): Promise<AdminSlotWit
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: {
productSlots: {
with: {
product: {
columns: {
id: true,
name: true,
images: true,
},
},
},
},
vendorSnippets: true, vendorSnippets: true,
}, },
}) })
@ -189,11 +193,26 @@ export async function getSlotByIdWithRelations(id: number): Promise<AdminSlotWit
return null return null
} }
// Fetch products for this slot
const productIds = slot.productIds || []
const productIdSet = new Set(productIds);
// const productsData = productIds.length > 0
// ? await db.query.productInfo.findMany({
// where: inArray(productInfo.id, productIds),
// columns: { id: true, name: true, images: true },
// })
// : []
let productsData = productIds.length > 0
? await db.query.productInfo.findMany({
columns: { id: true, name: true, images: true },
})
: []
productsData = productsData.filter(item => productIdSet.has(item.id))
return { return {
...mapDeliverySlot(slot), ...mapDeliverySlot(slot),
deliverySequence: getNumberArray(slot.deliverySequence), deliverySequence: getNumberArray(slot.deliverySequence),
groupIds: getNumberArray(slot.groupIds), groupIds: getNumberArray(slot.groupIds),
products: slot.productSlots.map((ps: any) => mapSlotProductSummary(ps.product)), products: productsData.map(product => mapSlotProductSummary(product)),
vendorSnippets: slot.vendorSnippets.map(mapVendorSnippet), vendorSnippets: slot.vendorSnippets.map(mapVendorSnippet),
} }
} }
@ -208,7 +227,18 @@ export async function createSlotWithRelations(input: {
}): Promise<AdminSlotCreateResult> { }): Promise<AdminSlotCreateResult> {
const { deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input const { deliveryTime, freezeTime, isActive, productIds, vendorSnippets: snippets, groupIds } = input
const normalizedProductIds = normalizeProductIds(productIds)
const result = await db.transaction(async (tx) => { const result = await db.transaction(async (tx) => {
// Validate product IDs if provided
if (normalizedProductIds.length > 0) {
const existingIds = await fetchExistingProductIds(tx, normalizedProductIds)
const missingIds = normalizedProductIds.filter((productId) => !existingIds.has(productId))
if (missingIds.length > 0) {
throw new Error(`Invalid product IDs: ${missingIds.join(', ')}`)
}
}
const [newSlot] = await tx const [newSlot] = await tx
.insert(deliverySlotInfo) .insert(deliverySlotInfo)
.values({ .values({
@ -216,27 +246,10 @@ export async function createSlotWithRelations(input: {
freezeTime: new Date(freezeTime), freezeTime: new Date(freezeTime),
isActive: isActive !== undefined ? isActive : true, isActive: isActive !== undefined ? isActive : true,
groupIds: groupIds !== undefined ? groupIds : [], groupIds: groupIds !== undefined ? groupIds : [],
productIds: normalizedProductIds,
}) })
.returning() .returning()
const normalizedProductIds = normalizeProductIds(productIds)
if (productIds && normalizedProductIds.length > 0) {
const existingIds = await fetchExistingProductIds(tx, normalizedProductIds)
const missingIds = normalizedProductIds.filter((productId) => !existingIds.has(productId))
if (missingIds.length > 0) {
throw new Error(`Invalid product IDs: ${missingIds.join(', ')}`)
}
const associations = normalizedProductIds.map((productId) => ({
productId,
slotId: newSlot.id,
}))
const associationChunks = chunkArray(associations, PRODUCT_SLOT_CHUNK_SIZE)
for (const chunk of associationChunks) {
await tx.insert(productSlots).values(chunk)
}
}
let createdSnippets: AdminVendorSnippet[] = [] let createdSnippets: AdminVendorSnippet[] = []
if (snippets && snippets.length > 0) { if (snippets && snippets.length > 0) {
for (const snippet of snippets) { for (const snippet of snippets) {
@ -295,7 +308,18 @@ export async function updateSlotWithRelations(input: {
validGroupIds = existingGroups.map((group: { id: number }) => group.id) validGroupIds = existingGroups.map((group: { id: number }) => group.id)
} }
const normalizedProductIds = productIds !== undefined ? normalizeProductIds(productIds) : undefined
const result = await db.transaction(async (tx) => { const result = await db.transaction(async (tx) => {
// Validate product IDs if provided
if (normalizedProductIds !== undefined && normalizedProductIds.length > 0) {
const existingIds = await fetchExistingProductIds(tx, normalizedProductIds)
const missingIds = normalizedProductIds.filter((productId) => !existingIds.has(productId))
if (missingIds.length > 0) {
throw new Error(`Invalid product IDs: ${missingIds.join(', ')}`)
}
}
const [updatedSlot] = await tx const [updatedSlot] = await tx
.update(deliverySlotInfo) .update(deliverySlotInfo)
.set({ .set({
@ -303,6 +327,7 @@ export async function updateSlotWithRelations(input: {
freezeTime: new Date(freezeTime), freezeTime: new Date(freezeTime),
isActive: isActive !== undefined ? isActive : true, isActive: isActive !== undefined ? isActive : true,
groupIds: validGroupIds !== undefined ? validGroupIds : [], groupIds: validGroupIds !== undefined ? validGroupIds : [],
...(normalizedProductIds !== undefined && { productIds: normalizedProductIds }),
}) })
.where(eq(deliverySlotInfo.id, id)) .where(eq(deliverySlotInfo.id, id))
.returning() .returning()
@ -311,28 +336,6 @@ export async function updateSlotWithRelations(input: {
return null return null
} }
if (productIds !== undefined) {
await tx.delete(productSlots).where(eq(productSlots.slotId, id))
const normalizedProductIds = normalizeProductIds(productIds)
if (productIds.length > 0 && normalizedProductIds.length > 0) {
const existingIds = await fetchExistingProductIds(tx, normalizedProductIds)
const missingIds = normalizedProductIds.filter((productId) => !existingIds.has(productId))
if (missingIds.length > 0) {
throw new Error(`Invalid product IDs: ${missingIds.join(', ')}`)
}
const associations = normalizedProductIds.map((productId) => ({
productId,
slotId: id,
}))
const associationChunks = chunkArray(associations, PRODUCT_SLOT_CHUNK_SIZE)
for (const chunk of associationChunks) {
await tx.insert(productSlots).values(chunk)
}
}
}
let createdSnippets: AdminVendorSnippet[] = [] let createdSnippets: AdminVendorSnippet[] = []
if (snippets && snippets.length > 0) { if (snippets && snippets.length > 0) {
for (const snippet of snippets) { for (const snippet of snippets) {

View file

@ -280,6 +280,7 @@ export const deliverySlotInfo = sqliteTable('delivery_slot_info', {
isCapacityFull: integer('is_capacity_full', { mode: 'boolean' }).notNull().default(false), isCapacityFull: integer('is_capacity_full', { mode: 'boolean' }).notNull().default(false),
deliverySequence: jsonText<Record<string, number>>('delivery_sequence').$defaultFn(() => ({})), deliverySequence: jsonText<Record<string, number>>('delivery_sequence').$defaultFn(() => ({})),
groupIds: jsonText<number[]>('group_ids').$defaultFn(() => []), groupIds: jsonText<number[]>('group_ids').$defaultFn(() => []),
productIds: jsonText<number[]>('product_ids').$defaultFn(() => []),
}) })
export const vendorSnippets = sqliteTable('vendor_snippets', { export const vendorSnippets = sqliteTable('vendor_snippets', {
@ -292,13 +293,6 @@ export const vendorSnippets = sqliteTable('vendor_snippets', {
createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}) })
export const productSlots = sqliteTable('product_slots', {
productId: integer('product_id').notNull().references(() => productInfo.id),
slotId: integer('slot_id').notNull().references(() => deliverySlotInfo.id),
}, (t) => ({
pk: primaryKey({ columns: [t.productId, t.slotId], name: 'product_slot_pk' }),
}))
export const specialDeals = sqliteTable('special_deals', { export const specialDeals = sqliteTable('special_deals', {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
productId: integer('product_id').notNull().references(() => productInfo.id), productId: integer('product_id').notNull().references(() => productInfo.id),
@ -561,7 +555,6 @@ export const unitsRelations = relations(units, ({ many }) => ({
export const productInfoRelations = relations(productInfo, ({ one, many }) => ({ export const productInfoRelations = relations(productInfo, ({ one, many }) => ({
unit: one(units, { fields: [productInfo.unitId], references: [units.id] }), unit: one(units, { fields: [productInfo.unitId], references: [units.id] }),
store: one(storeInfo, { fields: [productInfo.storeId], references: [storeInfo.id] }), store: one(storeInfo, { fields: [productInfo.storeId], references: [storeInfo.id] }),
productSlots: many(productSlots),
specialDeals: many(specialDeals), specialDeals: many(specialDeals),
orderItems: many(orderItems), orderItems: many(orderItems),
cartItems: many(cartItems), cartItems: many(cartItems),
@ -581,16 +574,10 @@ export const productTagsRelations = relations(productTags, ({ one }) => ({
})) }))
export const deliverySlotInfoRelations = relations(deliverySlotInfo, ({ many }) => ({ export const deliverySlotInfoRelations = relations(deliverySlotInfo, ({ many }) => ({
productSlots: many(productSlots),
orders: many(orders), orders: many(orders),
vendorSnippets: many(vendorSnippets), vendorSnippets: many(vendorSnippets),
})) }))
export const productSlotsRelations = relations(productSlots, ({ one }) => ({
product: one(productInfo, { fields: [productSlots.productId], references: [productInfo.id] }),
slot: one(deliverySlotInfo, { fields: [productSlots.slotId], references: [deliverySlotInfo.id] }),
}))
export const specialDealsRelations = relations(specialDeals, ({ one }) => ({ export const specialDealsRelations = relations(specialDeals, ({ one }) => ({
product: one(productInfo, { fields: [specialDeals.productId], references: [productInfo.id] }), product: one(productInfo, { fields: [specialDeals.productId], references: [productInfo.id] }),
})) }))

View file

@ -3,7 +3,6 @@ import {
units, units,
productInfo, productInfo,
deliverySlotInfo, deliverySlotInfo,
productSlots,
keyValStore, keyValStore,
staffRoles, staffRoles,
staffPermissions, staffPermissions,

View file

@ -5,7 +5,6 @@ import type {
units, units,
productInfo, productInfo,
deliverySlotInfo, deliverySlotInfo,
productSlots,
specialDeals, specialDeals,
orders, orders,
orderItems, orderItems,
@ -21,7 +20,6 @@ export type Address = InferSelectModel<typeof addresses>
export type Unit = InferSelectModel<typeof units> export type Unit = InferSelectModel<typeof units>
export type ProductInfo = InferSelectModel<typeof productInfo> export type ProductInfo = InferSelectModel<typeof productInfo>
export type DeliverySlotInfo = InferSelectModel<typeof deliverySlotInfo> export type DeliverySlotInfo = InferSelectModel<typeof deliverySlotInfo>
export type ProductSlot = InferSelectModel<typeof productSlots>
export type SpecialDeal = InferSelectModel<typeof specialDeals> export type SpecialDeal = InferSelectModel<typeof specialDeals>
export type Order = InferSelectModel<typeof orders> export type Order = InferSelectModel<typeof orders>
export type OrderItem = InferSelectModel<typeof orderItems> export type OrderItem = InferSelectModel<typeof orderItems>

View file

@ -1,5 +1,5 @@
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 { productInfo, units, specialDeals, productTags, productReviews, productGroupInfo, productGroupMembership } from '../db/schema';
import { eq, and, inArray, desc, sql, asc } from 'drizzle-orm'; import { eq, and, inArray, desc, sql, asc } from 'drizzle-orm';
export async function getAllProducts(): Promise<any[]> { export async function getAllProducts(): Promise<any[]> {
@ -18,11 +18,6 @@ export async function getProductById(id: number): Promise<any | null> {
with: { with: {
unit: true, unit: true,
store: true, store: true,
productSlots: {
with: {
slot: true,
},
},
specialDeals: true, specialDeals: true,
productTags: { productTags: {
with: { with: {

View file

@ -1,30 +1,44 @@
import { db } from '../db/db_index'; import { db } from '../db/db_index';
import { deliverySlotInfo, productSlots, productInfo, vendorSnippets } from '../db/schema'; import { deliverySlotInfo, productInfo, vendorSnippets } from '../db/schema';
import { eq, and, inArray, desc } from 'drizzle-orm'; import { eq, and, inArray, desc } from 'drizzle-orm';
export async function getAllSlots(): Promise<any[]> { export async function getAllSlots(): Promise<any[]> {
return await db.query.deliverySlotInfo.findMany({ const slots = await db.query.deliverySlotInfo.findMany({
orderBy: desc(deliverySlotInfo.deliveryTime), orderBy: desc(deliverySlotInfo.deliveryTime),
with: { with: {
productSlots: {
with: {
product: true,
},
},
vendorSnippets: true, vendorSnippets: true,
}, },
}); });
// Fetch products for all slots
const allProductIds = new Set<number>();
for (const slot of slots) {
for (const productId of (slot.productIds || [])) {
allProductIds.add(productId);
}
}
const productIdsArray = Array.from(allProductIds);
const productsData = productIdsArray.length > 0
? await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIdsArray),
})
: [];
const productMap = new Map(productsData.map(p => [p.id, p]));
return slots.map(slot => ({
...slot,
products: (slot.productIds || [])
.map(productId => productMap.get(productId))
.filter((p): p is NonNullable<typeof p> => p != null),
}));
} }
export async function getSlotById(id: number): Promise<any | null> { export async function getSlotById(id: number): Promise<any | null> {
return await db.query.deliverySlotInfo.findFirst({ const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, id), where: eq(deliverySlotInfo.id, id),
with: { with: {
productSlots: {
with: {
product: true,
},
},
vendorSnippets: { vendorSnippets: {
with: { with: {
slot: true, slot: true,
@ -32,6 +46,27 @@ export async function getSlotById(id: number): Promise<any | null> {
}, },
}, },
}); });
if (!slot) {
return null;
}
// Fetch products for this slot
const productIds = slot.productIds || [];
const productIdSet = new Set(productIds)
// const productsData = productIds.length > 0
// ? await db.query.productInfo.findMany({
// where: inArray(productInfo.id, productIds),
// })
// : [];
let productsData = productIds.length > 0 ? await db.query.productInfo.findMany({}) : [];
productsData = productsData.filter(item => productIdSet.has(item.id))
return {
...slot,
products: productsData,
};
} }
export async function createSlot(input: any): Promise<any> { export async function createSlot(input: any): Promise<any> {
@ -52,28 +87,62 @@ export async function deleteSlot(id: number): Promise<void> {
} }
export async function getSlotProducts(slotId: number): Promise<any[]> { export async function getSlotProducts(slotId: number): Promise<any[]> {
return await db.query.productSlots.findMany({ const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(productSlots.slotId, slotId), where: eq(deliverySlotInfo.id, slotId),
with: { });
product: true,
}, if (!slot) {
return [];
}
const productIds = slot.productIds || [];
if (productIds.length === 0) {
return [];
}
return await db.query.productInfo.findMany({
where: inArray(productInfo.id, productIds),
}); });
} }
export async function addProductToSlot(slotId: number, productId: number): Promise<void> { export async function addProductToSlot(slotId: number, productId: number): Promise<void> {
await db.insert(productSlots).values({ slotId, productId }); const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, slotId),
});
if (!slot) {
throw new Error(`Slot ${slotId} not found`);
}
const currentProductIds = slot.productIds || [];
if (!currentProductIds.includes(productId)) {
await db.update(deliverySlotInfo)
.set({ productIds: [...currentProductIds, productId] })
.where(eq(deliverySlotInfo.id, slotId));
}
} }
export async function removeProductFromSlot(slotId: number, productId: number): Promise<void> { export async function removeProductFromSlot(slotId: number, productId: number): Promise<void> {
await db.delete(productSlots) const slot = await db.query.deliverySlotInfo.findFirst({
.where(and( where: eq(deliverySlotInfo.id, slotId),
eq(productSlots.slotId, slotId), });
eq(productSlots.productId, productId)
)); if (!slot) {
throw new Error(`Slot ${slotId} not found`);
}
const currentProductIds = slot.productIds || [];
const updatedProductIds = currentProductIds.filter(id => id !== productId);
await db.update(deliverySlotInfo)
.set({ productIds: updatedProductIds })
.where(eq(deliverySlotInfo.id, slotId));
} }
export async function clearSlotProducts(slotId: number): Promise<void> { export async function clearSlotProducts(slotId: number): Promise<void> {
await db.delete(productSlots).where(eq(productSlots.slotId, slotId)); await db.update(deliverySlotInfo)
.set({ productIds: [] })
.where(eq(deliverySlotInfo.id, slotId));
} }
export async function updateSlotCapacity(slotId: number, maxCapacity: number): Promise<any> { export async function updateSlotCapacity(slotId: number, maxCapacity: number): Promise<any> {

View file

@ -6,7 +6,6 @@ import {
homeBanners, homeBanners,
productInfo, productInfo,
units, units,
productSlots,
deliverySlotInfo, deliverySlotInfo,
specialDeals, specialDeals,
storeInfo, storeInfo,
@ -14,7 +13,7 @@ import {
productTagInfo, productTagInfo,
userIncidents, userIncidents,
} from '../db/schema' } from '../db/schema'
import { eq, and, gt, sql, isNotNull, asc } from 'drizzle-orm' import { eq, and, gt, sql, isNotNull, asc, inArray } from 'drizzle-orm'
// ============================================================================ // ============================================================================
// BANNER STORE HELPERS // BANNER STORE HELPERS
@ -119,23 +118,30 @@ export async function getAllStoresForCache(): Promise<StoreBasicData[]> {
} }
export async function getAllDeliverySlotsForCache(): Promise<DeliverySlotData[]> { export async function getAllDeliverySlotsForCache(): Promise<DeliverySlotData[]> {
return db const slots = await db.query.deliverySlotInfo.findMany({
.select({ where: and(
productId: productSlots.productId,
id: deliverySlotInfo.id,
deliveryTime: deliverySlotInfo.deliveryTime,
freezeTime: deliverySlotInfo.freezeTime,
isCapacityFull: deliverySlotInfo.isCapacityFull,
})
.from(productSlots)
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
.where(
and(
eq(deliverySlotInfo.isActive, true), eq(deliverySlotInfo.isActive, true),
eq(deliverySlotInfo.isCapacityFull, false), eq(deliverySlotInfo.isCapacityFull, false),
gt(deliverySlotInfo.deliveryTime, sql`CURRENT_TIMESTAMP`) gt(deliverySlotInfo.deliveryTime, sql`CURRENT_TIMESTAMP`)
) ),
) })
// Flatten slots with their product IDs
const result: DeliverySlotData[] = []
for (const slot of slots) {
const productIds = slot.productIds || []
for (const productId of productIds) {
result.push({
productId,
id: slot.id,
deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime,
isCapacityFull: slot.isCapacityFull,
})
}
}
return result
} }
export async function getAllSpecialDealsForCache(): Promise<SpecialDealData[]> { export async function getAllSpecialDealsForCache(): Promise<SpecialDealData[]> {
@ -216,8 +222,7 @@ export interface SlotWithProductsData {
freezeTime: Date freezeTime: Date
isActive: boolean isActive: boolean
isCapacityFull: boolean isCapacityFull: boolean
productSlots: Array<{ products: Array<{
product: {
id: number id: number
name: string name: string
productQuantity: number productQuantity: number
@ -229,32 +234,84 @@ export interface SlotWithProductsData {
images: unknown images: unknown
isOutOfStock: boolean isOutOfStock: boolean
storeId: number | null storeId: number | null
}
}> }>
} }
export async function getAllSlotsWithProductsForCache(): Promise<SlotWithProductsData[]> { export async function getAllSlotsWithProductsForCache(): Promise<SlotWithProductsData[]> {
const now = new Date() const now = new Date()
return db.query.deliverySlotInfo.findMany({ // Get all active future slots
const slots = await db.query.deliverySlotInfo.findMany({
where: and( where: and(
eq(deliverySlotInfo.isActive, true), eq(deliverySlotInfo.isActive, true),
gt(deliverySlotInfo.deliveryTime, now) gt(deliverySlotInfo.deliveryTime, now)
), ),
with: { orderBy: asc(deliverySlotInfo.deliveryTime),
productSlots: { })
with: {
product: { // Get all unique product IDs from all slots
const allProductIds = new Set<number>()
for (const slot of slots) {
for (const productId of (slot.productIds || [])) {
allProductIds.add(productId)
}
}
// Fetch all products in one query
const productIdsArray = Array.from(allProductIds)
const productIdSet = new Set(productIdsArray);
// const productsData = productIdsArray.length > 0
// ? await db.query.productInfo.findMany({
// where: inArray(productInfo.id, productIdsArray),
// with: {
// unit: true,
// store: true,
// },
// })
// : []
let productsData = productIdsArray.length > 0
? await db.query.productInfo.findMany({
// where: inArray(productInfo.id, productIdsArray),
with: { with: {
unit: true, unit: true,
store: true, store: true,
}, },
}, })
}, : []
},
}, productsData = productsData.filter(item => productIdSet.has(item.id))
orderBy: asc(deliverySlotInfo.deliveryTime),
}) as Promise<SlotWithProductsData[]> // Create a map for quick lookup
const productMap = new Map(productsData.map(p => [p.id, p]))
// Build the result
return slots.map(slot => ({
id: slot.id,
deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime,
isActive: slot.isActive,
isCapacityFull: slot.isCapacityFull,
products: (slot.productIds || [])
.map(productId => productMap.get(productId))
.filter((p): p is NonNullable<typeof p> => p != null)
.map(product => ({
id: product.id,
name: product.name,
productQuantity: product.productQuantity,
shortDescription: product.shortDescription,
price: String(product.price ?? '0'),
marketPrice: product.marketPrice ? String(product.marketPrice) : null,
unit: product.unit ? { shortNotation: product.unit.shortNotation } : null,
store: product.store ? {
id: product.store.id,
name: product.store.name,
description: product.store.description
} : null,
images: product.images,
isOutOfStock: product.isOutOfStock,
storeId: product.storeId,
})),
})) as SlotWithProductsData[]
} }
// ============================================================================ // ============================================================================

View file

@ -1,6 +1,6 @@
import { db } from '../db/db_index' import { db } from '../db/db_index'
import { deliverySlotInfo, productInfo, productReviews, productSlots, productTags, specialDeals, storeInfo, units, users } from '../db/schema' import { deliverySlotInfo, productInfo, productReviews, productTags, specialDeals, storeInfo, units, users } from '../db/schema'
import { and, desc, eq, gt, inArray, sql } from 'drizzle-orm' import { and, desc, eq, gt, sql } from 'drizzle-orm'
import type { UserProductDetailData, UserProductReview } from '@packages/shared' import type { UserProductDetailData, UserProductReview } from '@packages/shared'
const getStringArray = (value: unknown): string[] | null => { const getStringArray = (value: unknown): string[] | null => {
@ -235,20 +235,22 @@ export async function getSuspendedProductIds(): Promise<number[]> {
* This version filters by both isActive AND isCapacityFull * This version filters by both isActive AND isCapacityFull
*/ */
export async function getNextDeliveryDateWithCapacity(productId: number): Promise<Date | null> { export async function getNextDeliveryDateWithCapacity(productId: number): Promise<Date | null> {
const result = await db const slots = await db.query.deliverySlotInfo.findMany({
.select({ deliveryTime: deliverySlotInfo.deliveryTime }) where: and(
.from(productSlots)
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
.where(
and(
eq(productSlots.productId, productId),
eq(deliverySlotInfo.isActive, true), eq(deliverySlotInfo.isActive, true),
eq(deliverySlotInfo.isCapacityFull, false), eq(deliverySlotInfo.isCapacityFull, false),
gt(deliverySlotInfo.deliveryTime, sql`CURRENT_TIMESTAMP`) gt(deliverySlotInfo.deliveryTime, sql`CURRENT_TIMESTAMP`)
) ),
) orderBy: desc(deliverySlotInfo.deliveryTime),
.orderBy(deliverySlotInfo.deliveryTime) })
.limit(1)
return result[0]?.deliveryTime || null // Find the first slot that contains this product
for (const slot of slots) {
const productIds = slot.productIds || []
if (productIds.includes(productId)) {
return slot.deliveryTime
}
}
return null
} }