Compare commits

..

No commits in common. "db2ee44a5f61f618c72d3b699c9e5285721c9a92" and "1b62c6add49c9b9539cbc9d94d8ed7be043ed91c" have entirely different histories.

33 changed files with 569 additions and 608 deletions

View file

@ -1,9 +1,4 @@
# Taste
# Taste (Continuously Learned by [CommandCode][cmd])
- Prefers that the agent not run typechecks; instead, read the project's AGENTS.md for instructions on what to do/not do before finalizing work. Confidence: 0.9
[cmd]: https://commandcode.ai/
- Prefers detailed analysis and explicit error/mistake checking before executing git operations (e.g., merges), rather than just performing the action. Confidence: 0.6
- When removing unused code (e.g., an unused tRPC procedure), prefers a complete cleanup: also remove internal helper methods used only by it, associated types, re-exports, orphaned hook/API files and their shared types, and even commented-out imports, leaving zero remaining references. Confidence: 0.9
- Before removing or deciding on code, likes to first verify where it is actually used across the codebase (asks questions like "where is X used"), rather than removing based on assumption. Confidence: 0.4

4
APIS_TO_REMOVE.md Normal file
View file

@ -0,0 +1,4 @@
- trpc.user.tags.getTagsByStore — apps/backend/src/trpc/apis/user-apis/apis/tags.ts
- trpc.common.product.getAllProductsSummary — apps/backend/src/trpc/apis/common-apis/common.ts
- remove slots from products cache
- remove redundant product details like name, description etc from the slots api

View file

@ -43,7 +43,7 @@ const SkuItemComponent: React.FC<SkuItemProps> = ({
const displayPrice = change.price !== undefined ? change.price : sku.price;
const displayMarketPrice = change.marketPrice !== undefined ? change.marketPrice : sku.marketPrice;
const displayFlashPrice = change.flashPrice !== undefined ? change.flashPrice : sku.flashPrice;
const displayUnit = (sku.features || []).map((f: any) => f.featureValue).join(' ') || '—';
const displayProductQuantity = change.productQuantity !== undefined ? change.productQuantity : sku.productQuantity;
return (
<View style={tw`bg-white p-4 mb-3 rounded-xl border border-gray-200 shadow-sm`}>
@ -130,7 +130,7 @@ const SkuItemComponent: React.FC<SkuItemProps> = ({
<View style={tw`items-center`}>
<MyText style={tw`text-xs text-gray-500 mb-1`}>Size</MyText>
<View style={tw`flex-row items-center`}>
<MyText style={tw`text-sm text-blue-600`}>{displayUnit}</MyText>
<MyText style={tw`text-sm text-blue-600`}>{displayProductQuantity ? `${displayProductQuantity}${sku.unit?.shortNotation || ''}` : "N/A"}</MyText>
<TouchableOpacity onPress={() => openEditDialog(sku, productName)} style={tw`ml-1`}>
<MaterialIcons name="edit" size={14} color="#6b7280" />
</TouchableOpacity>
@ -145,6 +145,7 @@ interface PendingChange {
price?: number;
marketPrice?: number | null;
flashPrice?: number | null;
productQuantity?: number | null;
isFlashAvailable?: boolean;
}
@ -155,6 +156,7 @@ interface EditDialogState {
tempPrice: string;
tempMarketPrice: string;
tempFlashPrice: string;
tempProductQuantity: string;
}
export default function PricesOverview() {
@ -168,6 +170,7 @@ export default function PricesOverview() {
tempPrice: "",
tempMarketPrice: "",
tempFlashPrice: "",
tempProductQuantity: "",
});
const [showMenu, setShowMenu] = useState(false);
@ -230,6 +233,7 @@ export default function PricesOverview() {
tempPrice: (change.price ?? sku.price)?.toString() || "",
tempMarketPrice: (change.marketPrice ?? sku.marketPrice)?.toString() || "",
tempFlashPrice: (change.flashPrice ?? sku.flashPrice)?.toString() || "",
tempProductQuantity: (change.productQuantity ?? sku.productQuantity)?.toString() || "",
});
};
@ -237,6 +241,7 @@ export default function PricesOverview() {
const price = parseFloat(editDialog.tempPrice);
const marketPrice = editDialog.tempMarketPrice ? parseFloat(editDialog.tempMarketPrice) : null;
const flashPrice = editDialog.tempFlashPrice ? parseFloat(editDialog.tempFlashPrice) : null;
const productQuantity = editDialog.tempProductQuantity ? parseFloat(editDialog.tempProductQuantity) : null;
if (isNaN(price) || price <= 0) {
Alert.alert("Error", "Please enter a valid price");
@ -253,24 +258,32 @@ export default function PricesOverview() {
return;
}
if (editDialog.tempProductQuantity && (isNaN(productQuantity!) || productQuantity! <= 0)) {
Alert.alert("Error", "Please enter a valid size");
return;
}
setPendingChanges(prev => ({
...prev,
[editDialog.sku.id]: {
price: price !== parseFloat(editDialog.sku.price) ? price : undefined,
marketPrice: marketPrice !== parseFloat(editDialog.sku.marketPrice || '0') ? marketPrice : undefined,
flashPrice: flashPrice !== parseFloat(editDialog.sku.flashPrice || '0') ? flashPrice : undefined,
productQuantity: productQuantity !== (editDialog.sku.productQuantity || 1) ? productQuantity : undefined,
},
}));
setEditDialog({ open: false, sku: null, productName: "", tempPrice: "", tempMarketPrice: "", tempFlashPrice: "", tempProductQuantity: "" });
};
const handleSave = () => {
const updates = Object.entries(pendingChanges).map(([skuId, change]) => {
const sku = allSkus.find(s => s.id === parseInt(skuId));
const update: any = { productId: sku?.id };
const update: any = { productId: sku?.productId };
if (change.price !== undefined) update.price = change.price;
if (change.marketPrice !== undefined) update.marketPrice = change.marketPrice;
if (change.flashPrice !== undefined) update.flashPrice = change.flashPrice;
if (change.productQuantity !== undefined) update.productQuantity = change.productQuantity;
if (change.isFlashAvailable !== undefined) update.isFlashAvailable = change.isFlashAvailable;
return update;
});
@ -364,7 +377,7 @@ export default function PricesOverview() {
/>
)}
<BottomDialog open={editDialog.open} onClose={() => setEditDialog({ ...editDialog, open: false, tempFlashPrice: "", tempMarketPrice: "", tempPrice: "" })}>
<BottomDialog open={editDialog.open} onClose={() => setEditDialog({ ...editDialog, open: false, tempFlashPrice: "", tempMarketPrice: "", tempPrice: "", tempProductQuantity: "" })}>
<View style={tw`p-4`}>
<MyText style={tw`text-lg font-bold mb-1`}>{editDialog.productName}</MyText>
<MyText style={tw`text-sm text-gray-500 mb-4`}>{editDialog.sku?.displayName || editDialog.sku?.name || ''}</MyText>
@ -406,6 +419,8 @@ export default function PricesOverview() {
<MyText style={tw`text-sm font-medium mb-1`}>Size</MyText>
<TextInput
style={tw`border border-gray-300 rounded-md px-3 py-2`}
value={editDialog.tempProductQuantity}
onChangeText={(text) => setEditDialog({ ...editDialog, tempProductQuantity: text })}
keyboardType="decimal-pad"
placeholder="Enter size"
/>

View file

@ -48,9 +48,6 @@ export default function AddProduct() {
featureName: attr.featureName,
featureValue: attr.featureValue,
})),
comboItems: (variant.comboItems || []).map((ci: any) => ({
skuId: typeof ci.skuId === 'string' ? parseInt(ci.skuId) : ci.skuId,
})),
}
})
@ -59,7 +56,6 @@ export default function AddProduct() {
shortDescription: values.shortDescription || undefined,
longDescription: values.longDescription || undefined,
storeId: values.storeId,
productType: values.productType,
incrementStep: 1,
skus,
})
@ -76,17 +72,14 @@ export default function AddProduct() {
shortDescription: '',
longDescription: '',
storeId: 1,
productType: 'item' as const,
variants: [
{
id: undefined as number | undefined,
name: '',
price: '',
marketPrice: '',
isFlashAvailable: false,
flashPrice: '',
attributes: [{ featureName: 'quantity', featureValue: '' }],
comboItems: [] as { skuId: number | string }[],
},
],
}

