enh
This commit is contained in:
parent
6193104e6f
commit
0cbd24341f
23 changed files with 571 additions and 424 deletions
|
|
@ -253,10 +253,6 @@ export default function PricesOverview() {
|
|||
return;
|
||||
}
|
||||
|
||||
Alert.alert("Error", "Please enter a valid size");
|
||||
return;
|
||||
}
|
||||
|
||||
setPendingChanges(prev => ({
|
||||
...prev,
|
||||
[editDialog.sku.id]: {
|
||||
|
|
@ -368,6 +364,7 @@ export default function PricesOverview() {
|
|||
/>
|
||||
)}
|
||||
|
||||
<BottomDialog open={editDialog.open} onClose={() => setEditDialog({ ...editDialog, open: false, tempFlashPrice: "", tempMarketPrice: "", tempPrice: "" })}>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ 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,
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -56,6 +59,7 @@ export default function AddProduct() {
|
|||
shortDescription: values.shortDescription || undefined,
|
||||
longDescription: values.longDescription || undefined,
|
||||
storeId: values.storeId,
|
||||
productType: values.productType,
|
||||
incrementStep: 1,
|
||||
skus,
|
||||
})
|
||||
|
|
@ -72,14 +76,17 @@ 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 }[],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ 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 || '',
|
||||
|
|
@ -52,6 +53,9 @@ export default function EditProduct() {
|
|||
featureName: f.featureName,
|
||||
featureValue: f.featureValue,
|
||||
})),
|
||||
comboItems: (sku.comboItems || []).map((ci: any) => ({
|
||||
skuId: ci.skuId.toString(),
|
||||
})),
|
||||
})),
|
||||
}
|
||||
}, [productData])
|
||||
|
|
@ -118,6 +122,9 @@ 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,
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -127,6 +134,7 @@ export default function EditProduct() {
|
|||
shortDescription: values.shortDescription || undefined,
|
||||
longDescription: values.longDescription || undefined,
|
||||
storeId: values.storeId,
|
||||
productType: values.productType,
|
||||
incrementStep: 1,
|
||||
skus,
|
||||
deletedImageKeys,
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ interface Variant {
|
|||
isFlashAvailable: boolean
|
||||
flashPrice: string
|
||||
attributes: Attribute[]
|
||||
comboItems: { skuId: number | string }[]
|
||||
}
|
||||
|
||||
interface ProductFormData {
|
||||
|
|
@ -25,6 +26,7 @@ interface ProductFormData {
|
|||
shortDescription: string
|
||||
longDescription: string
|
||||
storeId: number
|
||||
productType: 'item' | 'combo'
|
||||
variants: Variant[]
|
||||
}
|
||||
|
||||
|
|
@ -44,12 +46,14 @@ 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>(({
|
||||
|
|
@ -76,6 +80,12 @@ 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}
|
||||
|
|
@ -136,6 +146,19 @@ 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>
|
||||
|
|
@ -271,6 +294,46 @@ 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>
|
||||
|
|
|
|||
|
|
@ -5,11 +5,9 @@ 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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,137 +0,0 @@
|
|||
// 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();
|
||||
|
|
@ -273,7 +273,6 @@ export {
|
|||
type SlotWithProductsData,
|
||||
type UserNegativityData,
|
||||
// Automated Jobs
|
||||
toggleFlashDeliveryForItems,
|
||||
toggleKeyVal,
|
||||
getAllKeyValStore,
|
||||
// Post-order handler helpers
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ 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'
|
||||
|
||||
|
|
@ -33,6 +35,15 @@ 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> {
|
||||
|
|
@ -213,6 +224,14 @@ 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[]) || []
|
||||
|
|
@ -223,6 +242,14 @@ 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,
|
||||
|
|
@ -253,6 +280,8 @@ export async function getAllProducts(): Promise<Product[]> {
|
|||
validTill: d.validTill,
|
||||
})),
|
||||
productTags: productTags,
|
||||
productType: product.productType || 'item',
|
||||
comboItems: comboItems,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -190,6 +190,7 @@ 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'),
|
||||
|
|
@ -201,10 +202,13 @@ 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, skus } = input
|
||||
const { name, shortDescription, longDescription, storeId, incrementStep, productType, skus } = input
|
||||
|
||||
const existingProduct = await checkProductExistsByName(name.trim())
|
||||
if (existingProduct) {
|
||||
|
|
@ -224,6 +228,7 @@ export const productRouter = router({
|
|||
featureName: f.featureName,
|
||||
featureValue: f.featureValue,
|
||||
})),
|
||||
comboItems: (sku.comboItems || []).map((ci) => ({ skuId: ci.skuId })),
|
||||
}))
|
||||
|
||||
const newProduct = await createProductInDb({
|
||||
|
|
@ -232,6 +237,7 @@ export const productRouter = router({
|
|||
longDescription,
|
||||
storeId,
|
||||
incrementStep,
|
||||
productType,
|
||||
skus: skuInputs,
|
||||
} as any)
|
||||
|
||||
|
|
@ -265,6 +271,7 @@ 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(),
|
||||
|
|
@ -277,12 +284,15 @@ 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, skus, deletedImageKeys, newImageUrls } = input
|
||||
const { id, name, shortDescription, longDescription, storeId, incrementStep, productType, skus, deletedImageKeys, newImageUrls } = input
|
||||
|
||||
if (deletedImageKeys.length > 0) {
|
||||
await deleteImageUtil({ keys: deletedImageKeys })
|
||||
|
|
@ -302,6 +312,7 @@ export const productRouter = router({
|
|||
featureName: f.featureName,
|
||||
featureValue: f.featureValue,
|
||||
})),
|
||||
comboItems: (sku.comboItems || []).map((ci) => ({ skuId: ci.skuId })),
|
||||
}))
|
||||
|
||||
const updatedProduct = await updateProductInDb(id, {
|
||||
|
|
@ -310,6 +321,7 @@ export const productRouter = router({
|
|||
longDescription,
|
||||
storeId,
|
||||
incrementStep,
|
||||
productType,
|
||||
skus: skuInputs,
|
||||
} as any)
|
||||
|
||||
|
|
|
|||
|
|
@ -232,5 +232,18 @@ 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;
|
||||
|
|
|
|||
|
|
@ -331,11 +331,13 @@ export {
|
|||
getAllDeliverySlotsForCache,
|
||||
getAllSpecialDealsForCache,
|
||||
getAllProductTagsForCache,
|
||||
getAllProductCombosForCache,
|
||||
type ProductBasicData,
|
||||
type StoreBasicData,
|
||||
type DeliverySlotData,
|
||||
type SpecialDealData,
|
||||
type ProductTagData,
|
||||
type ProductComboCacheData,
|
||||
// Product Tag Store
|
||||
getAllTagsForCache,
|
||||
getAllTagProductMappings,
|
||||
|
|
@ -352,7 +354,6 @@ export {
|
|||
|
||||
// Automated Jobs Helpers
|
||||
export {
|
||||
toggleFlashDeliveryForItems,
|
||||
toggleKeyVal,
|
||||
getAllKeyValStore,
|
||||
} from './src/lib/automated-jobs'
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
productInfo,
|
||||
productSkus,
|
||||
skuFeatures,
|
||||
productCombos,
|
||||
units,
|
||||
specialDeals,
|
||||
deliverySlotInfo,
|
||||
|
|
@ -24,6 +25,7 @@ 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,
|
||||
|
|
@ -82,6 +84,7 @@ 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 => ({
|
||||
|
|
@ -91,7 +94,7 @@ const mapSkuFeature = (feature: SkuFeatureRow): AdminSkuFeature => ({
|
|||
featureValue: feature.featureValue,
|
||||
})
|
||||
|
||||
const mapSku = (sku: SkuRow, features: SkuFeatureRow[] = []): AdminSku => ({
|
||||
const mapSku = (sku: SkuRow, features: SkuFeatureRow[] = [], comboItems: any[] = []): AdminSku => ({
|
||||
id: sku.id,
|
||||
productId: sku.productId,
|
||||
name: sku.name ?? null,
|
||||
|
|
@ -105,6 +108,7 @@ const mapSku = (sku: SkuRow, features: SkuFeatureRow[] = []): AdminSku => ({
|
|||
flashPrice: sku.flashPrice ? String(sku.flashPrice) : null,
|
||||
createdAt: sku.createdAt,
|
||||
features: features.map(mapSkuFeature),
|
||||
comboItems: comboItems,
|
||||
})
|
||||
|
||||
const mapSpecialDeal = (deal: SpecialDealRow): AdminSpecialDeal => ({
|
||||
|
|
@ -128,7 +132,7 @@ const mapTagInfo = (tag: ProductTagInfoRow): AdminProductTagInfo => ({
|
|||
export async function getAllProducts(): Promise<AdminProductWithRelations[]> {
|
||||
type ProductWithRelationsRow = ProductRow & {
|
||||
store: StoreRow | null
|
||||
skus: Array<SkuRow & { features: SkuFeatureRow[] }>
|
||||
skus: Array<SkuRow & { features: SkuFeatureRow[]; comboItems: any[] }>
|
||||
}
|
||||
const products = await db.query.productInfo.findMany({
|
||||
orderBy: productInfo.name,
|
||||
|
|
@ -137,6 +141,11 @@ export async function getAllProducts(): Promise<AdminProductWithRelations[]> {
|
|||
skus: {
|
||||
with: {
|
||||
features: true,
|
||||
comboItems: {
|
||||
with: {
|
||||
sku: { with: { product: true, features: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -145,7 +154,17 @@ export async function getAllProducts(): Promise<AdminProductWithRelations[]> {
|
|||
return products.map((product) => ({
|
||||
...mapProduct(product),
|
||||
store: product.store ? mapStore(product.store) : null,
|
||||
skus: product.skus.map((sku) => mapSku(sku, sku.features)),
|
||||
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)
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -157,6 +176,11 @@ export async function getProductById(id: number): Promise<AdminProductWithDetail
|
|||
skus: {
|
||||
with: {
|
||||
features: true,
|
||||
comboItems: {
|
||||
with: {
|
||||
sku: { with: { product: true, features: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -180,12 +204,24 @@ export async function getProductById(id: number): Promise<AdminProductWithDetail
|
|||
with: {
|
||||
tag: true,
|
||||
},
|
||||
}) 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 {
|
||||
...mapProduct(product),
|
||||
store: product.store ? mapStore(product.store) : null,
|
||||
skus: product.skus.map((sku) => mapSku(sku, sku.features)),
|
||||
skus: skusWithCombos,
|
||||
deals: deals.map(mapSpecialDeal),
|
||||
tags: productTagsData.map((tag) => mapTagInfo(tag.tag)),
|
||||
}
|
||||
|
|
@ -227,6 +263,7 @@ 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(
|
||||
|
|
@ -251,6 +288,15 @@ 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({
|
||||
|
|
@ -283,6 +329,7 @@ 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))
|
||||
|
||||
|
|
@ -326,6 +373,16 @@ 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({
|
||||
|
|
@ -370,29 +427,6 @@ 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)),
|
||||
|
|
@ -815,35 +849,39 @@ export async function updateProductPrices(updates: Array<{
|
|||
}
|
||||
|
||||
const productIds = updates.map((update) => update.productId)
|
||||
const existingSkus = await db.query.productSkus.findMany({
|
||||
where: inArray(productSkus.id, productIds),
|
||||
columns: { id: true },
|
||||
}) as Array<{ id: number }>
|
||||
|
||||
const existingIds = new Set(existingSkus.map((sku: { id: number }) => sku.id))
|
||||
// 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),
|
||||
columns: { id: true },
|
||||
})
|
||||
})
|
||||
const existingIds = new Set(existingSkuChunks.flat().map((sku: { id: number }) => sku.id))
|
||||
const invalidIds = productIds.filter((id) => !existingIds.has(id))
|
||||
|
||||
if (invalidIds.length > 0) {
|
||||
return { updatedCount: 0, invalidIds }
|
||||
}
|
||||
|
||||
const updatePromises = updates.map((update) => {
|
||||
const { productId, price, marketPrice, flashPrice, isFlashAvailable } = update
|
||||
const updateData: any = {}
|
||||
// Apply updates in chunks inside a single transaction
|
||||
await runBatched(db, updates, 10, async (tx, chunk) => {
|
||||
for (const update of chunk) {
|
||||
const { productId, price, marketPrice, flashPrice, isFlashAvailable } = update
|
||||
const updateData: any = {}
|
||||
|
||||
if (price !== undefined) updateData.price = price.toString()
|
||||
if (marketPrice !== undefined) updateData.marketPrice = marketPrice === null ? null : marketPrice.toString()
|
||||
if (flashPrice !== undefined) updateData.flashPrice = flashPrice === null ? null : flashPrice.toString()
|
||||
if (isFlashAvailable !== undefined) updateData.isFlashAvailable = isFlashAvailable
|
||||
if (price !== undefined) updateData.price = price.toString()
|
||||
if (marketPrice !== undefined) updateData.marketPrice = marketPrice === null ? null : marketPrice.toString()
|
||||
if (flashPrice !== undefined) updateData.flashPrice = flashPrice === null ? null : flashPrice.toString()
|
||||
if (isFlashAvailable !== undefined) updateData.isFlashAvailable = isFlashAvailable
|
||||
|
||||
return db
|
||||
.update(productSkus)
|
||||
.set(updateData)
|
||||
.where(eq(productSkus.id, productId))
|
||||
await tx
|
||||
.update(productSkus)
|
||||
.set(updateData)
|
||||
.where(eq(productSkus.id, productId))
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(updatePromises)
|
||||
|
||||
return { updatedCount: updates.length, invalidIds: [] }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
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
|
||||
|
|
@ -44,32 +45,36 @@ export async function createStore(
|
|||
input: CreateStoreInput,
|
||||
products?: number[]
|
||||
): Promise<Store> {
|
||||
const [newStore] = await db
|
||||
.insert(storeInfo)
|
||||
.values({
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
imageUrl: input.imageUrl,
|
||||
owner: input.owner,
|
||||
})
|
||||
.returning()
|
||||
return db.transaction(async (tx) => {
|
||||
const [newStore] = await tx
|
||||
.insert(storeInfo)
|
||||
.values({
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
imageUrl: input.imageUrl,
|
||||
owner: input.owner,
|
||||
})
|
||||
.returning()
|
||||
|
||||
if (products && products.length > 0) {
|
||||
await db
|
||||
.update(productInfo)
|
||||
.set({ storeId: newStore.id })
|
||||
.where(inArray(productInfo.id, products))
|
||||
}
|
||||
if (products && products.length > 0) {
|
||||
await runBatched(tx, products, 10, async (t, chunk) => {
|
||||
await t
|
||||
.update(productInfo)
|
||||
.set({ storeId: newStore.id })
|
||||
.where(inArray(productInfo.id, chunk))
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
id: newStore.id,
|
||||
name: newStore.name,
|
||||
description: newStore.description,
|
||||
imageUrl: newStore.imageUrl,
|
||||
owner: newStore.owner,
|
||||
createdAt: newStore.createdAt,
|
||||
// updatedAt: newStore.updatedAt,
|
||||
}
|
||||
return {
|
||||
id: newStore.id,
|
||||
name: newStore.name,
|
||||
description: newStore.description,
|
||||
imageUrl: newStore.imageUrl,
|
||||
owner: newStore.owner,
|
||||
createdAt: newStore.createdAt,
|
||||
// updatedAt: newStore.updatedAt,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export interface UpdateStoreInput {
|
||||
|
|
@ -84,42 +89,46 @@ export async function updateStore(
|
|||
input: UpdateStoreInput,
|
||||
products?: number[]
|
||||
): Promise<Store> {
|
||||
const [updatedStore] = await db
|
||||
.update(storeInfo)
|
||||
.set({
|
||||
...input,
|
||||
// updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(storeInfo.id, id))
|
||||
.returning()
|
||||
return db.transaction(async (tx) => {
|
||||
const [updatedStore] = await tx
|
||||
.update(storeInfo)
|
||||
.set({
|
||||
...input,
|
||||
// updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(storeInfo.id, id))
|
||||
.returning()
|
||||
|
||||
if (!updatedStore) {
|
||||
throw new Error('Store not found')
|
||||
}
|
||||
|
||||
if (products !== undefined) {
|
||||
await db
|
||||
.update(productInfo)
|
||||
.set({ storeId: null })
|
||||
.where(eq(productInfo.storeId, id))
|
||||
|
||||
if (products.length > 0) {
|
||||
await db
|
||||
.update(productInfo)
|
||||
.set({ storeId: id })
|
||||
.where(inArray(productInfo.id, products))
|
||||
if (!updatedStore) {
|
||||
throw new Error('Store not found')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: updatedStore.id,
|
||||
name: updatedStore.name,
|
||||
description: updatedStore.description,
|
||||
imageUrl: updatedStore.imageUrl,
|
||||
owner: updatedStore.owner,
|
||||
createdAt: updatedStore.createdAt,
|
||||
// updatedAt: updatedStore.updatedAt,
|
||||
}
|
||||
if (products !== undefined) {
|
||||
await tx
|
||||
.update(productInfo)
|
||||
.set({ storeId: null })
|
||||
.where(eq(productInfo.storeId, id))
|
||||
|
||||
if (products.length > 0) {
|
||||
await runBatched(tx, products, 10, async (t, chunk) => {
|
||||
await t
|
||||
.update(productInfo)
|
||||
.set({ storeId: id })
|
||||
.where(inArray(productInfo.id, chunk))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: updatedStore.id,
|
||||
name: updatedStore.name,
|
||||
description: updatedStore.description,
|
||||
imageUrl: updatedStore.imageUrl,
|
||||
owner: updatedStore.owner,
|
||||
createdAt: updatedStore.createdAt,
|
||||
// updatedAt: updatedStore.updatedAt,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteStore(id: number): Promise<{ message: string }> {
|
||||
|
|
|
|||
|
|
@ -65,11 +65,13 @@ 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 }),
|
||||
|
|
@ -192,6 +194,7 @@ 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', {
|
||||
|
|
@ -217,6 +220,14 @@ 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(),
|
||||
|
|
@ -581,12 +592,25 @@ 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),
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -1,23 +1,8 @@
|
|||
import { db } from '../db/db_index'
|
||||
import { productInfo, keyValStore } from '../db/schema'
|
||||
import { inArray, eq } from 'drizzle-orm'
|
||||
import { keyValStore } from '../db/schema'
|
||||
import { 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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
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
|
||||
|
|
@ -13,26 +14,28 @@ export async function deleteOrdersWithRelations(orderIds: number[]): Promise<voi
|
|||
return
|
||||
}
|
||||
|
||||
// Delete child records first (in correct order to avoid FK constraint errors)
|
||||
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 db.delete(couponUsage).where(inArray(couponUsage.orderId, orderIds))
|
||||
// 1. Delete coupon usage records
|
||||
await tx.delete(couponUsage).where(inArray(couponUsage.orderId, chunk))
|
||||
|
||||
// 2. Delete complaints related to these orders
|
||||
await db.delete(complaints).where(inArray(complaints.orderId, orderIds))
|
||||
// 2. Delete complaints related to these orders
|
||||
await tx.delete(complaints).where(inArray(complaints.orderId, chunk))
|
||||
|
||||
// 3. Delete refunds
|
||||
await db.delete(refunds).where(inArray(refunds.orderId, orderIds))
|
||||
// 3. Delete refunds
|
||||
await tx.delete(refunds).where(inArray(refunds.orderId, chunk))
|
||||
|
||||
// 4. Delete payments
|
||||
await db.delete(payments).where(inArray(payments.orderId, orderIds))
|
||||
// 4. Delete payments
|
||||
await tx.delete(payments).where(inArray(payments.orderId, chunk))
|
||||
|
||||
// 5. Delete order status records
|
||||
await db.delete(orderStatus).where(inArray(orderStatus.orderId, orderIds))
|
||||
// 5. Delete order status records
|
||||
await tx.delete(orderStatus).where(inArray(orderStatus.orderId, chunk))
|
||||
|
||||
// 6. Delete order items
|
||||
await db.delete(orderItems).where(inArray(orderItems.orderId, orderIds))
|
||||
// 6. Delete order items
|
||||
await tx.delete(orderItems).where(inArray(orderItems.orderId, chunk))
|
||||
|
||||
// 7. Finally delete the orders themselves
|
||||
await db.delete(orders).where(inArray(orders.id, orderIds))
|
||||
// 7. Finally delete the orders themselves
|
||||
await tx.delete(orders).where(inArray(orders.id, chunk))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
39
packages/db_helper_sqlite/src/lib/run-batched.ts
Normal file
39
packages/db_helper_sqlite/src/lib/run-batched.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/** 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[];
|
||||
}
|
||||
|
|
@ -57,6 +57,7 @@ export interface ProductBasicData {
|
|||
productQuantity: number
|
||||
isFlashAvailable: boolean
|
||||
flashPrice: string | null
|
||||
productType: string
|
||||
}
|
||||
|
||||
export interface StoreBasicData {
|
||||
|
|
@ -130,6 +131,7 @@ export async function getAllProductsForCache(): Promise<ProductBasicData[]> {
|
|||
productQuantity: 1,
|
||||
isFlashAvailable: sku.isFlashAvailable,
|
||||
flashPrice: sku.flashPrice ? String(sku.flashPrice) : null,
|
||||
productType: sku.product?.productType ?? 'item',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -176,6 +178,41 @@ 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
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import type {
|
|||
UserRecentProduct,
|
||||
} from '@packages/shared'
|
||||
import { coerceDate } from '../lib/date'
|
||||
import { runBatched } from '../lib/run-batched'
|
||||
|
||||
export interface OrderItemInput {
|
||||
productId: number
|
||||
|
|
@ -626,12 +627,16 @@ export async function getRecentlyDeliveredOrderIds(
|
|||
export async function getSkuIdsFromOrders(
|
||||
orderIds: number[]
|
||||
): Promise<number[]> {
|
||||
const orderItemsResult = await db
|
||||
.select({ skuId: orderItems.skuId })
|
||||
.from(orderItems)
|
||||
.where(inArray(orderItems.orderId, orderIds))
|
||||
if (orderIds.length === 0) return []
|
||||
|
||||
return [...new Set(orderItemsResult.map((item) => item.skuId))]
|
||||
const skuChunks = await runBatched(db, orderIds, 10, async (tx, chunk) => {
|
||||
return tx
|
||||
.select({ skuId: orderItems.skuId })
|
||||
.from(orderItems)
|
||||
.where(inArray(orderItems.orderId, chunk))
|
||||
})
|
||||
|
||||
return [...new Set(skuChunks.flat().map((item) => item.skuId))]
|
||||
}
|
||||
|
||||
export interface RecentProductData {
|
||||
|
|
@ -649,19 +654,26 @@ export async function getProductsForRecentOrders(
|
|||
productIds: number[],
|
||||
limit: number
|
||||
): Promise<RecentProductData[]> {
|
||||
const skus = await db.query.productSkus.findMany({
|
||||
where: and(
|
||||
inArray(productSkus.id, productIds),
|
||||
eq(productSkus.isSuspended, false)
|
||||
),
|
||||
with: {
|
||||
product: true,
|
||||
features: true,
|
||||
},
|
||||
orderBy: desc(productSkus.createdAt),
|
||||
limit,
|
||||
if (productIds.length === 0) return []
|
||||
|
||||
const skuChunks = await runBatched(db, productIds, 10, async (tx, chunk) => {
|
||||
return tx.query.productSkus.findMany({
|
||||
where: and(
|
||||
inArray(productSkus.id, chunk),
|
||||
eq(productSkus.isSuspended, false)
|
||||
),
|
||||
with: {
|
||||
product: true,
|
||||
features: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const skus = skuChunks
|
||||
.flat()
|
||||
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
||||
.slice(0, limit)
|
||||
|
||||
return skus.map((sku) => {
|
||||
const features = sku.features || []
|
||||
return {
|
||||
|
|
@ -711,48 +723,49 @@ export interface OrderWithFullData {
|
|||
export async function getOrdersByIdsWithFullData(
|
||||
orderIds: number[]
|
||||
): Promise<OrderWithFullData[]> {
|
||||
console.log('getting orders byid')
|
||||
if (orderIds.length === 0) return []
|
||||
|
||||
const ordersResp = await db.query.orders.findMany({
|
||||
where: inArray(orders.id, orderIds),
|
||||
with: {
|
||||
address: {
|
||||
columns: {
|
||||
name: true,
|
||||
addressLine1: true,
|
||||
addressLine2: true,
|
||||
city: true,
|
||||
state: true,
|
||||
pincode: true,
|
||||
phone: true,
|
||||
const orderChunks = await runBatched(db, orderIds, 10, async (tx, chunk) => {
|
||||
return tx.query.orders.findMany({
|
||||
where: inArray(orders.id, chunk),
|
||||
with: {
|
||||
address: {
|
||||
columns: {
|
||||
name: true,
|
||||
addressLine1: true,
|
||||
addressLine2: true,
|
||||
city: true,
|
||||
state: true,
|
||||
pincode: true,
|
||||
phone: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderItems: {
|
||||
with: {
|
||||
sku: {
|
||||
columns: {
|
||||
name: true,
|
||||
price: true,
|
||||
},
|
||||
with: {
|
||||
product: {
|
||||
columns: { name: true },
|
||||
orderItems: {
|
||||
with: {
|
||||
sku: {
|
||||
columns: {
|
||||
name: true,
|
||||
price: true,
|
||||
},
|
||||
with: {
|
||||
product: {
|
||||
columns: { name: true },
|
||||
},
|
||||
features: true,
|
||||
},
|
||||
features: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
slot: {
|
||||
columns: {
|
||||
deliveryTime: true,
|
||||
slot: {
|
||||
columns: {
|
||||
deliveryTime: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
// as Promise<OrderWithFullData[]>
|
||||
|
||||
return ordersResp as OrderWithFullData[];
|
||||
return orderChunks.flat() as OrderWithFullData[]
|
||||
}
|
||||
|
||||
export interface OrderWithCancellationData extends OrderWithFullData {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { db } from '../db/db_index'
|
||||
import { deliverySlotInfo, productInfo, productSkus, productReviews, productTags, specialDeals, storeInfo, users } from '../db/schema'
|
||||
import { deliverySlotInfo, productInfo, productCombos, 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,6 +44,25 @@ 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',
|
||||
|
|
@ -69,6 +88,8 @@ export async function getProductDetailById(skuId: number): Promise<UserProductDe
|
|||
price: String(deal.price ?? '0'),
|
||||
validTill: deal.validTill,
|
||||
})),
|
||||
productType: product?.productType ?? 'item',
|
||||
comboItems,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -151,6 +172,8 @@ export async function createProductReview(
|
|||
export interface ProductSummaryData {
|
||||
id: number
|
||||
name: string
|
||||
skuId: number
|
||||
skuName: string | null
|
||||
shortDescription: string | null
|
||||
price: string
|
||||
marketPrice: string | null
|
||||
|
|
@ -158,48 +181,50 @@ export interface ProductSummaryData {
|
|||
isOutOfStock: boolean
|
||||
unitShortNotation: string
|
||||
productQuantity: number
|
||||
features: { featureName: string; featureValue: string }[]
|
||||
}
|
||||
|
||||
export async function getAllProductsWithUnits(tagId?: number): Promise<ProductSummaryData[]> {
|
||||
let productIds: number[] | null = null
|
||||
const taggedProductIdSet = new Set<number>()
|
||||
|
||||
// If tagId is provided, get products that have this tag
|
||||
// If tagId is provided, get product IDs that have this tag
|
||||
if (tagId) {
|
||||
const taggedProducts = await db
|
||||
.select({ productId: productTags.productId })
|
||||
.from(productTags)
|
||||
.where(eq(productTags.tagId, tagId))
|
||||
|
||||
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)
|
||||
for (const tp of taggedProducts) {
|
||||
taggedProductIdSet.add(tp.productId)
|
||||
}
|
||||
}
|
||||
|
||||
const skus = await db.query.productSkus.findMany({
|
||||
where: whereCondition,
|
||||
with: {
|
||||
product: true,
|
||||
features: true,
|
||||
},
|
||||
})
|
||||
|
||||
return skus.map((sku) => {
|
||||
const features = sku.features || []
|
||||
return {
|
||||
id: sku.product?.id ?? 0,
|
||||
name: sku.product?.name ?? 'Unknown',
|
||||
skuId: sku.id,
|
||||
skuName: sku.name ?? null,
|
||||
shortDescription: sku.product?.shortDescription ?? null,
|
||||
price: String(sku.price ?? '0'),
|
||||
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
|
||||
return skus
|
||||
.filter((sku) => {
|
||||
if (!tagId) return true
|
||||
return taggedProductIdSet.has(sku.productId)
|
||||
})
|
||||
.map((sku) => {
|
||||
const features = sku.features || []
|
||||
return {
|
||||
id: sku.product?.id ?? 0,
|
||||
name: sku.product?.name ?? 'Unknown',
|
||||
skuId: sku.id,
|
||||
skuName: sku.name ?? null,
|
||||
shortDescription: sku.product?.shortDescription ?? null,
|
||||
price: String(sku.price ?? '0'),
|
||||
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
|
||||
images: sku.images,
|
||||
isOutOfStock: sku.isOutOfStock,
|
||||
unitShortNotation: features.map((f) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
|
||||
productQuantity: 1,
|
||||
features: features.map((f) => ({ featureName: f.featureName, featureValue: f.featureValue })),
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { db } from '../db/db_index'
|
||||
import { productInfo, productSkus, storeInfo } from '../db/schema'
|
||||
import { and, eq, inArray, sql } from 'drizzle-orm'
|
||||
import { and, asc, eq, inArray } from 'drizzle-orm'
|
||||
import type { InferSelectModel } from 'drizzle-orm'
|
||||
import type { UserStoreDetailData, UserStoreProductData, UserStoreSummaryData, StoreSummary } from '@packages/shared'
|
||||
|
||||
|
|
@ -12,72 +12,44 @@ const getStringArray = (value: unknown): string[] | null => {
|
|||
}
|
||||
|
||||
export async function getStoreSummaries(): Promise<UserStoreSummaryData[]> {
|
||||
// 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,
|
||||
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 storesData = await db.select({
|
||||
id: storeInfo.id,
|
||||
name: storeInfo.name,
|
||||
description: storeInfo.description,
|
||||
imageUrl: storeInfo.imageUrl,
|
||||
}).from(storeInfo)
|
||||
|
||||
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))
|
||||
const skus = await db.query.productSkus.findMany({
|
||||
where: eq(productSkus.isSuspended, false),
|
||||
with: { product: true },
|
||||
orderBy: asc(productSkus.id),
|
||||
})
|
||||
|
||||
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),
|
||||
}))
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
return {
|
||||
id: store.id,
|
||||
name: store.name,
|
||||
description: store.description ?? null,
|
||||
imageUrl: store.imageUrl ?? null,
|
||||
productCount: store.productCount || 0,
|
||||
sampleProducts,
|
||||
}
|
||||
})
|
||||
)
|
||||
return storesData.map((store) => {
|
||||
const storeSkus = skusByStore.get(store.id) || []
|
||||
const sampleProducts = storeSkus.slice(0, 3).map((sku) => ({
|
||||
id: sku.id,
|
||||
name: sku.product?.name ?? sku.name ?? 'Unknown',
|
||||
images: getStringArray(sku.images),
|
||||
}))
|
||||
|
||||
return storesWithDetails
|
||||
return {
|
||||
id: store.id,
|
||||
name: store.name,
|
||||
description: store.description ?? null,
|
||||
imageUrl: store.imageUrl ?? null,
|
||||
productCount: storeSkus.length,
|
||||
sampleProducts,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function getStoreDetail(storeId: number): Promise<UserStoreDetailData | null> {
|
||||
|
|
|
|||
|
|
@ -360,13 +360,13 @@ export interface AdminUnit {
|
|||
fullName: string;
|
||||
}
|
||||
|
||||
export interface AdminSkuVariant {
|
||||
id: number
|
||||
skuId: number
|
||||
name: 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 {
|
||||
|
|
@ -383,7 +383,7 @@ export interface AdminSku {
|
|||
flashPrice: string | null
|
||||
createdAt: Date
|
||||
features: AdminSkuFeature[]
|
||||
}
|
||||
comboItems: AdminProductComboItem[]
|
||||
}
|
||||
|
||||
export interface AdminProduct {
|
||||
|
|
@ -394,6 +394,7 @@ export interface AdminProduct {
|
|||
storeId: number | null;
|
||||
incrementStep: number;
|
||||
createdAt: Date;
|
||||
productType: 'item' | 'combo';
|
||||
}
|
||||
|
||||
export interface AdminProductWithRelations extends AdminProduct {
|
||||
|
|
|
|||
|
|
@ -286,6 +286,15 @@ 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;
|
||||
|
|
@ -303,6 +312,8 @@ export interface UserProductDetailData {
|
|||
flashPrice: string | null;
|
||||
deliverySlots: UserProductDeliverySlot[];
|
||||
specialDeals: UserProductSpecialDeal[];
|
||||
productType: string;
|
||||
comboItems: UserProductComboItem[];
|
||||
}
|
||||
|
||||
export interface UserProductDetail extends UserProductDetailData {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue