This commit is contained in:
shafi54 2026-07-31 21:54:46 +05:30
parent f6bcc23ca2
commit 648a0a5d28
16 changed files with 207 additions and 184 deletions

View file

@ -42,6 +42,7 @@ export default function EditProduct() {
longDescription: productData.longDescription || '',
storeId: productData.storeId || 1,
variants: (productData.skus || []).map((sku) => ({
id: sku.id,
name: sku.name || '',
price: sku.price || '',
marketPrice: sku.marketPrice || '',
@ -106,6 +107,7 @@ export default function EditProduct() {
const allUrls = [...existingUrls, ...newUrls]
return {
id: variant.id,
name: variant.name || null,
price: parseFloat(variant.price),
marketPrice: variant.marketPrice ? parseFloat(variant.marketPrice) : undefined,

View file

@ -11,6 +11,7 @@ interface Attribute {
}
interface Variant {
id?: number
name: string
price: string
marketPrice: string

View file

@ -69,11 +69,11 @@ export {
checkProductExistsByName,
checkUnitExists,
getProductImagesById,
createSpecialDealsForProduct,
updateProductDeals,
createSpecialDealsForSku,
updateSkuDeals,
replaceProductTags,
mergeSkus,
toggleProductOutOfStock,
toggleSkuOutOfStock,
updateSlotProducts,
getSlotProductIds,
getSlotsProductIds,

View file

@ -310,6 +310,7 @@ export const productRouter = router({
storeId: z.number().min(1, 'Store is required'),
incrementStep: z.number().optional().default(1),
skus: z.array(z.object({
id: z.number().optional(),
name: z.string().optional().nullable(),
price: z.number().positive('Price must be positive'),
marketPrice: z.number().optional().nullable(),
@ -334,6 +335,7 @@ export const productRouter = router({
const allUploadUrls: string[] = skus.flatMap((sku) => sku.images)
const skuInputs = skus.map((sku) => ({
id: sku.id,
name: sku.name ?? null,
price: sku.price,
marketPrice: sku.marketPrice ?? null,

View file

@ -209,7 +209,7 @@ export const slotsRouter = router({
});
}
const result = await updateSlotProductsInDb(String(slotId), productIds.map(String))
const result = await updateSlotProductsInDb(String(slotId), skuIds.map(String))
/*
// Old implementation - direct DB queries:

View file

@ -403,7 +403,7 @@ export const vendorSnippetsRouter = router({
const filteredOrders = matchingOrders.filter(order => {
const status = order.orderStatus;
if (status[0].isCancelled) return false;
const orderProductIds = order.orderItems.map(item => item.productId);
const orderProductIds = order.orderItems.map(item => item.skuId);
return snippet.skuIds.some(productId => orderProductIds.includes(productId));
});
@ -411,17 +411,17 @@ export const vendorSnippetsRouter = router({
const formattedOrders = filteredOrders.map(order => {
// Filter orderItems to only include products attached to the snippet
const attachedOrderItems = order.orderItems.filter(item =>
snippet.skuIds.includes(item.productId)
snippet.skuIds.includes(item.skuId)
);
const products = attachedOrderItems.map(item => ({
orderItemId: item.id,
productId: item.productId,
productName: item.product.name,
productId: item.skuId,
productName: item.sku?.product?.name || 'Unknown',
quantity: parseFloat(item.quantity),
productSize: item.product.productQuantity,
productSize: 1,
price: parseFloat((item.price ?? 0).toString()),
unit: item.product.unit?.shortNotation || 'unit',
unit: (item.sku?.features || []).map((f: any) => f.featureValue).join(' ') || 'unit',
subtotal: parseFloat((item.price ?? 0).toString()) * parseFloat(item.quantity),
is_packaged: item.is_packaged,
is_package_verified: item.is_package_verified,
@ -488,9 +488,9 @@ export const vendorSnippetsRouter = router({
orderDate: order.createdAt ? order.createdAt.toISOString() : new Date(0).toISOString(),
totalQuantity: order.orderItems.reduce((sum, item) => sum + parseFloat(item.quantity || '0'), 0),
products: order.orderItems.map(item => ({
name: item.product.name,
name: item.sku?.product?.name || 'Unknown',
quantity: parseFloat(item.quantity || '0'),
unit: item.product.unit?.shortNotation || 'unit',
unit: (item.sku?.features || []).map((f: any) => f.featureValue).join(' ') || 'unit',
})),
}))
}),
@ -588,7 +588,7 @@ export const vendorSnippetsRouter = router({
const filteredOrders = matchingOrders.filter(order => {
const status = order.orderStatus;
if (status[0]?.isCancelled) return false;
const orderProductIds = order.orderItems.map(item => item.productId);
const orderProductIds = order.orderItems.map(item => item.skuId);
return snippet.skuIds.some(productId => orderProductIds.includes(productId));
});
@ -596,18 +596,18 @@ export const vendorSnippetsRouter = router({
const formattedOrders = filteredOrders.map(order => {
// Filter orderItems to only include products attached to the snippet
const attachedOrderItems = order.orderItems.filter(item =>
snippet.skuIds.includes(item.productId)
snippet.skuIds.includes(item.skuId)
);
const products = attachedOrderItems.map(item => ({
orderItemId: item.id,
productId: item.productId,
productName: item.product.name,
productId: item.skuId,
productName: item.sku?.product?.name || 'Unknown',
quantity: parseFloat(item.quantity),
price: parseFloat((item.price ?? 0).toString()),
unit: item.product.unit?.shortNotation || 'unit',
unit: (item.sku?.features || []).map((f: any) => f.featureValue).join(' ') || 'unit',
subtotal: parseFloat((item.price ?? 0).toString()) * parseFloat(item.quantity),
productSize: item.product.productQuantity,
productSize: 1,
is_packaged: item.is_packaged,
is_package_verified: item.is_package_verified,
}));

View file

@ -461,6 +461,7 @@ export default function CartPage({ isFlashDelivery = false }: CartPageProps) {
</MyText>
<MyText style={tw`text-xs text-gray-500 mr-2`}>
{(() => {
const unit = product?.unitNotation || '';
return unit;
})()}
</MyText>

View file

@ -67,7 +67,7 @@ export default function AddToCartDialog() {
// Pre-select cart's slotId and quantity if item is already in cart
useEffect(() => {
if (isOpen && product) {
const cartItem = cartData?.items?.find((item: any) => item.productId === product.id);
const cartItem = cartData?.items?.find((item: any) => item.skuId === product.id);
const cartQuantity = cartItem?.quantity || 0;
// Set quantity: 0 → 1, >1 → keep as is
@ -109,7 +109,7 @@ export default function AddToCartDialog() {
.filter((slot) => dayjs(slot.freezeTime).isAfter(dayjs()));
// Find cart item for this product
const cartItem = cartData?.items?.find((item: any) => item.productId === product?.id);
const cartItem = cartData?.items?.find((item: any) => item.skuId === product?.id);
// Determine if updating existing item (quantity > 1 means it's an update)
const isUpdate = (cartItem?.quantity || 0) >= 1;

View file

@ -79,11 +79,11 @@ export {
checkProductExistsByName,
checkUnitExists,
getProductImagesById,
createSpecialDealsForProduct,
updateProductDeals,
createSpecialDealsForSku,
updateSkuDeals,
replaceProductTags,
mergeSkus,
toggleProductOutOfStock,
toggleSkuOutOfStock,
updateSlotProducts,
getSlotProductIds,
getSlotsProductIds,

View file

@ -109,7 +109,7 @@ const mapSku = (sku: SkuRow, features: SkuFeatureRow[] = []): AdminSku => ({
const mapSpecialDeal = (deal: SpecialDealRow): AdminSpecialDeal => ({
id: deal.id,
productId: deal.productId,
skuId: deal.skuId,
quantity: String(deal.quantity ?? '0'),
price: String(deal.price ?? '0'),
validTill: deal.validTill,
@ -298,18 +298,37 @@ export async function updateProduct(id: number, input: any): Promise<AdminProduc
throw new Error('Every SKU must have a quantity feature')
}
const existingSkuIds = await db.query.productSkus.findMany({
const existingSkus = await db.query.productSkus.findMany({
where: eq(productSkus.productId, id),
columns: { id: true },
}).then((skus) => skus.map((sku) => sku.id))
})
const existingSkuIdSet = new Set(existingSkus.map((s) => s.id))
if (existingSkuIds.length > 0) {
await db.delete(skuFeatures).where(inArray(skuFeatures.skuId, existingSkuIds))
await db.delete(productSkus).where(inArray(productSkus.id, existingSkuIds))
}
for (const sku of skus) {
if (sku.id != null && existingSkuIdSet.has(sku.id)) {
// Update existing SKU
await db.update(productSkus)
.set({
name: sku.name ?? null,
price: String(sku.price),
marketPrice: sku.marketPrice != null ? String(sku.marketPrice) : null,
images: sku.images ?? null,
isFlashAvailable: sku.isFlashAvailable ?? false,
flashPrice: sku.flashPrice != null ? String(sku.flashPrice) : null,
})
.where(eq(productSkus.id, sku.id))
const skuRows = await db.insert(productSkus).values(
skus.map((sku: any) => ({
await db.delete(skuFeatures).where(eq(skuFeatures.skuId, sku.id))
await db.insert(skuFeatures).values(
sku.features.map((f: any) => ({
skuId: sku.id,
featureName: f.featureName,
featureValue: f.featureValue,
}))
)
} else {
// Insert new SKU
const [newSku] = await db.insert(productSkus).values({
productId: id,
name: sku.name ?? null,
price: String(sku.price),
@ -317,20 +336,18 @@ export async function updateProduct(id: number, input: any): Promise<AdminProduc
images: sku.images ?? null,
isFlashAvailable: sku.isFlashAvailable ?? false,
flashPrice: sku.flashPrice != null ? String(sku.flashPrice) : null,
}))
).returning()
}).returning()
for (let i = 0; i < skuRows.length; i++) {
const sku = skus[i]
await db.insert(skuFeatures).values(
sku.features.map((f: any) => ({
skuId: skuRows[i].id,
skuId: newSku.id,
featureName: f.featureName,
featureValue: f.featureValue,
}))
)
}
}
}
const updatedProduct = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
@ -353,9 +370,9 @@ export async function updateProduct(id: number, input: any): Promise<AdminProduc
}
}
export async function toggleProductOutOfStock(id: number): Promise<AdminProduct | null> {
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
export async function toggleSkuOutOfStock(id: number): Promise<AdminSku | null> {
const product = await db.query.productSkus.findFirst({
where: eq(productSkus.id, id),
})
if (!product) {
@ -363,18 +380,18 @@ export async function toggleProductOutOfStock(id: number): Promise<AdminProduct
}
const [updatedProduct] = await db
.update(productInfo)
.update(productSkus)
.set({
isOutOfStock: !product.isOutOfStock,
})
.where(eq(productInfo.id, id))
.where(eq(productSkus.id, id))
.returning()
if (!updatedProduct) {
return null
}
return mapProduct(updatedProduct)
return mapSku(updatedProduct)
}
export async function updateSlotProducts(slotId: string, productIds: string[]): Promise<AdminUpdateSlotProductsResult> {
@ -386,16 +403,15 @@ export async function updateSlotProducts(slotId: string, productIds: string[]):
throw new Error(`Slot ${slotId} not found`)
}
const currentProductIds = slot.productIds || []
const newProductIds = productIds.map((id: string) => parseInt(id))
const currentSkuIds = slot.skuIds || []
const newSkuIds = productIds.map((id: string) => parseInt(id))
// Simply update the productIds array
await db.update(deliverySlotInfo)
.set({ productIds: newProductIds })
.set({ skuIds: newSkuIds })
.where(eq(deliverySlotInfo.id, parseInt(slotId)))
const productsToAdd = newProductIds.filter((id: number) => !currentProductIds.includes(id))
const productsToRemove = currentProductIds.filter((id: number) => !newProductIds.includes(id))
const productsToAdd = newSkuIds.filter((id: number) => !currentSkuIds.includes(id))
const productsToRemove = currentSkuIds.filter((id: number) => !newSkuIds.includes(id))
return {
message: 'Slot products updated successfully',
@ -409,7 +425,7 @@ export async function getSlotProductIds(slotId: string): Promise<number[]> {
where: eq(deliverySlotInfo.id, parseInt(slotId)),
})
return slot?.productIds || []
return slot?.skuIds || []
}
export async function getAllUnits(): Promise<AdminUnit[]> {
@ -573,7 +589,7 @@ export async function getSlotsProductIds(slotIds: number[]): Promise<Record<numb
const result: Record<number, number[]> = {}
for (const slot of slots) {
result[slot.id] = slot.productIds || []
result[slot.id] = slot.skuIds || []
}
slotIds.forEach((slotId) => {
@ -856,8 +872,8 @@ export async function checkUnitExists(unitId: number): Promise<boolean> {
}
export async function getProductImagesById(productId: number): Promise<string[] | null> {
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, productId),
const product = await db.query.productSkus.findFirst({
where: eq(productSkus.id, productId),
columns: { images: true },
})
@ -874,7 +890,7 @@ export interface CreateSpecialDealInput {
validTill: string | Date
}
export async function createSpecialDealsForProduct(
export async function createSpecialDealsForSku(
productId: number,
deals: CreateSpecialDealInput[]
): Promise<AdminSpecialDeal[]> {
@ -897,17 +913,17 @@ export async function createSpecialDealsForProduct(
return createdDeals.map(mapSpecialDeal)
}
export async function updateProductDeals(
export async function updateSkuDeals(
productId: number,
deals: CreateSpecialDealInput[]
): Promise<void> {
if (deals.length === 0) {
await db.delete(specialDeals).where(eq(specialDeals.productId, productId))
await db.delete(specialDeals).where(eq(specialDeals.skuId, productId))
return
}
const existingDeals = await db.query.specialDeals.findMany({
where: eq(specialDeals.productId, productId),
where: eq(specialDeals.skuId, productId),
})
const existingDealsMap = new Map<string, SpecialDealRow>(

View file

@ -156,9 +156,10 @@ export async function getVendorOrdersBySlotId(slotId: number) {
with: {
orderItems: {
with: {
product: {
sku: {
with: {
unit: true,
product: true,
features: true,
},
},
},
@ -177,9 +178,10 @@ export async function getVendorOrders() {
user: true,
orderItems: {
with: {
product: {
sku: {
with: {
unit: true,
product: true,
features: true,
},
},
},
@ -193,9 +195,10 @@ export async function getOrderItemsByOrderIds(orderIds: number[]) {
return await db.query.orderItems.findMany({
where: inArray(orderItems.orderId, orderIds),
with: {
product: {
sku: {
with: {
unit: true,
product: true,
features: true,
},
},
},

View file

@ -5,7 +5,7 @@
import { db } from '@/src/db/db_index'
import {
userDetails,
productInfo,
productSkus,
productTagInfo,
complaints,
} from '@/src/db/schema'
@ -43,21 +43,21 @@ async function migrateUserDetails() {
}
async function migrateProductInfo() {
console.log('Migrating productInfo...')
const products = await db.select().from(productInfo).where(not(isNull(productInfo.images)))
console.log('Migrating productSkus...')
const products = await db.select().from(productSkus).where(not(isNull(productSkus.images)))
console.log(`Found ${products.length} product records with images`)
for (const product of products) {
if (product.images && Array.isArray(product.images)) {
const cleanedUrls = cleanImageUrls(product.images)
await db.update(productInfo)
await db.update(productSkus)
.set({ images: cleanedUrls })
.where(eq(productInfo.id, product.id))
.where(eq(productSkus.id, product.id))
}
}
console.log('productInfo migration completed')
console.log('productSkus migration completed')
}
async function migrateProductTagInfo() {

View file

@ -1,5 +1,5 @@
import { db } from '../db/db_index'
import { cartItems, productInfo, units } from '../db/schema'
import { cartItems, productSkus } from '../db/schema'
import { and, eq, sql } from 'drizzle-orm'
import type { UserCartItem } from '@packages/shared'
@ -9,55 +9,51 @@ const getStringArray = (value: unknown): string[] => {
}
export async function getCartItemsWithProducts(userId: number): Promise<UserCartItem[]> {
const cartItemsWithProducts = await db
.select({
cartId: cartItems.id,
productId: productInfo.id,
productName: productInfo.name,
productPrice: productInfo.price,
productImages: productInfo.images,
productQuantity: productInfo.productQuantity,
isOutOfStock: productInfo.isOutOfStock,
unitShortNotation: units.shortNotation,
quantity: cartItems.quantity,
addedAt: cartItems.addedAt,
const cartItemsWithProducts = await db.query.cartItems.findMany({
where: eq(cartItems.userId, userId),
with: {
sku: {
with: {
product: true,
features: true,
},
},
},
})
.from(cartItems)
.innerJoin(productInfo, eq(cartItems.productId, productInfo.id))
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(eq(cartItems.userId, userId))
return cartItemsWithProducts.map((item) => {
const priceValue = item.productPrice ?? '0'
const sku = item.sku
const features = sku?.features || []
const priceValue = sku?.price ?? '0'
const quantityValue = item.quantity ?? '0'
return {
id: item.cartId,
productId: item.productId,
id: item.id,
skuId: item.skuId,
quantity: parseFloat(quantityValue),
addedAt: item.addedAt,
product: {
id: item.productId,
name: item.productName,
price: priceValue.toString(),
productQuantity: item.productQuantity,
unit: item.unitShortNotation,
isOutOfStock: item.isOutOfStock,
images: getStringArray(item.productImages),
id: sku?.id ?? 0,
name: sku?.product?.name ?? 'Unknown',
price: String(priceValue),
productQuantity: 1,
unit: features.map((f) => f.featureValue).join(' '),
isOutOfStock: sku?.isOutOfStock ?? false,
images: getStringArray(sku?.images),
},
subtotal: parseFloat(priceValue.toString()) * parseFloat(quantityValue),
subtotal: parseFloat(String(priceValue)) * parseFloat(quantityValue),
}
})
}
export async function getProductById(productId: number) {
return db.query.productInfo.findFirst({
where: eq(productInfo.id, productId),
export async function getProductById(skuId: number) {
return db.query.productSkus.findFirst({
where: eq(productSkus.id, skuId),
})
}
export async function getCartItemByUserProduct(userId: number, productId: number) {
export async function getCartItemByUserProduct(userId: number, skuId: number) {
return db.query.cartItems.findFirst({
where: and(eq(cartItems.userId, userId), eq(cartItems.productId, productId)),
where: and(eq(cartItems.userId, userId), eq(cartItems.skuId, skuId)),
})
}
@ -69,10 +65,10 @@ export async function incrementCartItemQuantity(itemId: number, quantity: number
.where(eq(cartItems.id, itemId))
}
export async function insertCartItem(userId: number, productId: number, quantity: number): Promise<void> {
export async function insertCartItem(userId: number, skuId: number, quantity: number): Promise<void> {
await db.insert(cartItems).values({
userId,
productId,
skuId,
quantity: quantity.toString(),
})
}

View file

@ -649,32 +649,32 @@ export async function getProductsForRecentOrders(
productIds: number[],
limit: number
): Promise<RecentProductData[]> {
const results = await db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
price: productInfo.price,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
unitShortNotation: units.shortNotation,
incrementStep: productInfo.incrementStep,
const skus = await db.query.productSkus.findMany({
where: and(
inArray(productSkus.id, productIds),
eq(productSkus.isSuspended, false)
),
with: {
product: true,
features: true,
},
orderBy: desc(productSkus.createdAt),
limit,
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(
and(
inArray(productInfo.id, productIds),
eq(productInfo.isSuspended, false)
)
)
.orderBy(desc(productInfo.createdAt))
.limit(limit)
return results.map((product) => ({
...product,
price: String(product.price ?? '0'),
}))
return skus.map((sku) => {
const features = sku.features || []
return {
id: sku.id,
name: sku.product?.name ?? 'Unknown',
shortDescription: sku.product?.shortDescription ?? null,
price: String(sku.price ?? '0'),
images: sku.images,
isOutOfStock: sku.isOutOfStock,
unitShortNotation: features.map((f) => f.featureValue).join(' '),
incrementStep: sku.product?.incrementStep ?? 1,
}
})
}
// ============================================================================
@ -697,9 +697,11 @@ export interface OrderWithFullData {
} | null
orderItems: Array<{
quantity: string
sku: {
product: {
name: string
} | null
} | null
}>
slot: {
deliveryTime: Date
@ -777,6 +779,8 @@ export async function getOrderByIdWithFullData(
},
},
orderItems: {
with: {
sku: {
with: {
product: {
columns: {
@ -785,6 +789,8 @@ export async function getOrderByIdWithFullData(
},
},
},
},
},
slot: {
columns: {
deliveryTime: true,

View file

@ -1,5 +1,5 @@
import { db } from '../db/db_index'
import { deliverySlotInfo, productInfo, productSkus, productReviews, productTags, specialDeals, storeInfo, units, users } from '../db/schema'
import { deliverySlotInfo, productInfo, productSkus, productReviews, productTags, specialDeals, storeInfo, users } from '../db/schema'
import { and, desc, eq, gt, sql } from 'drizzle-orm'
import type { UserProductDetailData, UserProductReview } from '@packages/shared'
@ -177,30 +177,32 @@ export async function getAllProductsWithUnits(tagId?: number): Promise<ProductSu
// Filter by product IDs if tag filtering is applied
if (productIds && productIds.length > 0) {
whereCondition = inArray(productInfo.id, productIds)
whereCondition = inArray(productSkus.productId, productIds)
}
const results = await db
.select({
id: productInfo.id,
name: productInfo.name,
shortDescription: productInfo.shortDescription,
price: productInfo.price,
marketPrice: productInfo.marketPrice,
images: productInfo.images,
isOutOfStock: productInfo.isOutOfStock,
unitShortNotation: units.shortNotation,
productQuantity: productInfo.productQuantity,
const skus = await db.query.productSkus.findMany({
where: whereCondition,
with: {
product: true,
features: true,
},
})
.from(productInfo)
.innerJoin(units, eq(productInfo.unitId, units.id))
.where(whereCondition)
return results.map((product) => ({
...product,
price: String(product.price ?? '0'),
marketPrice: product.marketPrice ? String(product.marketPrice) : null,
}))
return skus.map((sku) => {
const features = sku.features || []
return {
id: sku.product?.id ?? 0,
name: sku.product?.name ?? 'Unknown',
skuId: sku.id,
skuName: sku.name ?? null,
shortDescription: sku.product?.shortDescription ?? null,
price: String(sku.price ?? '0'),
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
images: sku.images,
isOutOfStock: sku.isOutOfStock,
features: features.map((f) => ({ featureName: f.featureName, featureValue: f.featureValue })),
}
})
}
/**

View file

@ -372,13 +372,7 @@ export interface AdminSkuVariant {
export interface AdminSku {
id: number
productId: number
productName?: string
skuCode: string | null
displayName: string
unitId: number
unit?: AdminUnit
productQuantity: number
incrementStep: number
name: string | null
price: string
marketPrice: string | null
images: string[] | null
@ -387,11 +381,9 @@ export interface AdminSku {
isSuspended: boolean
isFlashAvailable: boolean
flashPrice: string | null
isComboOnly: boolean
sortOrder: number
isDefault: boolean
createdAt: Date
variants: AdminSkuVariant[]
features: AdminSkuFeature[]
}
}
export interface AdminProduct {
@ -443,6 +435,8 @@ export interface CreateProductInput {
incrementStep?: number
skus: CreateSkuInput[]
}
export interface AdminProductTagInfo {
id: number;
tagName: string;
tagDescription: string | null;
@ -465,7 +459,7 @@ export interface AdminProductTagWithProducts extends AdminProductTagInfo {
export interface AdminSpecialDeal {
id: number;
productId: number;
skuId: number;
quantity: string;
price: string;
validTill: Date;