View file

@ -41,7 +41,6 @@ export default function EditProduct() {
shortDescription: productData.shortDescription || '',
longDescription: productData.longDescription || '',
storeId: productData.storeId || 1,
productType: (productData.productType as 'item' | 'combo') || 'item',
variants: (productData.skus || []).map((sku) => ({
id: sku.id,
name: sku.name || '',
@ -53,9 +52,6 @@ export default function EditProduct() {
featureName: f.featureName,
featureValue: f.featureValue,
})),
comboItems: (sku.comboItems || []).map((ci: any) => ({
skuId: ci.skuId.toString(),
})),
})),
}
}, [productData])
@ -122,9 +118,6 @@ export default function EditProduct() {
featureName: attr.featureName,
featureValue: attr.featureValue,
})),
comboItems: (variant.comboItems || []).map((ci: any) => ({
skuId: typeof ci.skuId === 'string' ? parseInt(ci.skuId) : ci.skuId,
})),
}
})
@ -134,7 +127,6 @@ export default function EditProduct() {
shortDescription: values.shortDescription || undefined,
longDescription: values.longDescription || undefined,
storeId: values.storeId,
productType: values.productType,
incrementStep: 1,
skus,
deletedImageKeys,

View file

@ -18,7 +18,6 @@ interface Variant {
isFlashAvailable: boolean
flashPrice: string
attributes: Attribute[]
comboItems: { skuId: number | string }[]
}
interface ProductFormData {
@ -26,7 +25,6 @@ interface ProductFormData {
shortDescription: string
longDescription: string
storeId: number
productType: 'item' | 'combo'
variants: Variant[]
}
@ -46,14 +44,12 @@ interface ProductFormProps {
const defaultAttribute = (): Attribute => ({ featureName: 'quantity', featureValue: '' })
const defaultVariant = (): Variant => ({
id: undefined,
name: '',
price: '',
marketPrice: '',
isFlashAvailable: false,
flashPrice: '',
attributes: [defaultAttribute()],
comboItems: [],
})
const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
@ -80,12 +76,6 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
value: store.id,
})) || []
const { data: skusData } = trpc.common.product.getAllSkusSummary.useQuery({})
const skuOptions = (skusData?.skus || []).map((sku) => ({
label: sku.label,
value: sku.id.toString(),
}))
return (
<Formik
initialValues={initialValues}
@ -146,19 +136,6 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
style={{ marginBottom: 16 }}
/>
<BottomDropdown
topLabel="Product Type"
label="Product Type"
value={values.productType}
options={[
{ label: 'Item', value: 'item' },
{ label: 'Combo', value: 'combo' },
]}
onValueChange={(value) => setFieldValue('productType', value)}
placeholder="Select product type"
style={{ marginBottom: 16 }}
/>
<FieldArray name="variants">
{({ push, remove }) => (
<View>
@ -294,46 +271,6 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
}
allowMultiple={true}
/>
{values.productType === 'combo' && (
<View style={tw`mt-3 pt-3 border-t border-gray-100`}>
<View style={tw`flex-row justify-between items-center mb-2`}>
<MyText style={tw`font-medium text-gray-600`}>Included Items</MyText>
<TouchableOpacity
onPress={() => {
const items = variant.comboItems || []
items.push({ skuId: '' })
setFieldValue(`variants.${vIndex}.comboItems`, items)
}}
style={tw`bg-gray-200 px-2 py-0.5 rounded flex-row items-center`}
>
<MaterialIcons name="add" size={14} color="#4B5563" />
<MyText style={tw`text-gray-600 text-xs ml-0.5`}>Add</MyText>
</TouchableOpacity>
</View>
{variant.comboItems?.map((ci, cIndex) => (
<View key={cIndex} style={tw`flex-row items-center gap-2 mb-2`}>
<View style={tw`flex-1`}>
<BottomDropdown
label="SKU"
value={ci.skuId}
options={skuOptions}
onValueChange={(val) => setFieldValue(`variants.${vIndex}.comboItems.${cIndex}.skuId`, val)}
placeholder="Select SKU"
/>
</View>
<TouchableOpacity
onPress={() => {
const items = variant.comboItems.filter((_, i) => i !== cIndex)
setFieldValue(`variants.${vIndex}.comboItems`, items)
}}
>
<MaterialIcons name="close" size={18} color="#EF4444" />
</TouchableOpacity>
</View>
))}
</View>
)}
</View>
))}
</View>

View file

@ -5,9 +5,11 @@ import { createApp } from '@/src/app'
// import signedUrlCache from '@/src/lib/signed-url-cache';
import { seed } from '@/src/lib/seed';
import '@/src/jobs/jobs-index';
import { startAutomatedJobs } from '@/src/lib/automatedJobs';
seed()
initFunc()
startAutomatedJobs()
// signedUrlCache.loadFromDisk(); // Disabled for Workers compatibility

View file

