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

View file

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

View file

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

View file

@ -18,7 +18,6 @@ interface Variant {
isFlashAvailable: boolean isFlashAvailable: boolean
flashPrice: string flashPrice: string
attributes: Attribute[] attributes: Attribute[]
comboItems: { skuId: number | string }[]
} }
interface ProductFormData { interface ProductFormData {
@ -26,7 +25,6 @@ interface ProductFormData {
shortDescription: string shortDescription: string
longDescription: string longDescription: string
storeId: number storeId: number
productType: 'item' | 'combo'
variants: Variant[] variants: Variant[]
} }
@ -46,14 +44,12 @@ interface ProductFormProps {
const defaultAttribute = (): Attribute => ({ featureName: 'quantity', featureValue: '' }) const defaultAttribute = (): Attribute => ({ featureName: 'quantity', featureValue: '' })
const defaultVariant = (): Variant => ({ const defaultVariant = (): Variant => ({
id: undefined,
name: '', name: '',
price: '', price: '',
marketPrice: '', marketPrice: '',
isFlashAvailable: false, isFlashAvailable: false,
flashPrice: '', flashPrice: '',
attributes: [defaultAttribute()], attributes: [defaultAttribute()],
comboItems: [],
}) })
const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
@ -80,12 +76,6 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
value: store.id, 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 ( return (
<Formik <Formik
initialValues={initialValues} initialValues={initialValues}
@ -146,19 +136,6 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
style={{ marginBottom: 16 }} 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"> <FieldArray name="variants">
{({ push, remove }) => ( {({ push, remove }) => (
<View> <View>
@ -294,46 +271,6 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
} }
allowMultiple={true} 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> </View>

View file

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

View file

@ -51,7 +51,9 @@ export type {
AdminProductListResponse, AdminProductListResponse,
AdminProductResponse, AdminProductResponse,
AdminDeleteProductResult, AdminDeleteProductResult,
AdminToggleOutOfStockResult,
AdminUpdateSlotProductsResult, AdminUpdateSlotProductsResult,
AdminSlotProductIdsResult,
AdminSlotsProductIdsResult, AdminSlotsProductIdsResult,
AdminProductReview, AdminProductReview,
AdminProductReviewWithSignedUrls, 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, // replaceProductTags,
// toggleProductOutOfStock, // toggleProductOutOfStock,
// updateSlotProducts, // updateSlotProducts,
// getSlotProductIds,
// getSlotsProductIds, // getSlotsProductIds,
// getAllUnits, // getAllUnits,
// getAllProductTags, // getAllProductTags,

View file

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

View file

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

View file

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

View file

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

View file

@ -85,6 +85,7 @@ export {
replaceProductTags, replaceProductTags,
toggleProductOutOfStock, toggleProductOutOfStock,
updateSlotProducts, updateSlotProducts,
getSlotProductIds,
getSlotsProductIds, getSlotsProductIds,
getAllUnits, getAllUnits,
getAllProductTags, 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[]> { export async function getAllUnits(): Promise<AdminUnit[]> {
const allUnits = await db.query.units.findMany({ const allUnits = await db.query.units.findMany({
orderBy: units.shortNotation, orderBy: units.shortNotation,

View file

@ -232,18 +232,5 @@ WHERE `key` = 'popularItems'
-- 9. Clean up helper table. -- 9. Clean up helper table.
DROP TABLE `__product_to_sku`; 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 foreign_keys=ON;
PRAGMA defer_foreign_keys = off; PRAGMA defer_foreign_keys = off;

View file

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

View file

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

View file

@ -1,7 +1,6 @@
import { db } from '../db/db_index' import { db } from '../db/db_index'
import { storeInfo, productInfo } from '../db/schema' import { storeInfo, productInfo } from '../db/schema'
import { eq, inArray } from 'drizzle-orm' import { eq, inArray } from 'drizzle-orm'
import { runBatched } from '../lib/run-batched'
export interface Store { export interface Store {
id: number id: number
@ -45,8 +44,7 @@ export async function createStore(
input: CreateStoreInput, input: CreateStoreInput,
products?: number[] products?: number[]
): Promise<Store> { ): Promise<Store> {
return db.transaction(async (tx) => { const [newStore] = await db
const [newStore] = await tx
.insert(storeInfo) .insert(storeInfo)
.values({ .values({
name: input.name, name: input.name,
@ -57,12 +55,10 @@ export async function createStore(
.returning() .returning()
if (products && products.length > 0) { if (products && products.length > 0) {
await runBatched(tx, products, 10, async (t, chunk) => { await db
await t
.update(productInfo) .update(productInfo)
.set({ storeId: newStore.id }) .set({ storeId: newStore.id })
.where(inArray(productInfo.id, chunk)) .where(inArray(productInfo.id, products))
})
} }
return { return {
@ -74,7 +70,6 @@ export async function createStore(
createdAt: newStore.createdAt, createdAt: newStore.createdAt,
// updatedAt: newStore.updatedAt, // updatedAt: newStore.updatedAt,
} }
})
} }
export interface UpdateStoreInput { export interface UpdateStoreInput {
@ -89,8 +84,7 @@ export async function updateStore(
input: UpdateStoreInput, input: UpdateStoreInput,
products?: number[] products?: number[]
): Promise<Store> { ): Promise<Store> {
return db.transaction(async (tx) => { const [updatedStore] = await db
const [updatedStore] = await tx
.update(storeInfo) .update(storeInfo)
.set({ .set({
...input, ...input,
@ -104,18 +98,16 @@ export async function updateStore(
} }
if (products !== undefined) { if (products !== undefined) {
await tx await db
.update(productInfo) .update(productInfo)
.set({ storeId: null }) .set({ storeId: null })
.where(eq(productInfo.storeId, id)) .where(eq(productInfo.storeId, id))
if (products.length > 0) { if (products.length > 0) {
await runBatched(tx, products, 10, async (t, chunk) => { await db
await t
.update(productInfo) .update(productInfo)
.set({ storeId: id }) .set({ storeId: id })
.where(inArray(productInfo.id, chunk)) .where(inArray(productInfo.id, products))
})
} }
} }
@ -128,7 +120,6 @@ export async function updateStore(
createdAt: updatedStore.createdAt, createdAt: updatedStore.createdAt,
// updatedAt: updatedStore.updatedAt, // updatedAt: updatedStore.updatedAt,
} }
})
} }
export async function deleteStore(id: number): Promise<{ message: string }> { 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 staffPermissionValues = ['crud_product', 'make_coupon', 'crud_staff_users'] as const
const uploadStatusValues = ['pending', 'claimed'] as const const uploadStatusValues = ['pending', 'claimed'] as const
const paymentStatusValues = ['pending', 'success', 'cod', 'failed'] 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 staffRoleEnum = (name: string) => text(name, { enum: staffRoleValues })
export const staffPermissionEnum = (name: string) => text(name, { enum: staffPermissionValues }) export const staffPermissionEnum = (name: string) => text(name, { enum: staffPermissionValues })
export const uploadStatusEnum = (name: string) => text(name, { enum: uploadStatusValues }) export const uploadStatusEnum = (name: string) => text(name, { enum: uploadStatusValues })
export const paymentStatusEnum = (name: string) => text(name, { enum: paymentStatusValues }) export const paymentStatusEnum = (name: string) => text(name, { enum: paymentStatusValues })
export const productTypeEnum = (name: string) => text(name, { enum: productTypeValues })
export const users = sqliteTable('users', { export const users = sqliteTable('users', {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
@ -194,7 +192,6 @@ export const productInfo = sqliteTable('product_info', {
storeId: integer('store_id').references(() => storeInfo.id), storeId: integer('store_id').references(() => storeInfo.id),
incrementStep: real('increment_step').notNull().default(1), incrementStep: real('increment_step').notNull().default(1),
createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`), createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
productType: productTypeEnum('product_type').notNull().default('item'),
}) })
export const productSkus = sqliteTable('product_skus', { 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), 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', { export const productGroupInfo = sqliteTable('product_group_info', {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
groupName: text('group_name').notNull(), groupName: text('group_name').notNull(),
@ -592,25 +581,12 @@ export const productSkusRelations = relations(productSkus, ({ one, many }) => ({
orderItems: many(orderItems), orderItems: many(orderItems),
cartItems: many(cartItems), cartItems: many(cartItems),
applicableCoupons: many(couponApplicableProducts), applicableCoupons: many(couponApplicableProducts),
comboItems: many(productCombos, { relationName: 'comboSku' }),
})) }))
export const skuFeaturesRelations = relations(skuFeatures, ({ one }) => ({ export const skuFeaturesRelations = relations(skuFeatures, ({ one }) => ({
sku: one(productSkus, { fields: [skuFeatures.skuId], references: [productSkus.id] }), 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 }) => ({ export const productTagInfoRelations = relations(productTagInfo, ({ many }) => ({
products: many(productTags), products: many(productTags),
})) }))

View file

@ -1,8 +1,23 @@
import { db } from '../db/db_index' import { db } from '../db/db_index'
import { keyValStore } from '../db/schema' import { productInfo, keyValStore } from '../db/schema'
import { eq } from 'drizzle-orm' import { inArray, eq } from 'drizzle-orm'
import { castConstValue } from '../lib/const-keys' 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 * Update key-value store
* @param key - The key to update * @param key - The key to update

View file

@ -1,7 +1,6 @@
import { db } from '../db/db_index' import { db } from '../db/db_index'
import { orders, orderItems, orderStatus, payments, refunds, couponUsage, complaints } from '../db/schema' import { orders, orderItems, orderStatus, payments, refunds, couponUsage, complaints } from '../db/schema'
import { inArray } from 'drizzle-orm' import { inArray } from 'drizzle-orm'
import { runBatched } from './run-batched'
/** /**
* Delete orders and all their related records * Delete orders and all their related records
@ -14,28 +13,26 @@ export async function deleteOrdersWithRelations(orderIds: number[]): Promise<voi
return return
} }
await runBatched(db, orderIds, 10, async (tx, chunk) => {
// Delete child records first (in correct order to avoid FK constraint errors) // Delete child records first (in correct order to avoid FK constraint errors)
// 1. Delete coupon usage records // 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 // 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 // 3. Delete refunds
await tx.delete(refunds).where(inArray(refunds.orderId, chunk)) await db.delete(refunds).where(inArray(refunds.orderId, orderIds))
// 4. Delete payments // 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 // 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 // 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 // 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 productQuantity: number
isFlashAvailable: boolean isFlashAvailable: boolean
flashPrice: string | null flashPrice: string | null
productType: string
} }
export interface StoreBasicData { export interface StoreBasicData {
@ -131,7 +130,6 @@ export async function getAllProductsForCache(): Promise<ProductBasicData[]> {
productQuantity: 1, productQuantity: 1,
isFlashAvailable: sku.isFlashAvailable, isFlashAvailable: sku.isFlashAvailable,
flashPrice: sku.flashPrice ? String(sku.flashPrice) : null, 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)) .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 // PRODUCT TAG STORE HELPERS
// ============================================================================ // ============================================================================

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import { db } from '../db/db_index' 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 { and, desc, eq, gt, sql } from 'drizzle-orm'
import type { UserProductDetailData, UserProductReview } from '@packages/shared' import type { UserProductDetailData, UserProductReview } from '@packages/shared'
@ -44,25 +44,6 @@ export async function getProductDetailById(skuId: number): Promise<UserProductDe
) )
.orderBy(specialDeals.quantity) .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 { return {
id: sku.id, id: sku.id,
name: product?.name ?? 'Unknown', name: product?.name ?? 'Unknown',
@ -88,8 +69,6 @@ export async function getProductDetailById(skuId: number): Promise<UserProductDe
price: String(deal.price ?? '0'), price: String(deal.price ?? '0'),
validTill: deal.validTill, validTill: deal.validTill,
})), })),
productType: product?.productType ?? 'item',
comboItems,
} }
} }
@ -172,8 +151,6 @@ export async function createProductReview(
export interface ProductSummaryData { export interface ProductSummaryData {
id: number id: number
name: string name: string
skuId: number
skuName: string | null
shortDescription: string | null shortDescription: string | null
price: string price: string
marketPrice: string | null marketPrice: string | null
@ -181,37 +158,37 @@ export interface ProductSummaryData {
isOutOfStock: boolean isOutOfStock: boolean
unitShortNotation: string unitShortNotation: string
productQuantity: number productQuantity: number
features: { featureName: string; featureValue: string }[]
} }
export async function getAllProductsWithUnits(tagId?: number): Promise<ProductSummaryData[]> { 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) { if (tagId) {
const taggedProducts = await db const taggedProducts = await db
.select({ productId: productTags.productId }) .select({ productId: productTags.productId })
.from(productTags) .from(productTags)
.where(eq(productTags.tagId, tagId)) .where(eq(productTags.tagId, tagId))
for (const tp of taggedProducts) { productIds = taggedProducts.map(tp => tp.productId)
taggedProductIdSet.add(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({ const skus = await db.query.productSkus.findMany({
where: whereCondition,
with: { with: {
product: true, product: true,
features: true, features: true,
}, },
}) })
return skus return skus.map((sku) => {
.filter((sku) => {
if (!tagId) return true
return taggedProductIdSet.has(sku.productId)
})
.map((sku) => {
const features = sku.features || [] const features = sku.features || []
return { return {
id: sku.product?.id ?? 0, id: sku.product?.id ?? 0,
@ -223,8 +200,6 @@ export async function getAllProductsWithUnits(tagId?: number): Promise<ProductSu
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null, marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
images: sku.images, images: sku.images,
isOutOfStock: sku.isOutOfStock, 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 })), features: features.map((f) => ({ featureName: f.featureName, featureValue: f.featureValue })),
} }
}) })

View file

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

View file

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

View file

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

View file

@ -210,6 +210,25 @@ export interface token_user {
gender: string; 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 { export interface Order {
id: string; id: string;
orderId: 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,
});
};