@ -51,7 +51,9 @@ export type {
AdminProductListResponse,
AdminProductResponse,
AdminDeleteProductResult,
AdminToggleOutOfStockResult,
AdminUpdateSlotProductsResult,
AdminSlotProductIdsResult,
AdminSlotsProductIdsResult,
AdminProductReview,
AdminProductReviewWithSignedUrls,

View file

@ -0,0 +1,137 @@
// import * as cron from 'node-cron';
const cron:any = {}
import { toggleFlashDeliveryForItems, toggleKeyVal } from '@/src/dbService';
import { CONST_KEYS } from '@/src/lib/const-keys'
import { computeConstants } from '@/src/lib/const-store'
const MUTTON_ITEMS = [
12, //Lamb mutton
14, // Mutton Boti
35, //Mutton Kheema
84, //Mutton Brain
4, //Mutton
86, //Mutton Chops
87, //Mutton Soup bones
85 //Mutton paya
];
export const startAutomatedJobs = () => {
// Job to disable flash delivery for mutton at 12 PM daily
cron.schedule('0 12 * * *', async () => {
try {
console.log('Disabling flash delivery for products at 12 PM');
await toggleFlashDeliveryForItems(false, MUTTON_ITEMS);
console.log('Flash delivery disabled successfully');
} catch (error) {
console.error('Error disabling flash delivery:', error);
}
});
// Job to enable flash delivery for mutton at 6 AM daily
cron.schedule('0 6 * * *', async () => {
try {
console.log('Enabling flash delivery for products at 5 AM');
await toggleFlashDeliveryForItems(true, MUTTON_ITEMS);
console.log('Flash delivery enabled successfully');
} catch (error) {
console.error('Error enabling flash delivery:', error);
}
});
// Job to disable flash delivery feature at 9 PM daily
cron.schedule('0 21 * * *', async () => {
try {
console.log('Disabling flash delivery feature at 9 PM');
await toggleKeyVal(CONST_KEYS.isFlashDeliveryEnabled, false);
await computeConstants(); // Refresh Redis cache
console.log('Flash delivery feature disabled successfully');
} catch (error) {
console.error('Error disabling flash delivery feature:', error);
}
});
// Job to enable flash delivery feature at 6 AM daily
cron.schedule('0 6 * * *', async () => {
try {
console.log('Enabling flash delivery feature at 6 AM');
await toggleKeyVal(CONST_KEYS.isFlashDeliveryEnabled, true);
await computeConstants(); // Refresh Redis cache
console.log('Flash delivery feature enabled successfully');
} catch (error) {
console.error('Error enabling flash delivery feature:', error);
}
});
console.log('Automated jobs scheduled');
};
/*
// Old implementation - direct DB queries:
import { db } from '@/src/db/db_index'
import { productInfo, keyValStore } from '@/src/db/schema'
import { inArray, eq } from 'drizzle-orm';
// Job to disable flash delivery for mutton at 12 PM daily
cron.schedule('0 12 * * *', async () => {
try {
console.log('Disabling flash delivery for products at 12 PM');
await db
.update(productInfo)
.set({ isFlashAvailable: false })
.where(inArray(productInfo.id, MUTTON_ITEMS));
console.log('Flash delivery disabled successfully');
} catch (error) {
console.error('Error disabling flash delivery:', error);
}
});
// Job to enable flash delivery for mutton at 6 AM daily
cron.schedule('0 6 * * *', async () => {
try {
console.log('Enabling flash delivery for products at 5 AM');
await db
.update(productInfo)
.set({ isFlashAvailable: true })
.where(inArray(productInfo.id, MUTTON_ITEMS));
console.log('Flash delivery enabled successfully');
} catch (error) {
console.error('Error enabling flash delivery:', error);
}
});
// Job to disable flash delivery feature at 9 PM daily
cron.schedule('0 21 * * *', async () => {
try {
console.log('Disabling flash delivery feature at 9 PM');
await db
.update(keyValStore)
.set({ value: false })
.where(eq(keyValStore.key, CONST_KEYS.isFlashDeliveryEnabled));
await computeConstants(); // Refresh Redis cache
console.log('Flash delivery feature disabled successfully');
} catch (error) {
console.error('Error disabling flash delivery feature:', error);
}
});
// Job to enable flash delivery feature at 6 AM daily
cron.schedule('0 6 * * *', async () => {
try {
console.log('Enabling flash delivery feature at 6 AM');
await db
.update(keyValStore)
.set({ value: true })
.where(eq(keyValStore.key, CONST_KEYS.isFlashDeliveryEnabled));
await computeConstants(); // Refresh Redis cache
console.log('Flash delivery feature enabled successfully');
} catch (error) {
console.error('Error enabling flash delivery feature:', error);
}
});
*/
// Optional: Call on import if desired, or export and call in main app
// startAutomatedJobs();

View file

@ -64,6 +64,7 @@
// replaceProductTags,
// toggleProductOutOfStock,
// updateSlotProducts,
// getSlotProductIds,
// getSlotsProductIds,
// getAllUnits,
// getAllProductTags,

View file

@ -74,6 +74,7 @@ export {
replaceProductTags,
mergeSkus,
updateSlotProducts,
getSlotProductIds,
getSlotsProductIds,
getAllUnits,
getAllProductTags,
@ -272,6 +273,7 @@ export {
type SlotWithProductsData,
type UserNegativityData,
// Automated Jobs
toggleFlashDeliveryForItems,
toggleKeyVal,
getAllKeyValStore,
// Post-order handler helpers

View file

@ -6,13 +6,11 @@ import {
getAllSpecialDealsForCache,
getAllProductTagsForCache,
getProductById as getProductByIdFromDb,
getAllProductCombosForCache,
type ProductBasicData,
type StoreBasicData,
type DeliverySlotData,
type SpecialDealData,
type ProductTagData,
type ProductComboCacheData,
} from '@/src/dbService'
import { scaffoldAssetUrl } from '@/src/lib/s3-client'
@ -35,15 +33,6 @@ interface Product {
deliverySlots: Array<{ id: number; deliveryTime: Date; freezeTime: Date; isCapacityFull: boolean }>
specialDeals: Array<{ quantity: string; price: string; validTill: Date }>
productTags: string[]
productType: string
comboItems: Array<{
skuId: number
skuName: string | null
unitNotation: string
productName: string
images: string[] | null
price: string
}>
}
export async function initializeProducts(): Promise<void> {
@ -224,14 +213,6 @@ export async function getAllProducts(): Promise<Product[]> {
}
const products: Product[] = []
const allProductCombos = await getAllProductCombosForCache()
const productCombosMap = new Map<number, ProductComboCacheData[]>()
for (const comboItem of allProductCombos) {
if (!productCombosMap.has(comboItem.comboSkuId))
productCombosMap.set(comboItem.comboSkuId, [])
productCombosMap.get(comboItem.comboSkuId)!.push(comboItem)
}
for (const product of productsData) {
const signedImages = scaffoldAssetUrl(
(product.images as string[]) || []
@ -242,14 +223,6 @@ export async function getAllProducts(): Promise<Product[]> {
const deliverySlots = deliverySlotsMap.get(product.id) || []
const specialDeals = specialDealsMap.get(product.id) || []
const productTags = productTagsMap.get(product.productId) || []
const comboItems = (productCombosMap.get(product.id) || []).map((ci) => ({
skuId: ci.skuId,
skuName: ci.skuName,
unitNotation: ci.unitNotation,
productName: ci.productName,
images: ci.images ? scaffoldAssetUrl((ci.images as string[]) || []) : null,
price: ci.price,
}))
products.push({
id: product.id,
@ -280,8 +253,6 @@ export async function getAllProducts(): Promise<Product[]> {
validTill: d.validTill,
})),
productTags: productTags,
productType: product.productType || 'item',
comboItems: comboItems,
})
}

View file

@ -49,7 +49,7 @@ async function transformSlotToStoreSlot(slot: SlotWithProductsData): Promise<Slo
shortDescription: product.shortDescription,
price: product.price.toString(),
marketPrice: product.marketPrice?.toString() || null,
unit: product.unitNotation || null,
unit: product.unit?.shortNotation || null,
images: scaffoldAssetUrl(
(product.images as string[]) || []
),
@ -125,7 +125,7 @@ export async function initializeSlotStore(): Promise<void> {
shortDescription: product.shortDescription,
price: product.price.toString(),
marketPrice: product.marketPrice?.toString() || null,
unit: product.unitNotation || null,
unit: product.unit?.shortNotation || null,
images: scaffoldAssetUrl(
(product.images as string[]) || []
),

View file

@ -8,6 +8,7 @@ import {
getProductById as getProductByIdInDb,
deleteProduct as deleteProductInDb,
updateSlotProducts as updateSlotProductsInDb,
getSlotProductIds as getSlotProductIdsInDb,
getSlotsProductIds as getSlotsProductIdsInDb,
getProductReviews as getProductReviewsInDb,
respondToReview as respondToReviewInDb,
@ -41,6 +42,7 @@ import type {
AdminProductResponse,
AdminDeleteProductResult,
AdminUpdateSlotProductsResult,
AdminSlotProductIdsResult,
AdminSlotsProductIdsResult,
AdminUpdateProductPricesResult,
} from '@packages/shared'
@ -188,7 +190,6 @@ export const productRouter = router({
longDescription: z.string().optional(),
storeId: z.number().min(1, 'Store is required'),
incrementStep: z.number().optional().default(1),
productType: z.enum(['item', 'combo']).optional().default('item'),
skus: z.array(z.object({
name: z.string().optional().nullable(),
price: z.number().positive('Price must be positive'),
@ -200,13 +201,10 @@ export const productRouter = router({
featureName: z.string().min(1, 'Attribute name is required'),
featureValue: z.string().min(1, 'Value is required'),
})).min(1, 'At least one feature is required'),
comboItems: z.array(z.object({
skuId: z.number().int().positive(),
})).optional(),
})).min(1, 'At least one SKU is required'),
}))
.mutation(async ({ input }): Promise<{ product: AdminProductWithRelations; message: string }> => {
const { name, shortDescription, longDescription, storeId, incrementStep, productType, skus } = input
const { name, shortDescription, longDescription, storeId, incrementStep, skus } = input
const existingProduct = await checkProductExistsByName(name.trim())
if (existingProduct) {
@ -226,7 +224,6 @@ export const productRouter = router({
featureName: f.featureName,
featureValue: f.featureValue,
})),
comboItems: (sku.comboItems || []).map((ci) => ({ skuId: ci.skuId })),
}))
const newProduct = await createProductInDb({
@ -235,7 +232,6 @@ export const productRouter = router({
longDescription,
storeId,
incrementStep,
productType,
skus: skuInputs,
} as any)
@ -269,7 +265,6 @@ export const productRouter = router({
longDescription: z.string().optional(),
storeId: z.number().min(1, 'Store is required'),
incrementStep: z.number().optional().default(1),
productType: z.enum(['item', 'combo']).optional(),
skus: z.array(z.object({
id: z.number().optional(),
name: z.string().optional().nullable(),
@ -282,15 +277,12 @@ export const productRouter = router({
featureName: z.string().min(1, 'Attribute name is required'),
featureValue: z.string().min(1, 'Value is required'),
})).min(1, 'At least one feature is required'),
comboItems: z.array(z.object({
skuId: z.number().int().positive(),
})).optional(),
})).min(1, 'At least one SKU is required'),
deletedImageKeys: z.array(z.string()).optional().default([]),
newImageUrls: z.array(z.string()).optional().default([]),
}))
.mutation(async ({ input }): Promise<{ product: AdminProductWithRelations; message: string }> => {
const { id, name, shortDescription, longDescription, storeId, incrementStep, productType, skus, deletedImageKeys, newImageUrls } = input
const { id, name, shortDescription, longDescription, storeId, incrementStep, skus, deletedImageKeys, newImageUrls } = input
if (deletedImageKeys.length > 0) {
await deleteImageUtil({ keys: deletedImageKeys })
@ -310,7 +302,6 @@ export const productRouter = router({
featureName: f.featureName,
featureValue: f.featureValue,
})),
comboItems: (sku.comboItems || []).map((ci) => ({ skuId: ci.skuId })),
}))
const updatedProduct = await updateProductInDb(id, {
@ -319,7 +310,6 @@ export const productRouter = router({
longDescription,
storeId,
incrementStep,
productType,
skus: skuInputs,
} as any)
@ -419,6 +409,31 @@ export const productRouter = router({
}
}),
getSlotProductIds: protectedProcedure
.input(z.object({
slotId: z.string(),
}))
.query(async ({ input }): Promise<AdminSlotProductIdsResult> => {
const { slotId } = input;
const skuIds = await getSlotProductIdsInDb(slotId)
/*
// Old implementation - direct DB queries:
const associations = await db.query.productSlots.findMany({
where: eq(productSlots.slotId, parseInt(slotId)),
columns: {
productId: true,
},
});
const productIds = associations.map(assoc => assoc.productId);
return {
skuIds,
}
}),
getSlotsProductIds: protectedProcedure
.input(z.object({
slotIds: z.array(z.number()),

View file

@ -85,6 +85,7 @@ export {
replaceProductTags,
toggleProductOutOfStock,
updateSlotProducts,
getSlotProductIds,
getSlotsProductIds,
getAllUnits,
getAllProductTags,

View file

@ -229,6 +229,14 @@ export async function updateSlotProducts(slotId: string, productIds: string[]):
}
}
export async function getSlotProductIds(slotId: string): Promise<number[]> {
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, parseInt(slotId)),
})
return slot?.productIds || []
}
export async function getAllUnits(): Promise<AdminUnit[]> {
const allUnits = await db.query.units.findMany({
orderBy: units.shortNotation,

View file

@ -232,18 +232,5 @@ WHERE `key` = 'popularItems'
-- 9. Clean up helper table.
DROP TABLE `__product_to_sku`;
-- 10. Add product_type column and product_combos table for combo products.
ALTER TABLE `product_info` ADD COLUMN `product_type` text DEFAULT 'item';
CREATE TABLE `product_combos` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`combo_sku_id` integer NOT NULL,
`sku_id` integer NOT NULL,
FOREIGN KEY (`combo_sku_id`) REFERENCES `product_skus`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`sku_id`) REFERENCES `product_skus`(`id`) ON UPDATE no action ON DELETE no action
);
CREATE UNIQUE INDEX `unique_combo_sku_item` ON `product_combos` (`combo_sku_id`,`sku_id`);
-- PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys = off;

View file

@ -84,6 +84,7 @@ export {
replaceProductTags,
mergeSkus,
updateSlotProducts,
getSlotProductIds,
getSlotsProductIds,
getAllUnits,
getAllProductTags,
@ -330,13 +331,11 @@ export {
getAllDeliverySlotsForCache,
getAllSpecialDealsForCache,
getAllProductTagsForCache,
getAllProductCombosForCache,
type ProductBasicData,
type StoreBasicData,
type DeliverySlotData,
type SpecialDealData,
type ProductTagData,
type ProductComboCacheData,
// Product Tag Store
getAllTagsForCache,
getAllTagProductMappings,
@ -353,6 +352,7 @@ export {
// Automated Jobs Helpers
export {
toggleFlashDeliveryForItems,
toggleKeyVal,
getAllKeyValStore,
} from './src/lib/automated-jobs'

View file

@ -4,7 +4,6 @@ import {
productInfo,
productSkus,
skuFeatures,
productCombos,
units,
specialDeals,
deliverySlotInfo,
@ -25,7 +24,6 @@ import {
couponApplicableProducts,
} from '../db/schema'
import { and, desc, eq, inArray, sql } from 'drizzle-orm'
import { runBatched } from '../lib/run-batched'
import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'
import type {
AdminProduct,
@ -84,7 +82,6 @@ const mapProduct = (product: ProductRow): AdminProduct => ({
storeId: product.storeId,
incrementStep: product.incrementStep,
createdAt: product.createdAt,
productType: product.productType as 'item' | 'combo',
})
const mapSkuFeature = (feature: SkuFeatureRow): AdminSkuFeature => ({
@ -94,7 +91,7 @@ const mapSkuFeature = (feature: SkuFeatureRow): AdminSkuFeature => ({
featureValue: feature.featureValue,
})
const mapSku = (sku: SkuRow, features: SkuFeatureRow[] = [], comboItems: any[] = []): AdminSku => ({
const mapSku = (sku: SkuRow, features: SkuFeatureRow[] = []): AdminSku => ({
id: sku.id,
productId: sku.productId,
name: sku.name ?? null,
@ -108,7 +105,6 @@ const mapSku = (sku: SkuRow, features: SkuFeatureRow[] = [], comboItems: any[] =
flashPrice: sku.flashPrice ? String(sku.flashPrice) : null,
createdAt: sku.createdAt,
features: features.map(mapSkuFeature),
comboItems: comboItems,
})
const mapSpecialDeal = (deal: SpecialDealRow): AdminSpecialDeal => ({
@ -132,7 +128,7 @@ const mapTagInfo = (tag: ProductTagInfoRow): AdminProductTagInfo => ({
export async function getAllProducts(): Promise<AdminProductWithRelations[]> {
type ProductWithRelationsRow = ProductRow & {
store: StoreRow | null
skus: Array<SkuRow & { features: SkuFeatureRow[]; comboItems: any[] }>
skus: Array<SkuRow & { features: SkuFeatureRow[] }>
}
const products = await db.query.productInfo.findMany({
orderBy: productInfo.name,
@ -141,11 +137,6 @@ export async function getAllProducts(): Promise<AdminProductWithRelations[]> {
skus: {
with: {
features: true,
comboItems: {
with: {
sku: { with: { product: true, features: true } },
},
},
},
},
},
@ -154,17 +145,7 @@ export async function getAllProducts(): Promise<AdminProductWithRelations[]> {
return products.map((product) => ({
...mapProduct(product),
store: product.store ? mapStore(product.store) : null,
skus: product.skus.map((sku) => {
const comboItems = (sku.comboItems || []).map((ci: any) => ({
skuId: ci.skuId,
skuName: ci.sku?.name ?? null,
features: (ci.sku?.features || []).map((f: any) => ({ featureName: f.featureName, featureValue: f.featureValue })),
productName: ci.sku?.product?.name ?? 'Unknown',
images: getStringArray(ci.sku?.images),
price: String(ci.sku?.price ?? '0'),
}))
return mapSku(sku, sku.features, comboItems)
}),
skus: product.skus.map((sku) => mapSku(sku, sku.features)),
}))
}
@ -176,11 +157,6 @@ export async function getProductById(id: number): Promise<AdminProductWithDetail
skus: {
with: {
features: true,
comboItems: {
with: {
sku: { with: { product: true, features: true } },
},
},
},
},
},
@ -206,22 +182,10 @@ export async function getProductById(id: number): Promise<AdminProductWithDetail
},
}) as Array<ProductTagRow & { tag: ProductTagInfoRow }>
const skusWithCombos = product.skus.map((sku) => {
const comboItems = (sku.comboItems || []).map((ci: any) => ({
skuId: ci.skuId,
skuName: ci.sku?.name ?? null,
features: (ci.sku?.features || []).map((f) => ({ featureName: f.featureName, featureValue: f.featureValue })),
productName: ci.sku?.product?.name ?? 'Unknown',
images: getStringArray(ci.sku?.images),
price: String(ci.sku?.price ?? '0'),
}))
return mapSku(sku, sku.features, comboItems)
})
return {
...mapProduct(product),
store: product.store ? mapStore(product.store) : null,
skus: skusWithCombos,
skus: product.skus.map((sku) => mapSku(sku, sku.features)),
deals: deals.map(mapSpecialDeal),
tags: productTagsData.map((tag) => mapTagInfo(tag.tag)),
}
@ -263,7 +227,6 @@ export async function createProduct(input: CreateProductInput): Promise<AdminPro
longDescription: productData.longDescription ?? null,
storeId: productData.storeId ?? null,
incrementStep: productData.incrementStep ?? 1,
productType: productData.productType ?? 'item',
}).returning()
const skuRows = await db.insert(productSkus).values(
@ -288,15 +251,6 @@ export async function createProduct(input: CreateProductInput): Promise<AdminPro
featureValue: f.featureValue,
}))
)
if (sku.comboItems && sku.comboItems.length > 0) {
await db.insert(productCombos).values(
sku.comboItems.map((ci: any) => ({
comboSkuId: skuRow.id,
skuId: ci.skuId,
}))
)
}
}
const createdSkus = await db.query.productSkus.findMany({
@ -329,7 +283,6 @@ export async function updateProduct(id: number, input: any): Promise<AdminProduc
longDescription: productData.longDescription ?? null,
storeId: productData.storeId ?? null,
incrementStep: productData.incrementStep ?? 1,
productType: productData.productType,
})
.where(eq(productInfo.id, id))
@ -373,16 +326,6 @@ export async function updateProduct(id: number, input: any): Promise<AdminProduc
featureValue: f.featureValue,
}))
)
await db.delete(productCombos).where(eq(productCombos.comboSkuId, sku.id))
if (sku.comboItems && sku.comboItems.length > 0) {
await db.insert(productCombos).values(
sku.comboItems.map((ci: any) => ({
comboSkuId: sku.id,
skuId: ci.skuId,
}))
)
}
} else {
// Insert new SKU
const [newSku] = await db.insert(productSkus).values({
@ -427,6 +370,29 @@ export async function updateProduct(id: number, input: any): Promise<AdminProduc
}
}
const product = await db.query.productSkus.findFirst({
where: eq(productSkus.id, id),
})
if (!product) {
return null
}
const [updatedProduct] = await db
.update(productSkus)
.set({
isOutOfStock: !product.isOutOfStock,
})
.where(eq(productSkus.id, id))
.returning()
if (!updatedProduct) {
return null
}
return mapSku(updatedProduct)
}
export async function updateSlotProducts(slotId: string, productIds: string[]): Promise<AdminUpdateSlotProductsResult> {
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, parseInt(slotId)),
@ -453,6 +419,14 @@ export async function updateSlotProducts(slotId: string, productIds: string[]):
}
}
export async function getSlotProductIds(slotId: string): Promise<number[]> {
const slot = await db.query.deliverySlotInfo.findFirst({
where: eq(deliverySlotInfo.id, parseInt(slotId)),
})
return slot?.skuIds || []
}
export async function getAllUnits(): Promise<AdminUnit[]> {
const allUnits = await db.query.units.findMany({
orderBy: units.shortNotation,
@ -841,24 +815,19 @@ export async function updateProductPrices(updates: Array<{
}
const productIds = updates.map((update) => update.productId)
// Validate all SKU IDs exist (in chunks to avoid large IN clauses)
const existingSkuChunks = await runBatched(db, productIds, 10, async (tx, chunk) => {
return tx.query.productSkus.findMany({
where: inArray(productSkus.id, chunk),
const existingSkus = await db.query.productSkus.findMany({
where: inArray(productSkus.id, productIds),
columns: { id: true },
})
})
const existingIds = new Set(existingSkuChunks.flat().map((sku: { id: number }) => sku.id))
}) as Array<{ id: number }>
const existingIds = new Set(existingSkus.map((sku: { id: number }) => sku.id))
const invalidIds = productIds.filter((id) => !existingIds.has(id))
if (invalidIds.length > 0) {
return { updatedCount: 0, invalidIds }
}
// Apply updates in chunks inside a single transaction
await runBatched(db, updates, 10, async (tx, chunk) => {
for (const update of chunk) {
const updatePromises = updates.map((update) => {
const { productId, price, marketPrice, flashPrice, isFlashAvailable } = update
const updateData: any = {}
@ -867,13 +836,14 @@ export async function updateProductPrices(updates: Array<{
if (flashPrice !== undefined) updateData.flashPrice = flashPrice === null ? null : flashPrice.toString()
if (isFlashAvailable !== undefined) updateData.isFlashAvailable = isFlashAvailable
await tx
return db
.update(productSkus)
.set(updateData)
.where(eq(productSkus.id, productId))
}
})
await Promise.all(updatePromises)
return { updatedCount: updates.length, invalidIds: [] }
}

View file

@ -1,7 +1,6 @@
import { db } from '../db/db_index'
import { storeInfo, productInfo } from '../db/schema'
import { eq, inArray } from 'drizzle-orm'
import { runBatched } from '../lib/run-batched'
export interface Store {
id: number
@ -45,8 +44,7 @@ export async function createStore(
input: CreateStoreInput,
products?: number[]
): Promise<Store> {
return db.transaction(async (tx) => {
const [newStore] = await tx
const [newStore] = await db
.insert(storeInfo)
.values({
name: input.name,
@ -57,12 +55,10 @@ export async function createStore(
.returning()
if (products && products.length > 0) {
await runBatched(tx, products, 10, async (t, chunk) => {
await t
await db
.update(productInfo)
.set({ storeId: newStore.id })
.where(inArray(productInfo.id, chunk))
})
.where(inArray(productInfo.id, products))
}
return {
@ -74,7 +70,6 @@ export async function createStore(
createdAt: newStore.createdAt,
// updatedAt: newStore.updatedAt,
}
})
}
export interface UpdateStoreInput {
@ -89,8 +84,7 @@ export async function updateStore(
input: UpdateStoreInput,
products?: number[]
): Promise<Store> {
return db.transaction(async (tx) => {
const [updatedStore] = await tx
const [updatedStore] = await db
.update(storeInfo)
.set({
...input,
@ -104,18 +98,16 @@ export async function updateStore(
}
if (products !== undefined) {
await tx
await db
.update(productInfo)
.set({ storeId: null })
.where(eq(productInfo.storeId, id))
if (products.length > 0) {
await runBatched(tx, products, 10, async (t, chunk) => {
await t
await db
.update(productInfo)
.set({ storeId: id })
.where(inArray(productInfo.id, chunk))
})
.where(inArray(productInfo.id, products))
}
}
@ -128,7 +120,6 @@ export async function updateStore(
createdAt: updatedStore.createdAt,
// updatedAt: updatedStore.updatedAt,
}
})
}
export async function deleteStore(id: number): Promise<{ message: string }> {

View file

@ -65,13 +65,11 @@ const staffRoleValues = ['super_admin', 'admin', 'marketer', 'delivery_staff'] a
const staffPermissionValues = ['crud_product', 'make_coupon', 'crud_staff_users'] as const
const uploadStatusValues = ['pending', 'claimed'] as const
const paymentStatusValues = ['pending', 'success', 'cod', 'failed'] as const
const productTypeValues = ['item', 'combo'] as const
export const staffRoleEnum = (name: string) => text(name, { enum: staffRoleValues })
export const staffPermissionEnum = (name: string) => text(name, { enum: staffPermissionValues })
export const uploadStatusEnum = (name: string) => text(name, { enum: uploadStatusValues })
export const paymentStatusEnum = (name: string) => text(name, { enum: paymentStatusValues })
export const productTypeEnum = (name: string) => text(name, { enum: productTypeValues })
export const users = sqliteTable('users', {
id: integer().primaryKey({ autoIncrement: true }),
@ -194,7 +192,6 @@ export const productInfo = sqliteTable('product_info', {
storeId: integer('store_id').references(() => storeInfo.id),
incrementStep: real('increment_step').notNull().default(1),
createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
productType: productTypeEnum('product_type').notNull().default('item'),
})
export const productSkus = sqliteTable('product_skus', {
@ -220,14 +217,6 @@ export const skuFeatures = sqliteTable('sku_features', {
unq_sku_feature_name: uniqueIndex('unique_sku_feature_name').on(t.skuId, t.featureName),
}))
export const productCombos = sqliteTable('product_combos', {
id: integer().primaryKey({ autoIncrement: true }),
comboSkuId: integer('combo_sku_id').notNull().references(() => productSkus.id),
skuId: integer('sku_id').notNull().references(() => productSkus.id),
}, (t) => ({
unq_combo_sku: uniqueIndex('unique_combo_sku_item').on(t.comboSkuId, t.skuId),
}))
export const productGroupInfo = sqliteTable('product_group_info', {
id: integer().primaryKey({ autoIncrement: true }),
groupName: text('group_name').notNull(),
@ -592,25 +581,12 @@ export const productSkusRelations = relations(productSkus, ({ one, many }) => ({
orderItems: many(orderItems),
cartItems: many(cartItems),
applicableCoupons: many(couponApplicableProducts),
comboItems: many(productCombos, { relationName: 'comboSku' }),
}))
export const skuFeaturesRelations = relations(skuFeatures, ({ one }) => ({
sku: one(productSkus, { fields: [skuFeatures.skuId], references: [productSkus.id] }),
}))
export const productCombosRelations = relations(productCombos, ({ one }) => ({
comboSku: one(productSkus, {
fields: [productCombos.comboSkuId],
references: [productSkus.id],
relationName: 'comboSku',
}),
sku: one(productSkus, {
fields: [productCombos.skuId],
references: [productSkus.id],
}),
}))
export const productTagInfoRelations = relations(productTagInfo, ({ many }) => ({
products: many(productTags),
}))

View file

@ -1,8 +1,23 @@
import { db } from '../db/db_index'
import { keyValStore } from '../db/schema'
import { eq } from 'drizzle-orm'
import { productInfo, keyValStore } from '../db/schema'
import { inArray, eq } from 'drizzle-orm'
import { castConstValue } from '../lib/const-keys'
/**
* Toggle flash delivery availability for specific products
* @param isAvailable - Whether flash delivery should be available
* @param productIds - Array of product IDs to update
*/
export async function toggleFlashDeliveryForItems(
isAvailable: boolean,
productIds: number[]
): Promise<void> {
await db
.update(productInfo)
.set({ isFlashAvailable: isAvailable })
.where(inArray(productInfo.id, productIds))
}
/**
* Update key-value store
* @param key - The key to update

View file

@ -1,7 +1,6 @@
import { db } from '../db/db_index'
import { orders, orderItems, orderStatus, payments, refunds, couponUsage, complaints } from '../db/schema'
import { inArray } from 'drizzle-orm'
import { runBatched } from './run-batched'
/**
* Delete orders and all their related records
@ -14,28 +13,26 @@ export async function deleteOrdersWithRelations(orderIds: number[]): Promise<voi
return
}
await runBatched(db, orderIds, 10, async (tx, chunk) => {
// Delete child records first (in correct order to avoid FK constraint errors)
// 1. Delete coupon usage records
await tx.delete(couponUsage).where(inArray(couponUsage.orderId, chunk))
await db.delete(couponUsage).where(inArray(couponUsage.orderId, orderIds))
// 2. Delete complaints related to these orders
await tx.delete(complaints).where(inArray(complaints.orderId, chunk))
await db.delete(complaints).where(inArray(complaints.orderId, orderIds))
// 3. Delete refunds
await tx.delete(refunds).where(inArray(refunds.orderId, chunk))
await db.delete(refunds).where(inArray(refunds.orderId, orderIds))
// 4. Delete payments
await tx.delete(payments).where(inArray(payments.orderId, chunk))
await db.delete(payments).where(inArray(payments.orderId, orderIds))
// 5. Delete order status records
await tx.delete(orderStatus).where(inArray(orderStatus.orderId, chunk))
await db.delete(orderStatus).where(inArray(orderStatus.orderId, orderIds))
// 6. Delete order items
await tx.delete(orderItems).where(inArray(orderItems.orderId, chunk))
await db.delete(orderItems).where(inArray(orderItems.orderId, orderIds))
// 7. Finally delete the orders themselves
await tx.delete(orders).where(inArray(orders.id, chunk))
})
await db.delete(orders).where(inArray(orders.id, orderIds))
}

View file

@ -1,39 +0,0 @@
/** Extracts the transaction handle type from any Drizzle db. */
type TxOf<DB> = DB extends {
transaction: (cb: (tx: infer Tx) => any, ...args: any[]) => any;
}
? Tx
: never;
/**
* Runs `runChunk` sequentially over `items` inside a single transaction,
* passing at most `n` items per call. All chunks commit together, or the
* whole thing rolls back.
*
* @param db Drizzle database instance.
* @param items Full array of values (e.g. product ids).
* @param n Max items per batch (keep under SQLite's 999 var limit).
* @param runChunk Async fn that runs your query for one chunk, using `tx`.
*/
export async function runBatched<
DB extends { transaction: (cb: (tx: any) => any, ...args: any[]) => any },
T,
R,
>(
db: DB,
items: readonly T[],
n: number,
runChunk: (tx: TxOf<DB>, chunk: T[]) => Promise<R>,
): Promise<R[]> {
if (n < 1) throw new RangeError("`n` must be >= 1");
const result = await db.transaction(async (tx: TxOf<DB>) => {
const out: R[] = [];
for (let i = 0; i < items.length; i += n) {
out.push(await runChunk(tx, items.slice(i, i + n)));
}
return out;
});
return result as R[];
}

View file

@ -57,7 +57,6 @@ export interface ProductBasicData {
productQuantity: number
isFlashAvailable: boolean
flashPrice: string | null
productType: string
}
export interface StoreBasicData {
@ -131,7 +130,6 @@ export async function getAllProductsForCache(): Promise<ProductBasicData[]> {
productQuantity: 1,
isFlashAvailable: sku.isFlashAvailable,
flashPrice: sku.flashPrice ? String(sku.flashPrice) : null,
productType: sku.product?.productType ?? 'item',
}
})
}
@ -178,41 +176,6 @@ export async function getAllProductTagsForCache(): Promise<ProductTagData[]> {
.innerJoin(productTagInfo, eq(productTags.tagId, productTagInfo.id))
}
// ============================================================================
// PRODUCT COMBO STORE HELPERS
// ============================================================================
export interface ProductComboCacheData {
comboSkuId: number
skuId: number
productName: string
skuName: string | null
images: unknown
unitNotation: string
price: string
}
export async function getAllProductCombosForCache(): Promise<ProductComboCacheData[]> {
const results = await db.query.productCombos.findMany({
with: {
sku: { with: { product: true, features: true } },
},
})
return results.map((ci) => {
const features = ci.sku?.features || []
return {
comboSkuId: ci.comboSkuId,
skuId: ci.skuId,
productName: ci.sku?.product?.name ?? 'Unknown',
skuName: ci.sku?.name ?? null,
images: ci.sku?.images,
unitNotation: features.map((f) => f.featureValue).join(' '),
price: String(ci.sku?.price ?? '0'),
}
})
}
// ============================================================================
// PRODUCT TAG STORE HELPERS
// ============================================================================

View file

@ -11,7 +11,7 @@ const mapBanner = (banner: BannerRow): UserBanner => ({
name: banner.name,
imageUrl: banner.imageUrl,
description: banner.description ?? null,
skuIds: banner.skuIds ?? null,
productIds: banner.productIds ?? null,
redirectUrl: banner.redirectUrl ?? null,
serialNum: banner.serialNum ?? null,
isActive: banner.isActive,

View file

@ -22,7 +22,6 @@ import type {
UserRecentProduct,
} from '@packages/shared'
import { coerceDate } from '../lib/date'
import { runBatched } from '../lib/run-batched'
export interface OrderItemInput {
productId: number
@ -82,19 +81,16 @@ export interface OrderWithRelations {
createdAt: Date
orderItems: Array<{
id: number
skuId: number
productId: number
quantity: string
price: string
discountedPrice: string | null
is_packaged: boolean
sku: {
id: number
name: string | null
images: unknown
product: {
id: number
name: string
} | null
} | null
images: unknown
}
}>
slot: {
deliveryTime: Date
@ -130,19 +126,16 @@ export interface OrderDetailWithRelations {
createdAt: Date
orderItems: Array<{
id: number
skuId: number
productId: number
quantity: string
price: string
discountedPrice: string | null
is_packaged: boolean
sku: {
id: number
name: string | null
images: unknown
product: {
id: number
name: string
} | null
} | null
images: unknown
}
}>
slot: {
deliveryTime: Date
@ -393,7 +386,6 @@ export async function getOrdersWithRelations(
},
columns: {
id: true,
name: true,
images: true,
},
},
@ -476,7 +468,6 @@ export async function getOrderByIdWithRelations(
},
columns: {
id: true,
name: true,
images: true,
},
},
@ -635,16 +626,12 @@ export async function getRecentlyDeliveredOrderIds(
export async function getSkuIdsFromOrders(
orderIds: number[]
): Promise<number[]> {
if (orderIds.length === 0) return []
const skuChunks = await runBatched(db, orderIds, 10, async (tx, chunk) => {
return tx
const orderItemsResult = await db
.select({ skuId: orderItems.skuId })
.from(orderItems)
.where(inArray(orderItems.orderId, chunk))
})
.where(inArray(orderItems.orderId, orderIds))
return [...new Set(skuChunks.flat().map((item) => item.skuId))]
return [...new Set(orderItemsResult.map((item) => item.skuId))]
}
export interface RecentProductData {
@ -662,25 +649,18 @@ export async function getProductsForRecentOrders(
productIds: number[],
limit: number
): Promise<RecentProductData[]> {
if (productIds.length === 0) return []
const skuChunks = await runBatched(db, productIds, 10, async (tx, chunk) => {
return tx.query.productSkus.findMany({
const skus = await db.query.productSkus.findMany({
where: and(
inArray(productSkus.id, chunk),
inArray(productSkus.id, productIds),
eq(productSkus.isSuspended, false)
),
with: {
product: true,
features: true,
},
orderBy: desc(productSkus.createdAt),
limit,
})
})
const skus = skuChunks
.flat()
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
.slice(0, limit)
return skus.map((sku) => {
const features = sku.features || []
@ -731,11 +711,10 @@ export interface OrderWithFullData {
export async function getOrdersByIdsWithFullData(
orderIds: number[]
): Promise<OrderWithFullData[]> {
if (orderIds.length === 0) return []
console.log('getting orders byid')
const orderChunks = await runBatched(db, orderIds, 10, async (tx, chunk) => {
return tx.query.orders.findMany({
where: inArray(orders.id, chunk),
const ordersResp = await db.query.orders.findMany({
where: inArray(orders.id, orderIds),
with: {
address: {
columns: {
@ -771,9 +750,9 @@ export async function getOrdersByIdsWithFullData(
},
},
})
})
// as Promise<OrderWithFullData[]>
return orderChunks.flat() as OrderWithFullData[]
return ordersResp as OrderWithFullData[];
}
export interface OrderWithCancellationData extends OrderWithFullData {

View file

@ -1,5 +1,5 @@
import { db } from '../db/db_index'
import { deliverySlotInfo, productInfo, productCombos, productSkus, productReviews, productTags, specialDeals, storeInfo, 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'
@ -44,25 +44,6 @@ export async function getProductDetailById(skuId: number): Promise<UserProductDe
)
.orderBy(specialDeals.quantity)
const comboItemsData = await db.query.productCombos.findMany({
where: eq(productCombos.comboSkuId, skuId),
with: {
sku: { with: { product: true, features: true } },
},
})
const comboItems = comboItemsData.map((ci) => {
const ciFeatures = ci.sku?.features || []
return {
skuId: ci.skuId,
skuName: ci.sku?.name ?? null,
unitNotation: ciFeatures.map((f) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
productName: ci.sku?.product?.name ?? 'Unknown',
images: getStringArray(ci.sku?.images),
price: String(ci.sku?.price ?? '0'),
}
})
return {
id: sku.id,
name: product?.name ?? 'Unknown',
@ -88,8 +69,6 @@ export async function getProductDetailById(skuId: number): Promise<UserProductDe
price: String(deal.price ?? '0'),
validTill: deal.validTill,
})),
productType: product?.productType ?? 'item',
comboItems,
}
}
@ -172,8 +151,6 @@ export async function createProductReview(
export interface ProductSummaryData {
id: number
name: string
skuId: number
skuName: string | null
shortDescription: string | null
price: string
marketPrice: string | null
@ -181,37 +158,37 @@ export interface ProductSummaryData {
isOutOfStock: boolean
unitShortNotation: string
productQuantity: number
features: { featureName: string; featureValue: string }[]
}
export async function getAllProductsWithUnits(tagId?: number): Promise<ProductSummaryData[]> {
const taggedProductIdSet = new Set<number>()
let productIds: number[] | null = null
// If tagId is provided, get product IDs that have this tag
// If tagId is provided, get products that have this tag
if (tagId) {
const taggedProducts = await db
.select({ productId: productTags.productId })
.from(productTags)
.where(eq(productTags.tagId, tagId))
for (const tp of taggedProducts) {
taggedProductIdSet.add(tp.productId)
productIds = taggedProducts.map(tp => tp.productId)
}
let whereCondition = undefined
// Filter by product IDs if tag filtering is applied
if (productIds && productIds.length > 0) {
whereCondition = inArray(productSkus.productId, productIds)
}
const skus = await db.query.productSkus.findMany({
where: whereCondition,
with: {
product: true,
features: true,
},
})
return skus
.filter((sku) => {
if (!tagId) return true
return taggedProductIdSet.has(sku.productId)
})
.map((sku) => {
return skus.map((sku) => {
const features = sku.features || []
return {
id: sku.product?.id ?? 0,
@ -223,8 +200,6 @@ export async function getAllProductsWithUnits(tagId?: number): Promise<ProductSu
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
images: sku.images,
isOutOfStock: sku.isOutOfStock,
unitShortNotation: features.map((f) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
productQuantity: 1,
features: features.map((f) => ({ featureName: f.featureName, featureValue: f.featureValue })),
}
})

View file

@ -1,6 +1,6 @@
import { db } from '../db/db_index'
import { productInfo, productSkus, storeInfo } from '../db/schema'
import { and, asc, eq, inArray } from 'drizzle-orm'
import { and, eq, inArray, sql } from 'drizzle-orm'
import type { InferSelectModel } from 'drizzle-orm'
import type { UserStoreDetailData, UserStoreProductData, UserStoreSummaryData, StoreSummary } from '@packages/shared'
@ -12,44 +12,72 @@ const getStringArray = (value: unknown): string[] | null => {
}
export async function getStoreSummaries(): Promise<UserStoreSummaryData[]> {
const storesData = await db.select({
// Count SKUs per store, filtering by suspended SKUs
const storesData = await db
.select({
id: storeInfo.id,
name: storeInfo.name,
description: storeInfo.description,
imageUrl: storeInfo.imageUrl,
}).from(storeInfo)
const skus = await db.query.productSkus.findMany({
where: eq(productSkus.isSuspended, false),
with: { product: true },
orderBy: asc(productSkus.id),
productCount: sql<number>`count(${productSkus.id})`.as('productCount'),
})
.from(storeInfo)
.leftJoin(
productInfo,
eq(productInfo.storeId, storeInfo.id)
)
.leftJoin(
productSkus,
and(
eq(productSkus.productId, productInfo.id),
eq(productSkus.isSuspended, false)
)
)
.groupBy(storeInfo.id)
const skusByStore = new Map<number, typeof skus>()
for (const sku of skus) {
const storeId = sku.product?.storeId
if (storeId == null) continue
if (!skusByStore.has(storeId)) skusByStore.set(storeId, [])
skusByStore.get(storeId)!.push(sku)
}
const storesWithDetails = await Promise.all(
storesData.map(async (store) => {
let sampleProducts: any[] = []
// Get sample SKUs from this store
if (store.productCount > 0) {
const storeProductIds = await db
.select({ id: productInfo.id })
.from(productInfo)
.where(eq(productInfo.storeId, store.id))
return storesData.map((store) => {
const storeSkus = skusByStore.get(store.id) || []
const sampleProducts = storeSkus.slice(0, 3).map((sku) => ({
const productIdArr = storeProductIds.map((p) => p.id)
if (productIdArr.length > 0) {
const skus = await db.query.productSkus.findMany({
where: and(
inArray(productSkus.productId, productIdArr),
eq(productSkus.isSuspended, false)
),
with: {
product: { columns: { name: true } },
},
columns: { id: true, images: true, name: true },
limit: 3,
})
sampleProducts = skus.map((sku) => ({
id: sku.id,
name: sku.product?.name ?? sku.name ?? 'Unknown',
images: getStringArray(sku.images),
}))
}
}
return {
id: store.id,
name: store.name,
description: store.description ?? null,
imageUrl: store.imageUrl ?? null,
productCount: storeSkus.length,
productCount: store.productCount || 0,
sampleProducts,
}
})
)
return storesWithDetails
}
export async function getStoreDetail(storeId: number): Promise<UserStoreDetailData | null> {

View file

@ -360,20 +360,13 @@ export interface AdminUnit {
fullName: string;
}
export interface AdminSkuFeature {
id: number;
skuId: number;
featureName: string;
featureValue: string;
}
export interface AdminProductComboItem {
skuId: number;
skuName: string | null;
features: AdminSkuFeature[];
productName: string;
images: string[] | null;
price: string;
export interface AdminSkuVariant {
id: number
skuId: number
name: string
value: string
unitId: number | null
sortOrder: number
}
export interface AdminSku {
@ -390,7 +383,7 @@ export interface AdminSku {
flashPrice: string | null
createdAt: Date
features: AdminSkuFeature[]
comboItems: AdminProductComboItem[]
}
}
export interface AdminProduct {
@ -401,7 +394,6 @@ export interface AdminProduct {
storeId: number | null;
incrementStep: number;
createdAt: Date;
productType: 'item' | 'combo';
}
export interface AdminProductWithRelations extends AdminProduct {
@ -499,6 +491,10 @@ export interface AdminUpdateSlotProductsResult {
removed: number;
}
export interface AdminSlotProductIdsResult {
skuIds: number[];
}
export type AdminSlotsProductIdsResult = Record<number, number[]>;
export interface AdminProductReview {

View file

@ -145,7 +145,7 @@ export interface UserCartProduct {
export interface UserCartItem {
id: number;
skuId: number;
productId: number;
quantity: number;
addedAt: Date;
product: UserCartProduct;
@ -286,15 +286,6 @@ export interface UserProductSpecialDeal {
validTill: Date;
}
export interface UserProductComboItem {
skuId: number;
skuName: string | null;
unitNotation: string;
productName: string;
images: string[] | null;
price: string;
}
export interface UserProductDetailData {
id: number;
name: string;
@ -312,8 +303,6 @@ export interface UserProductDetailData {
flashPrice: string | null;
deliverySlots: UserProductDeliverySlot[];
specialDeals: UserProductSpecialDeal[];
productType: string;
comboItems: UserProductComboItem[];
}
export interface UserProductDetail extends UserProductDetailData {
@ -496,7 +485,7 @@ export interface UserCouponApplicableUser {
export interface UserCouponApplicableProduct {
id: number;
couponId: number;
skuId: number;
productId: number;
}
export interface UserCoupon {
@ -506,7 +495,7 @@ export interface UserCoupon {
discountPercent: string | null;
flatDiscount: string | null;
minOrder: string | null;
skuIds: unknown;
productIds: unknown;
maxValue: string | null;
isApplyForAll: boolean;
validTill: Date | null;

View file

@ -210,6 +210,25 @@ export interface token_user {
gender: string;
}
export interface ProductSummary {
id: number;
name: string;
shortDescription?: string;
price: number;
unit: string;
isOutOfStock: boolean;
nextDeliveryDate: string | null;
images: string[];
}
export interface GetSlotsProductIdsPayload {
slotIds: number[];
}
export interface GetSlotsProductIdsResponse {
[slotId: number]: number[];
}
export interface Order {
id: string;
orderId: string;

View file

@ -0,0 +1,40 @@
import { useQuery } from "@tanstack/react-query";
import axios from "../services/axios";
import type { ProductSummary, GetSlotsProductIdsPayload, GetSlotsProductIdsResponse } from '../../shared-types';
export interface GetProductsSummaryResponse {
products: ProductSummary[];
count: number;
}
const getAllProductsSummaryApi = async (): Promise<GetProductsSummaryResponse> => {
const response = await axios.get('/cm/products/summary');
return response.data;
};
export const useGetAllProductsSummary = () => {
return useQuery({
queryKey: ['products-summary'],
// queryFn: getAllProductsSummaryApi,
queryFn: async () => {
const response = await axios.get('/cm/products/summary');
return response.data;
}
});
};
const getSlotsProductIdsApi = async (payload: GetSlotsProductIdsPayload): Promise<GetSlotsProductIdsResponse> => {
const response = await axios.post('/av/products/slots/product-ids', payload);
return response.data;
};
export const useGetSlotsProductIds = (slotIds: number[]) => {
return useQuery({
queryKey: ['slots-product-ids', slotIds],
queryFn: () => getSlotsProductIdsApi({ slotIds }),
enabled: slotIds.length > 0,
});
};