Compare commits
No commits in common. "db2ee44a5f61f618c72d3b699c9e5285721c9a92" and "1b62c6add49c9b9539cbc9d94d8ed7be043ed91c" have entirely different histories.
db2ee44a5f
...
1b62c6add4
33 changed files with 569 additions and 608 deletions
|
|
@ -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
4
APIS_TO_REMOVE.md
Normal 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
|
||||
|
|
@ -43,7 +43,7 @@ const SkuItemComponent: React.FC<SkuItemProps> = ({
|
|||
const displayPrice = change.price !== undefined ? change.price : sku.price;
|
||||
const displayMarketPrice = change.marketPrice !== undefined ? change.marketPrice : sku.marketPrice;
|
||||
const displayFlashPrice = change.flashPrice !== undefined ? change.flashPrice : sku.flashPrice;
|
||||
const displayUnit = (sku.features || []).map((f: any) => f.featureValue).join(' ') || '—';
|
||||
const displayProductQuantity = change.productQuantity !== undefined ? change.productQuantity : sku.productQuantity;
|
||||
|
||||
return (
|
||||
<View style={tw`bg-white p-4 mb-3 rounded-xl border border-gray-200 shadow-sm`}>
|
||||
|
|
@ -130,7 +130,7 @@ const SkuItemComponent: React.FC<SkuItemProps> = ({
|
|||
<View style={tw`items-center`}>
|
||||
<MyText style={tw`text-xs text-gray-500 mb-1`}>Size</MyText>
|
||||
<View style={tw`flex-row items-center`}>
|
||||
<MyText style={tw`text-sm text-blue-600`}>{displayUnit}</MyText>
|
||||
<MyText style={tw`text-sm text-blue-600`}>{displayProductQuantity ? `${displayProductQuantity}${sku.unit?.shortNotation || ''}` : "N/A"}</MyText>
|
||||
<TouchableOpacity onPress={() => openEditDialog(sku, productName)} style={tw`ml-1`}>
|
||||
<MaterialIcons name="edit" size={14} color="#6b7280" />
|
||||
</TouchableOpacity>
|
||||
|
|
@ -145,6 +145,7 @@ interface PendingChange {
|
|||
price?: number;
|
||||
marketPrice?: number | null;
|
||||
flashPrice?: number | null;
|
||||
productQuantity?: number | null;
|
||||
isFlashAvailable?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -155,6 +156,7 @@ interface EditDialogState {
|
|||
tempPrice: string;
|
||||
tempMarketPrice: string;
|
||||
tempFlashPrice: string;
|
||||
tempProductQuantity: string;
|
||||
}
|
||||
|
||||
export default function PricesOverview() {
|
||||
|
|
@ -168,6 +170,7 @@ export default function PricesOverview() {
|
|||
tempPrice: "",
|
||||
tempMarketPrice: "",
|
||||
tempFlashPrice: "",
|
||||
tempProductQuantity: "",
|
||||
});
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
|
||||
|
|
@ -230,6 +233,7 @@ export default function PricesOverview() {
|
|||
tempPrice: (change.price ?? sku.price)?.toString() || "",
|
||||
tempMarketPrice: (change.marketPrice ?? sku.marketPrice)?.toString() || "",
|
||||
tempFlashPrice: (change.flashPrice ?? sku.flashPrice)?.toString() || "",
|
||||
tempProductQuantity: (change.productQuantity ?? sku.productQuantity)?.toString() || "",
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -237,6 +241,7 @@ export default function PricesOverview() {
|
|||
const price = parseFloat(editDialog.tempPrice);
|
||||
const marketPrice = editDialog.tempMarketPrice ? parseFloat(editDialog.tempMarketPrice) : null;
|
||||
const flashPrice = editDialog.tempFlashPrice ? parseFloat(editDialog.tempFlashPrice) : null;
|
||||
const productQuantity = editDialog.tempProductQuantity ? parseFloat(editDialog.tempProductQuantity) : null;
|
||||
|
||||
if (isNaN(price) || price <= 0) {
|
||||
Alert.alert("Error", "Please enter a valid price");
|
||||
|
|
@ -253,24 +258,32 @@ export default function PricesOverview() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (editDialog.tempProductQuantity && (isNaN(productQuantity!) || productQuantity! <= 0)) {
|
||||
Alert.alert("Error", "Please enter a valid size");
|
||||
return;
|
||||
}
|
||||
|
||||
setPendingChanges(prev => ({
|
||||
...prev,
|
||||
[editDialog.sku.id]: {
|
||||
price: price !== parseFloat(editDialog.sku.price) ? price : undefined,
|
||||
marketPrice: marketPrice !== parseFloat(editDialog.sku.marketPrice || '0') ? marketPrice : undefined,
|
||||
flashPrice: flashPrice !== parseFloat(editDialog.sku.flashPrice || '0') ? flashPrice : undefined,
|
||||
productQuantity: productQuantity !== (editDialog.sku.productQuantity || 1) ? productQuantity : undefined,
|
||||
},
|
||||
}));
|
||||
|
||||
setEditDialog({ open: false, sku: null, productName: "", tempPrice: "", tempMarketPrice: "", tempFlashPrice: "", tempProductQuantity: "" });
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const updates = Object.entries(pendingChanges).map(([skuId, change]) => {
|
||||
const sku = allSkus.find(s => s.id === parseInt(skuId));
|
||||
const update: any = { productId: sku?.id };
|
||||
const update: any = { productId: sku?.productId };
|
||||
if (change.price !== undefined) update.price = change.price;
|
||||
if (change.marketPrice !== undefined) update.marketPrice = change.marketPrice;
|
||||
if (change.flashPrice !== undefined) update.flashPrice = change.flashPrice;
|
||||
if (change.productQuantity !== undefined) update.productQuantity = change.productQuantity;
|
||||
if (change.isFlashAvailable !== undefined) update.isFlashAvailable = change.isFlashAvailable;
|
||||
return update;
|
||||
});
|
||||
|
|
@ -364,7 +377,7 @@ export default function PricesOverview() {
|
|||
/>
|
||||
)}
|
||||
|
||||
<BottomDialog open={editDialog.open} onClose={() => setEditDialog({ ...editDialog, open: false, tempFlashPrice: "", tempMarketPrice: "", tempPrice: "" })}>
|
||||
<BottomDialog open={editDialog.open} onClose={() => setEditDialog({ ...editDialog, open: false, tempFlashPrice: "", tempMarketPrice: "", tempPrice: "", tempProductQuantity: "" })}>
|
||||
<View style={tw`p-4`}>
|
||||
<MyText style={tw`text-lg font-bold mb-1`}>{editDialog.productName}</MyText>
|
||||
<MyText style={tw`text-sm text-gray-500 mb-4`}>{editDialog.sku?.displayName || editDialog.sku?.name || ''}</MyText>
|
||||
|
|
@ -406,6 +419,8 @@ export default function PricesOverview() {
|
|||
<MyText style={tw`text-sm font-medium mb-1`}>Size</MyText>
|
||||
<TextInput
|
||||
style={tw`border border-gray-300 rounded-md px-3 py-2`}
|
||||
value={editDialog.tempProductQuantity}
|
||||
onChangeText={(text) => setEditDialog({ ...editDialog, tempProductQuantity: text })}
|
||||
keyboardType="decimal-pad"
|
||||
placeholder="Enter size"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -48,9 +48,6 @@ export default function AddProduct() {
|
|||
featureName: attr.featureName,
|
||||
featureValue: attr.featureValue,
|
||||
})),
|
||||
comboItems: (variant.comboItems || []).map((ci: any) => ({
|
||||
skuId: typeof ci.skuId === 'string' ? parseInt(ci.skuId) : ci.skuId,
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -59,7 +56,6 @@ export default function AddProduct() {
|
|||
shortDescription: values.shortDescription || undefined,
|
||||
longDescription: values.longDescription || undefined,
|
||||
storeId: values.storeId,
|
||||
productType: values.productType,
|
||||
incrementStep: 1,
|
||||
skus,
|
||||
})
|
||||
|
|
@ -76,17 +72,14 @@ export default function AddProduct() {
|
|||
shortDescription: '',
|
||||
longDescription: '',
|
||||
storeId: 1,
|
||||
productType: 'item' as const,
|
||||
variants: [
|
||||
{
|
||||
id: undefined as number | undefined,
|
||||
name: '',
|
||||
price: '',
|
||||
marketPrice: '',
|
||||
isFlashAvailable: false,
|
||||
flashPrice: '',
|
||||
attributes: [{ featureName: 'quantity', featureValue: '' }],
|
||||
comboItems: [] as { skuId: number | string }[],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ export default function EditProduct() {
|
|||
shortDescription: productData.shortDescription || '',
|
||||
longDescription: productData.longDescription || '',
|
||||
storeId: productData.storeId || 1,
|
||||
productType: (productData.productType as 'item' | 'combo') || 'item',
|
||||
variants: (productData.skus || []).map((sku) => ({
|
||||
id: sku.id,
|
||||
name: sku.name || '',
|
||||
|
|
@ -53,9 +52,6 @@ export default function EditProduct() {
|
|||
featureName: f.featureName,
|
||||
featureValue: f.featureValue,
|
||||
})),
|
||||
comboItems: (sku.comboItems || []).map((ci: any) => ({
|
||||
skuId: ci.skuId.toString(),
|
||||
})),
|
||||
})),
|
||||
}
|
||||
}, [productData])
|
||||
|
|
@ -122,9 +118,6 @@ export default function EditProduct() {
|
|||
featureName: attr.featureName,
|
||||
featureValue: attr.featureValue,
|
||||
})),
|
||||
comboItems: (variant.comboItems || []).map((ci: any) => ({
|
||||
skuId: typeof ci.skuId === 'string' ? parseInt(ci.skuId) : ci.skuId,
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -134,7 +127,6 @@ export default function EditProduct() {
|
|||
shortDescription: values.shortDescription || undefined,
|
||||
longDescription: values.longDescription || undefined,
|
||||
storeId: values.storeId,
|
||||
productType: values.productType,
|
||||
incrementStep: 1,
|
||||
skus,
|
||||
deletedImageKeys,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ interface Variant {
|
|||
isFlashAvailable: boolean
|
||||
flashPrice: string
|
||||
attributes: Attribute[]
|
||||
comboItems: { skuId: number | string }[]
|
||||
}
|
||||
|
||||
interface ProductFormData {
|
||||
|
|
@ -26,7 +25,6 @@ interface ProductFormData {
|
|||
shortDescription: string
|
||||
longDescription: string
|
||||
storeId: number
|
||||
productType: 'item' | 'combo'
|
||||
variants: Variant[]
|
||||
}
|
||||
|
||||
|
|
@ -46,14 +44,12 @@ interface ProductFormProps {
|
|||
const defaultAttribute = (): Attribute => ({ featureName: 'quantity', featureValue: '' })
|
||||
|
||||
const defaultVariant = (): Variant => ({
|
||||
id: undefined,
|
||||
name: '',
|
||||
price: '',
|
||||
marketPrice: '',
|
||||
isFlashAvailable: false,
|
||||
flashPrice: '',
|
||||
attributes: [defaultAttribute()],
|
||||
comboItems: [],
|
||||
})
|
||||
|
||||
const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||
|
|
@ -80,12 +76,6 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
value: store.id,
|
||||
})) || []
|
||||
|
||||
const { data: skusData } = trpc.common.product.getAllSkusSummary.useQuery({})
|
||||
const skuOptions = (skusData?.skus || []).map((sku) => ({
|
||||
label: sku.label,
|
||||
value: sku.id.toString(),
|
||||
}))
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
|
|
@ -146,19 +136,6 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<BottomDropdown
|
||||
topLabel="Product Type"
|
||||
label="Product Type"
|
||||
value={values.productType}
|
||||
options={[
|
||||
{ label: 'Item', value: 'item' },
|
||||
{ label: 'Combo', value: 'combo' },
|
||||
]}
|
||||
onValueChange={(value) => setFieldValue('productType', value)}
|
||||
placeholder="Select product type"
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
|
||||
<FieldArray name="variants">
|
||||
{({ push, remove }) => (
|
||||
<View>
|
||||
|
|
@ -294,46 +271,6 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
|||
}
|
||||
allowMultiple={true}
|
||||
/>
|
||||
|
||||
{values.productType === 'combo' && (
|
||||
<View style={tw`mt-3 pt-3 border-t border-gray-100`}>
|
||||
<View style={tw`flex-row justify-between items-center mb-2`}>
|
||||
<MyText style={tw`font-medium text-gray-600`}>Included Items</MyText>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
const items = variant.comboItems || []
|
||||
items.push({ skuId: '' })
|
||||
setFieldValue(`variants.${vIndex}.comboItems`, items)
|
||||
}}
|
||||
style={tw`bg-gray-200 px-2 py-0.5 rounded flex-row items-center`}
|
||||
>
|
||||
<MaterialIcons name="add" size={14} color="#4B5563" />
|
||||
<MyText style={tw`text-gray-600 text-xs ml-0.5`}>Add</MyText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{variant.comboItems?.map((ci, cIndex) => (
|
||||
<View key={cIndex} style={tw`flex-row items-center gap-2 mb-2`}>
|
||||
<View style={tw`flex-1`}>
|
||||
<BottomDropdown
|
||||
label="SKU"
|
||||
value={ci.skuId}
|
||||
options={skuOptions}
|
||||
onValueChange={(val) => setFieldValue(`variants.${vIndex}.comboItems.${cIndex}.skuId`, val)}
|
||||
placeholder="Select SKU"
|
||||
/>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
const items = variant.comboItems.filter((_, i) => i !== cIndex)
|
||||
setFieldValue(`variants.${vIndex}.comboItems`, items)
|
||||
}}
|
||||
>
|
||||
<MaterialIcons name="close" size={18} color="#EF4444" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import { createApp } from '@/src/app'
|
|||
// import signedUrlCache from '@/src/lib/signed-url-cache';
|
||||
import { seed } from '@/src/lib/seed';
|
||||
import '@/src/jobs/jobs-index';
|
||||
import { startAutomatedJobs } from '@/src/lib/automatedJobs';
|
||||
|
||||
seed()
|
||||
initFunc()
|
||||
startAutomatedJobs()
|
||||
|
||||
// signedUrlCache.loadFromDisk(); // Disabled for Workers compatibility
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,9 @@ export type {
|
|||
AdminProductListResponse,
|
||||
AdminProductResponse,
|
||||
AdminDeleteProductResult,
|
||||
AdminToggleOutOfStockResult,
|
||||
AdminUpdateSlotProductsResult,
|
||||
AdminSlotProductIdsResult,
|
||||
AdminSlotsProductIdsResult,
|
||||
AdminProductReview,
|
||||
AdminProductReviewWithSignedUrls,
|
||||
|
|
|
|||
137
apps/backend/src/lib/automatedJobs.ts
Normal file
137
apps/backend/src/lib/automatedJobs.ts
Normal 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();
|
||||
|
|
@ -64,6 +64,7 @@
|
|||
// replaceProductTags,
|
||||
// toggleProductOutOfStock,
|
||||
// updateSlotProducts,
|
||||
// getSlotProductIds,
|
||||
// getSlotsProductIds,
|
||||
// getAllUnits,
|
||||
// getAllProductTags,
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ export {
|
|||
replaceProductTags,
|
||||
mergeSkus,
|
||||
updateSlotProducts,
|
||||
getSlotProductIds,
|
||||
getSlotsProductIds,
|
||||
getAllUnits,
|
||||
getAllProductTags,
|
||||
|
|
@ -272,6 +273,7 @@ export {
|
|||
type SlotWithProductsData,
|
||||
type UserNegativityData,
|
||||
// Automated Jobs
|
||||
toggleFlashDeliveryForItems,
|
||||
toggleKeyVal,
|
||||
getAllKeyValStore,
|
||||
// Post-order handler helpers
|
||||
|
|
|
|||
|
|
@ -6,13 +6,11 @@ import {
|
|||
getAllSpecialDealsForCache,
|
||||
getAllProductTagsForCache,
|
||||
getProductById as getProductByIdFromDb,
|
||||
getAllProductCombosForCache,
|
||||
type ProductBasicData,
|
||||
type StoreBasicData,
|
||||
type DeliverySlotData,
|
||||
type SpecialDealData,
|
||||
type ProductTagData,
|
||||
type ProductComboCacheData,
|
||||
} from '@/src/dbService'
|
||||
import { scaffoldAssetUrl } from '@/src/lib/s3-client'
|
||||
|
||||
|
|
@ -35,15 +33,6 @@ interface Product {
|
|||
deliverySlots: Array<{ id: number; deliveryTime: Date; freezeTime: Date; isCapacityFull: boolean }>
|
||||
specialDeals: Array<{ quantity: string; price: string; validTill: Date }>
|
||||
productTags: string[]
|
||||
productType: string
|
||||
comboItems: Array<{
|
||||
skuId: number
|
||||
skuName: string | null
|
||||
unitNotation: string
|
||||
productName: string
|
||||
images: string[] | null
|
||||
price: string
|
||||
}>
|
||||
}
|
||||
|
||||
export async function initializeProducts(): Promise<void> {
|
||||
|
|
@ -224,14 +213,6 @@ export async function getAllProducts(): Promise<Product[]> {
|
|||
}
|
||||
|
||||
const products: Product[] = []
|
||||
const allProductCombos = await getAllProductCombosForCache()
|
||||
const productCombosMap = new Map<number, ProductComboCacheData[]>()
|
||||
for (const comboItem of allProductCombos) {
|
||||
if (!productCombosMap.has(comboItem.comboSkuId))
|
||||
productCombosMap.set(comboItem.comboSkuId, [])
|
||||
productCombosMap.get(comboItem.comboSkuId)!.push(comboItem)
|
||||
}
|
||||
|
||||
for (const product of productsData) {
|
||||
const signedImages = scaffoldAssetUrl(
|
||||
(product.images as string[]) || []
|
||||
|
|
@ -242,14 +223,6 @@ export async function getAllProducts(): Promise<Product[]> {
|
|||
const deliverySlots = deliverySlotsMap.get(product.id) || []
|
||||
const specialDeals = specialDealsMap.get(product.id) || []
|
||||
const productTags = productTagsMap.get(product.productId) || []
|
||||
const comboItems = (productCombosMap.get(product.id) || []).map((ci) => ({
|
||||
skuId: ci.skuId,
|
||||
skuName: ci.skuName,
|
||||
unitNotation: ci.unitNotation,
|
||||
productName: ci.productName,
|
||||
images: ci.images ? scaffoldAssetUrl((ci.images as string[]) || []) : null,
|
||||
price: ci.price,
|
||||
}))
|
||||
|
||||
products.push({
|
||||
id: product.id,
|
||||
|
|
@ -280,8 +253,6 @@ export async function getAllProducts(): Promise<Product[]> {
|
|||
validTill: d.validTill,
|
||||
})),
|
||||
productTags: productTags,
|
||||
productType: product.productType || 'item',
|
||||
comboItems: comboItems,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ async function transformSlotToStoreSlot(slot: SlotWithProductsData): Promise<Slo
|
|||
shortDescription: product.shortDescription,
|
||||
price: product.price.toString(),
|
||||
marketPrice: product.marketPrice?.toString() || null,
|
||||
unit: product.unitNotation || null,
|
||||
unit: product.unit?.shortNotation || null,
|
||||
images: scaffoldAssetUrl(
|
||||
(product.images as string[]) || []
|
||||
),
|
||||
|
|
@ -125,7 +125,7 @@ export async function initializeSlotStore(): Promise<void> {
|
|||
shortDescription: product.shortDescription,
|
||||
price: product.price.toString(),
|
||||
marketPrice: product.marketPrice?.toString() || null,
|
||||
unit: product.unitNotation || null,
|
||||
unit: product.unit?.shortNotation || null,
|
||||
images: scaffoldAssetUrl(
|
||||
(product.images as string[]) || []
|
||||
),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
getProductById as getProductByIdInDb,
|
||||
deleteProduct as deleteProductInDb,
|
||||
updateSlotProducts as updateSlotProductsInDb,
|
||||
getSlotProductIds as getSlotProductIdsInDb,
|
||||
getSlotsProductIds as getSlotsProductIdsInDb,
|
||||
getProductReviews as getProductReviewsInDb,
|
||||
respondToReview as respondToReviewInDb,
|
||||
|
|
@ -41,6 +42,7 @@ import type {
|
|||
AdminProductResponse,
|
||||
AdminDeleteProductResult,
|
||||
AdminUpdateSlotProductsResult,
|
||||
AdminSlotProductIdsResult,
|
||||
AdminSlotsProductIdsResult,
|
||||
AdminUpdateProductPricesResult,
|
||||
} from '@packages/shared'
|
||||
|
|
@ -188,7 +190,6 @@ export const productRouter = router({
|
|||
longDescription: z.string().optional(),
|
||||
storeId: z.number().min(1, 'Store is required'),
|
||||
incrementStep: z.number().optional().default(1),
|
||||
productType: z.enum(['item', 'combo']).optional().default('item'),
|
||||
skus: z.array(z.object({
|
||||
name: z.string().optional().nullable(),
|
||||
price: z.number().positive('Price must be positive'),
|
||||
|
|
@ -200,13 +201,10 @@ export const productRouter = router({
|
|||
featureName: z.string().min(1, 'Attribute name is required'),
|
||||
featureValue: z.string().min(1, 'Value is required'),
|
||||
})).min(1, 'At least one feature is required'),
|
||||
comboItems: z.array(z.object({
|
||||
skuId: z.number().int().positive(),
|
||||
})).optional(),
|
||||
})).min(1, 'At least one SKU is required'),
|
||||
}))
|
||||
.mutation(async ({ input }): Promise<{ product: AdminProductWithRelations; message: string }> => {
|
||||
const { name, shortDescription, longDescription, storeId, incrementStep, productType, skus } = input
|
||||
const { name, shortDescription, longDescription, storeId, incrementStep, skus } = input
|
||||
|
||||
const existingProduct = await checkProductExistsByName(name.trim())
|
||||
if (existingProduct) {
|
||||
|
|
@ -226,7 +224,6 @@ export const productRouter = router({
|
|||
featureName: f.featureName,
|
||||
featureValue: f.featureValue,
|
||||
})),
|
||||
comboItems: (sku.comboItems || []).map((ci) => ({ skuId: ci.skuId })),
|
||||
}))
|
||||
|
||||
const newProduct = await createProductInDb({
|
||||
|
|
@ -235,7 +232,6 @@ export const productRouter = router({
|
|||
longDescription,
|
||||
storeId,
|
||||
incrementStep,
|
||||
productType,
|
||||
skus: skuInputs,
|
||||
} as any)
|
||||
|
||||
|
|
@ -269,7 +265,6 @@ export const productRouter = router({
|
|||
longDescription: z.string().optional(),
|
||||
storeId: z.number().min(1, 'Store is required'),
|
||||
incrementStep: z.number().optional().default(1),
|
||||
productType: z.enum(['item', 'combo']).optional(),
|
||||
skus: z.array(z.object({
|
||||
id: z.number().optional(),
|
||||
name: z.string().optional().nullable(),
|
||||
|
|
@ -282,15 +277,12 @@ export const productRouter = router({
|
|||
featureName: z.string().min(1, 'Attribute name is required'),
|
||||
featureValue: z.string().min(1, 'Value is required'),
|
||||
})).min(1, 'At least one feature is required'),
|
||||
comboItems: z.array(z.object({
|
||||
skuId: z.number().int().positive(),
|
||||
})).optional(),
|
||||
})).min(1, 'At least one SKU is required'),
|
||||
deletedImageKeys: z.array(z.string()).optional().default([]),
|
||||
newImageUrls: z.array(z.string()).optional().default([]),
|
||||
}))
|
||||
.mutation(async ({ input }): Promise<{ product: AdminProductWithRelations; message: string }> => {
|
||||
const { id, name, shortDescription, longDescription, storeId, incrementStep, productType, skus, deletedImageKeys, newImageUrls } = input
|
||||
const { id, name, shortDescription, longDescription, storeId, incrementStep, skus, deletedImageKeys, newImageUrls } = input
|
||||
|
||||
if (deletedImageKeys.length > 0) {
|
||||
await deleteImageUtil({ keys: deletedImageKeys })
|
||||
|
|
@ -310,7 +302,6 @@ export const productRouter = router({
|
|||
featureName: f.featureName,
|
||||
featureValue: f.featureValue,
|
||||
})),
|
||||
comboItems: (sku.comboItems || []).map((ci) => ({ skuId: ci.skuId })),
|
||||
}))
|
||||
|
||||
const updatedProduct = await updateProductInDb(id, {
|
||||
|
|
@ -319,7 +310,6 @@ export const productRouter = router({
|
|||
longDescription,
|
||||
storeId,
|
||||
incrementStep,
|
||||
productType,
|
||||
skus: skuInputs,
|
||||
} as any)
|
||||
|
||||
|
|
@ -419,6 +409,31 @@ export const productRouter = router({
|
|||
}
|
||||
}),
|
||||
|
||||
getSlotProductIds: protectedProcedure
|
||||
.input(z.object({
|
||||
slotId: z.string(),
|
||||
}))
|
||||
.query(async ({ input }): Promise<AdminSlotProductIdsResult> => {
|
||||
const { slotId } = input;
|
||||
|
||||
const skuIds = await getSlotProductIdsInDb(slotId)
|
||||
|
||||
/*
|
||||
// Old implementation - direct DB queries:
|
||||
const associations = await db.query.productSlots.findMany({
|
||||
where: eq(productSlots.slotId, parseInt(slotId)),
|
||||
columns: {
|
||||
productId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const productIds = associations.map(assoc => assoc.productId);
|
||||
|
||||
return {
|
||||
skuIds,
|
||||
}
|
||||
}),
|
||||
|
||||
getSlotsProductIds: protectedProcedure
|
||||
.input(z.object({
|
||||
slotIds: z.array(z.number()),
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ export {
|
|||
replaceProductTags,
|
||||
toggleProductOutOfStock,
|
||||
updateSlotProducts,
|
||||
getSlotProductIds,
|
||||
getSlotsProductIds,
|
||||
getAllUnits,
|
||||
getAllProductTags,
|
||||
|
|
|
|||
|
|
@ -229,6 +229,14 @@ export async function updateSlotProducts(slotId: string, productIds: string[]):
|
|||
}
|
||||
}
|
||||
|
||||
export async function getSlotProductIds(slotId: string): Promise<number[]> {
|
||||
const slot = await db.query.deliverySlotInfo.findFirst({
|
||||
where: eq(deliverySlotInfo.id, parseInt(slotId)),
|
||||
})
|
||||
|
||||
return slot?.productIds || []
|
||||
}
|
||||
|
||||
export async function getAllUnits(): Promise<AdminUnit[]> {
|
||||
const allUnits = await db.query.units.findMany({
|
||||
orderBy: units.shortNotation,
|
||||
|
|
|
|||
|
|
@ -232,18 +232,5 @@ WHERE `key` = 'popularItems'
|
|||
-- 9. Clean up helper table.
|
||||
DROP TABLE `__product_to_sku`;
|
||||
|
||||
-- 10. Add product_type column and product_combos table for combo products.
|
||||
ALTER TABLE `product_info` ADD COLUMN `product_type` text DEFAULT 'item';
|
||||
|
||||
CREATE TABLE `product_combos` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`combo_sku_id` integer NOT NULL,
|
||||
`sku_id` integer NOT NULL,
|
||||
FOREIGN KEY (`combo_sku_id`) REFERENCES `product_skus`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`sku_id`) REFERENCES `product_skus`(`id`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX `unique_combo_sku_item` ON `product_combos` (`combo_sku_id`,`sku_id`);
|
||||
|
||||
-- PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys = off;
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ export {
|
|||
replaceProductTags,
|
||||
mergeSkus,
|
||||
updateSlotProducts,
|
||||
getSlotProductIds,
|
||||
getSlotsProductIds,
|
||||
getAllUnits,
|
||||
getAllProductTags,
|
||||
|
|
@ -330,13 +331,11 @@ export {
|
|||
getAllDeliverySlotsForCache,
|
||||
getAllSpecialDealsForCache,
|
||||
getAllProductTagsForCache,
|
||||
getAllProductCombosForCache,
|
||||
type ProductBasicData,
|
||||
type StoreBasicData,
|
||||
type DeliverySlotData,
|
||||
type SpecialDealData,
|
||||
type ProductTagData,
|
||||
type ProductComboCacheData,
|
||||
// Product Tag Store
|
||||
getAllTagsForCache,
|
||||
getAllTagProductMappings,
|
||||
|
|
@ -353,6 +352,7 @@ export {
|
|||
|
||||
// Automated Jobs Helpers
|
||||
export {
|
||||
toggleFlashDeliveryForItems,
|
||||
toggleKeyVal,
|
||||
getAllKeyValStore,
|
||||
} from './src/lib/automated-jobs'
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import {
|
|||
productInfo,
|
||||
productSkus,
|
||||
skuFeatures,
|
||||
productCombos,
|
||||
units,
|
||||
specialDeals,
|
||||
deliverySlotInfo,
|
||||
|
|
@ -25,7 +24,6 @@ import {
|
|||
couponApplicableProducts,
|
||||
} from '../db/schema'
|
||||
import { and, desc, eq, inArray, sql } from 'drizzle-orm'
|
||||
import { runBatched } from '../lib/run-batched'
|
||||
import type { InferInsertModel, InferSelectModel } from 'drizzle-orm'
|
||||
import type {
|
||||
AdminProduct,
|
||||
|
|
@ -84,7 +82,6 @@ const mapProduct = (product: ProductRow): AdminProduct => ({
|
|||
storeId: product.storeId,
|
||||
incrementStep: product.incrementStep,
|
||||
createdAt: product.createdAt,
|
||||
productType: product.productType as 'item' | 'combo',
|
||||
})
|
||||
|
||||
const mapSkuFeature = (feature: SkuFeatureRow): AdminSkuFeature => ({
|
||||
|
|
@ -94,7 +91,7 @@ const mapSkuFeature = (feature: SkuFeatureRow): AdminSkuFeature => ({
|
|||
featureValue: feature.featureValue,
|
||||
})
|
||||
|
||||
const mapSku = (sku: SkuRow, features: SkuFeatureRow[] = [], comboItems: any[] = []): AdminSku => ({
|
||||
const mapSku = (sku: SkuRow, features: SkuFeatureRow[] = []): AdminSku => ({
|
||||
id: sku.id,
|
||||
productId: sku.productId,
|
||||
name: sku.name ?? null,
|
||||
|
|
@ -108,7 +105,6 @@ const mapSku = (sku: SkuRow, features: SkuFeatureRow[] = [], comboItems: any[] =
|
|||
flashPrice: sku.flashPrice ? String(sku.flashPrice) : null,
|
||||
createdAt: sku.createdAt,
|
||||
features: features.map(mapSkuFeature),
|
||||
comboItems: comboItems,
|
||||
})
|
||||
|
||||
const mapSpecialDeal = (deal: SpecialDealRow): AdminSpecialDeal => ({
|
||||
|
|
@ -132,7 +128,7 @@ const mapTagInfo = (tag: ProductTagInfoRow): AdminProductTagInfo => ({
|
|||
export async function getAllProducts(): Promise<AdminProductWithRelations[]> {
|
||||
type ProductWithRelationsRow = ProductRow & {
|
||||
store: StoreRow | null
|
||||
skus: Array<SkuRow & { features: SkuFeatureRow[]; comboItems: any[] }>
|
||||
skus: Array<SkuRow & { features: SkuFeatureRow[] }>
|
||||
}
|
||||
const products = await db.query.productInfo.findMany({
|
||||
orderBy: productInfo.name,
|
||||
|
|
@ -141,11 +137,6 @@ export async function getAllProducts(): Promise<AdminProductWithRelations[]> {
|
|||
skus: {
|
||||
with: {
|
||||
features: true,
|
||||
comboItems: {
|
||||
with: {
|
||||
sku: { with: { product: true, features: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -154,17 +145,7 @@ export async function getAllProducts(): Promise<AdminProductWithRelations[]> {
|
|||
return products.map((product) => ({
|
||||
...mapProduct(product),
|
||||
store: product.store ? mapStore(product.store) : null,
|
||||
skus: product.skus.map((sku) => {
|
||||
const comboItems = (sku.comboItems || []).map((ci: any) => ({
|
||||
skuId: ci.skuId,
|
||||
skuName: ci.sku?.name ?? null,
|
||||
features: (ci.sku?.features || []).map((f: any) => ({ featureName: f.featureName, featureValue: f.featureValue })),
|
||||
productName: ci.sku?.product?.name ?? 'Unknown',
|
||||
images: getStringArray(ci.sku?.images),
|
||||
price: String(ci.sku?.price ?? '0'),
|
||||
}))
|
||||
return mapSku(sku, sku.features, comboItems)
|
||||
}),
|
||||
skus: product.skus.map((sku) => mapSku(sku, sku.features)),
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -176,11 +157,6 @@ export async function getProductById(id: number): Promise<AdminProductWithDetail
|
|||
skus: {
|
||||
with: {
|
||||
features: true,
|
||||
comboItems: {
|
||||
with: {
|
||||
sku: { with: { product: true, features: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -204,24 +180,12 @@ export async function getProductById(id: number): Promise<AdminProductWithDetail
|
|||
with: {
|
||||
tag: true,
|
||||
},
|
||||
}) 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)
|
||||
})
|
||||
}) as Array<ProductTagRow & { tag: ProductTagInfoRow }>
|
||||
|
||||
return {
|
||||
...mapProduct(product),
|
||||
store: product.store ? mapStore(product.store) : null,
|
||||
skus: skusWithCombos,
|
||||
skus: product.skus.map((sku) => mapSku(sku, sku.features)),
|
||||
deals: deals.map(mapSpecialDeal),
|
||||
tags: productTagsData.map((tag) => mapTagInfo(tag.tag)),
|
||||
}
|
||||
|
|
@ -263,7 +227,6 @@ export async function createProduct(input: CreateProductInput): Promise<AdminPro
|
|||
longDescription: productData.longDescription ?? null,
|
||||
storeId: productData.storeId ?? null,
|
||||
incrementStep: productData.incrementStep ?? 1,
|
||||
productType: productData.productType ?? 'item',
|
||||
}).returning()
|
||||
|
||||
const skuRows = await db.insert(productSkus).values(
|
||||
|
|
@ -288,15 +251,6 @@ export async function createProduct(input: CreateProductInput): Promise<AdminPro
|
|||
featureValue: f.featureValue,
|
||||
}))
|
||||
)
|
||||
|
||||
if (sku.comboItems && sku.comboItems.length > 0) {
|
||||
await db.insert(productCombos).values(
|
||||
sku.comboItems.map((ci: any) => ({
|
||||
comboSkuId: skuRow.id,
|
||||
skuId: ci.skuId,
|
||||
}))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const createdSkus = await db.query.productSkus.findMany({
|
||||
|
|
@ -329,7 +283,6 @@ export async function updateProduct(id: number, input: any): Promise<AdminProduc
|
|||
longDescription: productData.longDescription ?? null,
|
||||
storeId: productData.storeId ?? null,
|
||||
incrementStep: productData.incrementStep ?? 1,
|
||||
productType: productData.productType,
|
||||
})
|
||||
.where(eq(productInfo.id, id))
|
||||
|
||||
|
|
@ -373,16 +326,6 @@ export async function updateProduct(id: number, input: any): Promise<AdminProduc
|
|||
featureValue: f.featureValue,
|
||||
}))
|
||||
)
|
||||
|
||||
await db.delete(productCombos).where(eq(productCombos.comboSkuId, sku.id))
|
||||
if (sku.comboItems && sku.comboItems.length > 0) {
|
||||
await db.insert(productCombos).values(
|
||||
sku.comboItems.map((ci: any) => ({
|
||||
comboSkuId: sku.id,
|
||||
skuId: ci.skuId,
|
||||
}))
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Insert new SKU
|
||||
const [newSku] = await db.insert(productSkus).values({
|
||||
|
|
@ -427,6 +370,29 @@ export async function updateProduct(id: number, input: any): Promise<AdminProduc
|
|||
}
|
||||
}
|
||||
|
||||
const product = await db.query.productSkus.findFirst({
|
||||
where: eq(productSkus.id, id),
|
||||
})
|
||||
|
||||
if (!product) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [updatedProduct] = await db
|
||||
.update(productSkus)
|
||||
.set({
|
||||
isOutOfStock: !product.isOutOfStock,
|
||||
})
|
||||
.where(eq(productSkus.id, id))
|
||||
.returning()
|
||||
|
||||
if (!updatedProduct) {
|
||||
return null
|
||||
}
|
||||
|
||||
return mapSku(updatedProduct)
|
||||
}
|
||||
|
||||
export async function updateSlotProducts(slotId: string, productIds: string[]): Promise<AdminUpdateSlotProductsResult> {
|
||||
const slot = await db.query.deliverySlotInfo.findFirst({
|
||||
where: eq(deliverySlotInfo.id, parseInt(slotId)),
|
||||
|
|
@ -453,6 +419,14 @@ export async function updateSlotProducts(slotId: string, productIds: string[]):
|
|||
}
|
||||
}
|
||||
|
||||
export async function getSlotProductIds(slotId: string): Promise<number[]> {
|
||||
const slot = await db.query.deliverySlotInfo.findFirst({
|
||||
where: eq(deliverySlotInfo.id, parseInt(slotId)),
|
||||
})
|
||||
|
||||
return slot?.skuIds || []
|
||||
}
|
||||
|
||||
export async function getAllUnits(): Promise<AdminUnit[]> {
|
||||
const allUnits = await db.query.units.findMany({
|
||||
orderBy: units.shortNotation,
|
||||
|
|
@ -841,39 +815,35 @@ 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 }>
|
||||
|
||||
// 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 existingIds = new Set(existingSkus.map((sku: { id: number }) => sku.id))
|
||||
const invalidIds = productIds.filter((id) => !existingIds.has(id))
|
||||
|
||||
if (invalidIds.length > 0) {
|
||||
return { updatedCount: 0, invalidIds }
|
||||
}
|
||||
|
||||
// Apply updates in chunks inside a single transaction
|
||||
await runBatched(db, updates, 10, async (tx, chunk) => {
|
||||
for (const update of chunk) {
|
||||
const { productId, price, marketPrice, flashPrice, isFlashAvailable } = update
|
||||
const updateData: any = {}
|
||||
const updatePromises = updates.map((update) => {
|
||||
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
|
||||
|
||||
await tx
|
||||
.update(productSkus)
|
||||
.set(updateData)
|
||||
.where(eq(productSkus.id, productId))
|
||||
}
|
||||
return db
|
||||
.update(productSkus)
|
||||
.set(updateData)
|
||||
.where(eq(productSkus.id, productId))
|
||||
})
|
||||
|
||||
await Promise.all(updatePromises)
|
||||
|
||||
return { updatedCount: updates.length, invalidIds: [] }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { db } from '../db/db_index'
|
||||
import { storeInfo, productInfo } from '../db/schema'
|
||||
import { eq, inArray } from 'drizzle-orm'
|
||||
import { runBatched } from '../lib/run-batched'
|
||||
|
||||
export interface Store {
|
||||
id: number
|
||||
|
|
@ -45,36 +44,32 @@ export async function createStore(
|
|||
input: CreateStoreInput,
|
||||
products?: number[]
|
||||
): Promise<Store> {
|
||||
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()
|
||||
const [newStore] = await db
|
||||
.insert(storeInfo)
|
||||
.values({
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
imageUrl: input.imageUrl,
|
||||
owner: input.owner,
|
||||
})
|
||||
.returning()
|
||||
|
||||
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))
|
||||
})
|
||||
}
|
||||
if (products && products.length > 0) {
|
||||
await db
|
||||
.update(productInfo)
|
||||
.set({ storeId: newStore.id })
|
||||
.where(inArray(productInfo.id, products))
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -89,46 +84,42 @@ export async function updateStore(
|
|||
input: UpdateStoreInput,
|
||||
products?: number[]
|
||||
): Promise<Store> {
|
||||
return db.transaction(async (tx) => {
|
||||
const [updatedStore] = await tx
|
||||
.update(storeInfo)
|
||||
.set({
|
||||
...input,
|
||||
// updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(storeInfo.id, id))
|
||||
.returning()
|
||||
const [updatedStore] = await db
|
||||
.update(storeInfo)
|
||||
.set({
|
||||
...input,
|
||||
// updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(storeInfo.id, id))
|
||||
.returning()
|
||||
|
||||
if (!updatedStore) {
|
||||
throw new Error('Store not found')
|
||||
}
|
||||
if (!updatedStore) {
|
||||
throw new Error('Store not found')
|
||||
}
|
||||
|
||||
if (products !== undefined) {
|
||||
await tx
|
||||
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: 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))
|
||||
})
|
||||
}
|
||||
.set({ storeId: id })
|
||||
.where(inArray(productInfo.id, products))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: updatedStore.id,
|
||||
name: updatedStore.name,
|
||||
description: updatedStore.description,
|
||||
imageUrl: updatedStore.imageUrl,
|
||||
owner: updatedStore.owner,
|
||||
createdAt: updatedStore.createdAt,
|
||||
// updatedAt: updatedStore.updatedAt,
|
||||
}
|
||||
})
|
||||
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,13 +65,11 @@ const staffRoleValues = ['super_admin', 'admin', 'marketer', 'delivery_staff'] a
|
|||
const staffPermissionValues = ['crud_product', 'make_coupon', 'crud_staff_users'] as const
|
||||
const uploadStatusValues = ['pending', 'claimed'] as const
|
||||
const paymentStatusValues = ['pending', 'success', 'cod', 'failed'] as const
|
||||
const productTypeValues = ['item', 'combo'] as const
|
||||
|
||||
export const staffRoleEnum = (name: string) => text(name, { enum: staffRoleValues })
|
||||
export const staffPermissionEnum = (name: string) => text(name, { enum: staffPermissionValues })
|
||||
export const uploadStatusEnum = (name: string) => text(name, { enum: uploadStatusValues })
|
||||
export const paymentStatusEnum = (name: string) => text(name, { enum: paymentStatusValues })
|
||||
export const productTypeEnum = (name: string) => text(name, { enum: productTypeValues })
|
||||
|
||||
export const users = sqliteTable('users', {
|
||||
id: integer().primaryKey({ autoIncrement: true }),
|
||||
|
|
@ -194,7 +192,6 @@ export const productInfo = sqliteTable('product_info', {
|
|||
storeId: integer('store_id').references(() => storeInfo.id),
|
||||
incrementStep: real('increment_step').notNull().default(1),
|
||||
createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
|
||||
productType: productTypeEnum('product_type').notNull().default('item'),
|
||||
})
|
||||
|
||||
export const productSkus = sqliteTable('product_skus', {
|
||||
|
|
@ -220,14 +217,6 @@ export const skuFeatures = sqliteTable('sku_features', {
|
|||
unq_sku_feature_name: uniqueIndex('unique_sku_feature_name').on(t.skuId, t.featureName),
|
||||
}))
|
||||
|
||||
export const productCombos = sqliteTable('product_combos', {
|
||||
id: integer().primaryKey({ autoIncrement: true }),
|
||||
comboSkuId: integer('combo_sku_id').notNull().references(() => productSkus.id),
|
||||
skuId: integer('sku_id').notNull().references(() => productSkus.id),
|
||||
}, (t) => ({
|
||||
unq_combo_sku: uniqueIndex('unique_combo_sku_item').on(t.comboSkuId, t.skuId),
|
||||
}))
|
||||
|
||||
export const productGroupInfo = sqliteTable('product_group_info', {
|
||||
id: integer().primaryKey({ autoIncrement: true }),
|
||||
groupName: text('group_name').notNull(),
|
||||
|
|
@ -592,25 +581,12 @@ export const productSkusRelations = relations(productSkus, ({ one, many }) => ({
|
|||
orderItems: many(orderItems),
|
||||
cartItems: many(cartItems),
|
||||
applicableCoupons: many(couponApplicableProducts),
|
||||
comboItems: many(productCombos, { relationName: 'comboSku' }),
|
||||
}))
|
||||
|
||||
export const skuFeaturesRelations = relations(skuFeatures, ({ one }) => ({
|
||||
sku: one(productSkus, { fields: [skuFeatures.skuId], references: [productSkus.id] }),
|
||||
}))
|
||||
|
||||
export const productCombosRelations = relations(productCombos, ({ one }) => ({
|
||||
comboSku: one(productSkus, {
|
||||
fields: [productCombos.comboSkuId],
|
||||
references: [productSkus.id],
|
||||
relationName: 'comboSku',
|
||||
}),
|
||||
sku: one(productSkus, {
|
||||
fields: [productCombos.skuId],
|
||||
references: [productSkus.id],
|
||||
}),
|
||||
}))
|
||||
|
||||
export const productTagInfoRelations = relations(productTagInfo, ({ many }) => ({
|
||||
products: many(productTags),
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -1,8 +1,23 @@
|
|||
import { db } from '../db/db_index'
|
||||
import { keyValStore } from '../db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { productInfo, keyValStore } from '../db/schema'
|
||||
import { inArray, eq } from 'drizzle-orm'
|
||||
import { castConstValue } from '../lib/const-keys'
|
||||
|
||||
/**
|
||||
* Toggle flash delivery availability for specific products
|
||||
* @param isAvailable - Whether flash delivery should be available
|
||||
* @param productIds - Array of product IDs to update
|
||||
*/
|
||||
export async function toggleFlashDeliveryForItems(
|
||||
isAvailable: boolean,
|
||||
productIds: number[]
|
||||
): Promise<void> {
|
||||
await db
|
||||
.update(productInfo)
|
||||
.set({ isFlashAvailable: isAvailable })
|
||||
.where(inArray(productInfo.id, productIds))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update key-value store
|
||||
* @param key - The key to update
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { db } from '../db/db_index'
|
||||
import { orders, orderItems, orderStatus, payments, refunds, couponUsage, complaints } from '../db/schema'
|
||||
import { inArray } from 'drizzle-orm'
|
||||
import { runBatched } from './run-batched'
|
||||
|
||||
/**
|
||||
* Delete orders and all their related records
|
||||
|
|
@ -14,28 +13,26 @@ export async function deleteOrdersWithRelations(orderIds: number[]): Promise<voi
|
|||
return
|
||||
}
|
||||
|
||||
await runBatched(db, orderIds, 10, async (tx, chunk) => {
|
||||
// Delete child records first (in correct order to avoid FK constraint errors)
|
||||
// Delete child records first (in correct order to avoid FK constraint errors)
|
||||
|
||||
// 1. Delete coupon usage records
|
||||
await tx.delete(couponUsage).where(inArray(couponUsage.orderId, chunk))
|
||||
// 1. Delete coupon usage records
|
||||
await db.delete(couponUsage).where(inArray(couponUsage.orderId, orderIds))
|
||||
|
||||
// 2. Delete complaints related to these orders
|
||||
await tx.delete(complaints).where(inArray(complaints.orderId, chunk))
|
||||
// 2. Delete complaints related to these orders
|
||||
await db.delete(complaints).where(inArray(complaints.orderId, orderIds))
|
||||
|
||||
// 3. Delete refunds
|
||||
await tx.delete(refunds).where(inArray(refunds.orderId, chunk))
|
||||
// 3. Delete refunds
|
||||
await db.delete(refunds).where(inArray(refunds.orderId, orderIds))
|
||||
|
||||
// 4. Delete payments
|
||||
await tx.delete(payments).where(inArray(payments.orderId, chunk))
|
||||
// 4. Delete payments
|
||||
await db.delete(payments).where(inArray(payments.orderId, orderIds))
|
||||
|
||||
// 5. Delete order status records
|
||||
await tx.delete(orderStatus).where(inArray(orderStatus.orderId, chunk))
|
||||
// 5. Delete order status records
|
||||
await db.delete(orderStatus).where(inArray(orderStatus.orderId, orderIds))
|
||||
|
||||
// 6. Delete order items
|
||||
await tx.delete(orderItems).where(inArray(orderItems.orderId, chunk))
|
||||
// 6. Delete order items
|
||||
await db.delete(orderItems).where(inArray(orderItems.orderId, orderIds))
|
||||
|
||||
// 7. Finally delete the orders themselves
|
||||
await tx.delete(orders).where(inArray(orders.id, chunk))
|
||||
})
|
||||
// 7. Finally delete the orders themselves
|
||||
await db.delete(orders).where(inArray(orders.id, orderIds))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
}
|
||||
|
|
@ -57,7 +57,6 @@ export interface ProductBasicData {
|
|||
productQuantity: number
|
||||
isFlashAvailable: boolean
|
||||
flashPrice: string | null
|
||||
productType: string
|
||||
}
|
||||
|
||||
export interface StoreBasicData {
|
||||
|
|
@ -131,7 +130,6 @@ export async function getAllProductsForCache(): Promise<ProductBasicData[]> {
|
|||
productQuantity: 1,
|
||||
isFlashAvailable: sku.isFlashAvailable,
|
||||
flashPrice: sku.flashPrice ? String(sku.flashPrice) : null,
|
||||
productType: sku.product?.productType ?? 'item',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -178,41 +176,6 @@ export async function getAllProductTagsForCache(): Promise<ProductTagData[]> {
|
|||
.innerJoin(productTagInfo, eq(productTags.tagId, productTagInfo.id))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PRODUCT COMBO STORE HELPERS
|
||||
// ============================================================================
|
||||
|
||||
export interface ProductComboCacheData {
|
||||
comboSkuId: number
|
||||
skuId: number
|
||||
productName: string
|
||||
skuName: string | null
|
||||
images: unknown
|
||||
unitNotation: string
|
||||
price: string
|
||||
}
|
||||
|
||||
export async function getAllProductCombosForCache(): Promise<ProductComboCacheData[]> {
|
||||
const results = await db.query.productCombos.findMany({
|
||||
with: {
|
||||
sku: { with: { product: true, features: true } },
|
||||
},
|
||||
})
|
||||
|
||||
return results.map((ci) => {
|
||||
const features = ci.sku?.features || []
|
||||
return {
|
||||
comboSkuId: ci.comboSkuId,
|
||||
skuId: ci.skuId,
|
||||
productName: ci.sku?.product?.name ?? 'Unknown',
|
||||
skuName: ci.sku?.name ?? null,
|
||||
images: ci.sku?.images,
|
||||
unitNotation: features.map((f) => f.featureValue).join(' '),
|
||||
price: String(ci.sku?.price ?? '0'),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PRODUCT TAG STORE HELPERS
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ const mapBanner = (banner: BannerRow): UserBanner => ({
|
|||
name: banner.name,
|
||||
imageUrl: banner.imageUrl,
|
||||
description: banner.description ?? null,
|
||||
skuIds: banner.skuIds ?? null,
|
||||
productIds: banner.productIds ?? null,
|
||||
redirectUrl: banner.redirectUrl ?? null,
|
||||
serialNum: banner.serialNum ?? null,
|
||||
isActive: banner.isActive,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import type {
|
|||
UserRecentProduct,
|
||||
} from '@packages/shared'
|
||||
import { coerceDate } from '../lib/date'
|
||||
import { runBatched } from '../lib/run-batched'
|
||||
|
||||
export interface OrderItemInput {
|
||||
productId: number
|
||||
|
|
@ -82,19 +81,16 @@ export interface OrderWithRelations {
|
|||
createdAt: Date
|
||||
orderItems: Array<{
|
||||
id: number
|
||||
skuId: number
|
||||
productId: number
|
||||
quantity: string
|
||||
price: string
|
||||
discountedPrice: string | null
|
||||
is_packaged: boolean
|
||||
sku: {
|
||||
product: {
|
||||
id: number
|
||||
name: string | null
|
||||
name: string
|
||||
images: unknown
|
||||
product: {
|
||||
name: string
|
||||
} | null
|
||||
} | null
|
||||
}
|
||||
}>
|
||||
slot: {
|
||||
deliveryTime: Date
|
||||
|
|
@ -130,19 +126,16 @@ export interface OrderDetailWithRelations {
|
|||
createdAt: Date
|
||||
orderItems: Array<{
|
||||
id: number
|
||||
skuId: number
|
||||
productId: number
|
||||
quantity: string
|
||||
price: string
|
||||
discountedPrice: string | null
|
||||
is_packaged: boolean
|
||||
sku: {
|
||||
product: {
|
||||
id: number
|
||||
name: string | null
|
||||
name: string
|
||||
images: unknown
|
||||
product: {
|
||||
name: string
|
||||
} | null
|
||||
} | null
|
||||
}
|
||||
}>
|
||||
slot: {
|
||||
deliveryTime: Date
|
||||
|
|
@ -393,7 +386,6 @@ export async function getOrdersWithRelations(
|
|||
},
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
images: true,
|
||||
},
|
||||
},
|
||||
|
|
@ -476,7 +468,6 @@ export async function getOrderByIdWithRelations(
|
|||
},
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
images: true,
|
||||
},
|
||||
},
|
||||
|
|
@ -635,16 +626,12 @@ export async function getRecentlyDeliveredOrderIds(
|
|||
export async function getSkuIdsFromOrders(
|
||||
orderIds: number[]
|
||||
): Promise<number[]> {
|
||||
if (orderIds.length === 0) return []
|
||||
const orderItemsResult = await db
|
||||
.select({ skuId: orderItems.skuId })
|
||||
.from(orderItems)
|
||||
.where(inArray(orderItems.orderId, orderIds))
|
||||
|
||||
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))]
|
||||
return [...new Set(orderItemsResult.map((item) => item.skuId))]
|
||||
}
|
||||
|
||||
export interface RecentProductData {
|
||||
|
|
@ -662,26 +649,19 @@ export async function getProductsForRecentOrders(
|
|||
productIds: number[],
|
||||
limit: number
|
||||
): Promise<RecentProductData[]> {
|
||||
if (productIds.length === 0) return []
|
||||
|
||||
const skuChunks = await runBatched(db, productIds, 10, async (tx, chunk) => {
|
||||
return tx.query.productSkus.findMany({
|
||||
where: and(
|
||||
inArray(productSkus.id, chunk),
|
||||
eq(productSkus.isSuspended, false)
|
||||
),
|
||||
with: {
|
||||
product: true,
|
||||
features: true,
|
||||
},
|
||||
})
|
||||
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,
|
||||
})
|
||||
|
||||
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 {
|
||||
|
|
@ -731,49 +711,48 @@ export interface OrderWithFullData {
|
|||
export async function getOrdersByIdsWithFullData(
|
||||
orderIds: number[]
|
||||
): Promise<OrderWithFullData[]> {
|
||||
if (orderIds.length === 0) return []
|
||||
console.log('getting orders byid')
|
||||
|
||||
const orderChunks = await runBatched(db, orderIds, 10, async (tx, chunk) => {
|
||||
return tx.query.orders.findMany({
|
||||
where: inArray(orders.id, chunk),
|
||||
with: {
|
||||
address: {
|
||||
columns: {
|
||||
name: true,
|
||||
addressLine1: true,
|
||||
addressLine2: true,
|
||||
city: true,
|
||||
state: true,
|
||||
pincode: true,
|
||||
phone: true,
|
||||
},
|
||||
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,
|
||||
},
|
||||
orderItems: {
|
||||
with: {
|
||||
sku: {
|
||||
columns: {
|
||||
name: true,
|
||||
price: true,
|
||||
},
|
||||
with: {
|
||||
product: {
|
||||
columns: { name: true },
|
||||
},
|
||||
features: true,
|
||||
},
|
||||
orderItems: {
|
||||
with: {
|
||||
sku: {
|
||||
columns: {
|
||||
name: true,
|
||||
price: true,
|
||||
},
|
||||
with: {
|
||||
product: {
|
||||
columns: { name: true },
|
||||
},
|
||||
features: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
slot: {
|
||||
columns: {
|
||||
deliveryTime: true,
|
||||
},
|
||||
},
|
||||
slot: {
|
||||
columns: {
|
||||
deliveryTime: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
// as Promise<OrderWithFullData[]>
|
||||
|
||||
return orderChunks.flat() as OrderWithFullData[]
|
||||
return ordersResp as OrderWithFullData[];
|
||||
}
|
||||
|
||||
export interface OrderWithCancellationData extends OrderWithFullData {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { db } from '../db/db_index'
|
||||
import { deliverySlotInfo, productInfo, productCombos, productSkus, productReviews, productTags, specialDeals, storeInfo, users } from '../db/schema'
|
||||
import { deliverySlotInfo, productInfo, productSkus, productReviews, productTags, specialDeals, storeInfo, users } from '../db/schema'
|
||||
import { and, desc, eq, gt, sql } from 'drizzle-orm'
|
||||
import type { UserProductDetailData, UserProductReview } from '@packages/shared'
|
||||
|
||||
|
|
@ -44,25 +44,6 @@ export async function getProductDetailById(skuId: number): Promise<UserProductDe
|
|||
)
|
||||
.orderBy(specialDeals.quantity)
|
||||
|
||||
const comboItemsData = await db.query.productCombos.findMany({
|
||||
where: eq(productCombos.comboSkuId, skuId),
|
||||
with: {
|
||||
sku: { with: { product: true, features: true } },
|
||||
},
|
||||
})
|
||||
|
||||
const comboItems = comboItemsData.map((ci) => {
|
||||
const ciFeatures = ci.sku?.features || []
|
||||
return {
|
||||
skuId: ci.skuId,
|
||||
skuName: ci.sku?.name ?? null,
|
||||
unitNotation: ciFeatures.map((f) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
|
||||
productName: ci.sku?.product?.name ?? 'Unknown',
|
||||
images: getStringArray(ci.sku?.images),
|
||||
price: String(ci.sku?.price ?? '0'),
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
id: sku.id,
|
||||
name: product?.name ?? 'Unknown',
|
||||
|
|
@ -88,8 +69,6 @@ export async function getProductDetailById(skuId: number): Promise<UserProductDe
|
|||
price: String(deal.price ?? '0'),
|
||||
validTill: deal.validTill,
|
||||
})),
|
||||
productType: product?.productType ?? 'item',
|
||||
comboItems,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,8 +151,6 @@ export async function createProductReview(
|
|||
export interface ProductSummaryData {
|
||||
id: number
|
||||
name: string
|
||||
skuId: number
|
||||
skuName: string | null
|
||||
shortDescription: string | null
|
||||
price: string
|
||||
marketPrice: string | null
|
||||
|
|
@ -181,50 +158,48 @@ export interface ProductSummaryData {
|
|||
isOutOfStock: boolean
|
||||
unitShortNotation: string
|
||||
productQuantity: number
|
||||
features: { featureName: string; featureValue: string }[]
|
||||
}
|
||||
|
||||
export async function getAllProductsWithUnits(tagId?: number): Promise<ProductSummaryData[]> {
|
||||
const taggedProductIdSet = new Set<number>()
|
||||
let productIds: number[] | null = null
|
||||
|
||||
// If tagId is provided, get product IDs that have this tag
|
||||
// If tagId is provided, get products that have this tag
|
||||
if (tagId) {
|
||||
const taggedProducts = await db
|
||||
.select({ productId: productTags.productId })
|
||||
.from(productTags)
|
||||
.where(eq(productTags.tagId, tagId))
|
||||
|
||||
for (const tp of taggedProducts) {
|
||||
taggedProductIdSet.add(tp.productId)
|
||||
}
|
||||
productIds = taggedProducts.map(tp => tp.productId)
|
||||
}
|
||||
|
||||
let whereCondition = undefined
|
||||
|
||||
// Filter by product IDs if tag filtering is applied
|
||||
if (productIds && productIds.length > 0) {
|
||||
whereCondition = inArray(productSkus.productId, productIds)
|
||||
}
|
||||
|
||||
const skus = await db.query.productSkus.findMany({
|
||||
where: whereCondition,
|
||||
with: {
|
||||
product: true,
|
||||
features: true,
|
||||
},
|
||||
})
|
||||
|
||||
return skus
|
||||
.filter((sku) => {
|
||||
if (!tagId) return true
|
||||
return taggedProductIdSet.has(sku.productId)
|
||||
})
|
||||
.map((sku) => {
|
||||
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.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, asc, eq, inArray } from 'drizzle-orm'
|
||||
import { and, eq, inArray, sql } from 'drizzle-orm'
|
||||
import type { InferSelectModel } from 'drizzle-orm'
|
||||
import type { UserStoreDetailData, UserStoreProductData, UserStoreSummaryData, StoreSummary } from '@packages/shared'
|
||||
|
||||
|
|
@ -12,44 +12,72 @@ const getStringArray = (value: unknown): string[] | null => {
|
|||
}
|
||||
|
||||
export async function getStoreSummaries(): Promise<UserStoreSummaryData[]> {
|
||||
const storesData = await db.select({
|
||||
id: storeInfo.id,
|
||||
name: storeInfo.name,
|
||||
description: storeInfo.description,
|
||||
imageUrl: storeInfo.imageUrl,
|
||||
}).from(storeInfo)
|
||||
// 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 skus = await db.query.productSkus.findMany({
|
||||
where: eq(productSkus.isSuspended, false),
|
||||
with: { product: true },
|
||||
orderBy: asc(productSkus.id),
|
||||
})
|
||||
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 skusByStore = new Map<number, typeof skus>()
|
||||
for (const sku of skus) {
|
||||
const storeId = sku.product?.storeId
|
||||
if (storeId == null) continue
|
||||
if (!skusByStore.has(storeId)) skusByStore.set(storeId, [])
|
||||
skusByStore.get(storeId)!.push(sku)
|
||||
}
|
||||
const productIdArr = storeProductIds.map((p) => p.id)
|
||||
if (productIdArr.length > 0) {
|
||||
const skus = await db.query.productSkus.findMany({
|
||||
where: and(
|
||||
inArray(productSkus.productId, productIdArr),
|
||||
eq(productSkus.isSuspended, false)
|
||||
),
|
||||
with: {
|
||||
product: { columns: { name: true } },
|
||||
},
|
||||
columns: { id: true, images: true, name: true },
|
||||
limit: 3,
|
||||
})
|
||||
sampleProducts = skus.map((sku) => ({
|
||||
id: sku.id,
|
||||
name: sku.product?.name ?? sku.name ?? 'Unknown',
|
||||
images: getStringArray(sku.images),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
return 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 {
|
||||
id: store.id,
|
||||
name: store.name,
|
||||
description: store.description ?? null,
|
||||
imageUrl: store.imageUrl ?? null,
|
||||
productCount: store.productCount || 0,
|
||||
sampleProducts,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
id: store.id,
|
||||
name: store.name,
|
||||
description: store.description ?? null,
|
||||
imageUrl: store.imageUrl ?? null,
|
||||
productCount: storeSkus.length,
|
||||
sampleProducts,
|
||||
}
|
||||
})
|
||||
return storesWithDetails
|
||||
}
|
||||
|
||||
export async function getStoreDetail(storeId: number): Promise<UserStoreDetailData | null> {
|
||||
|
|
|
|||
|
|
@ -360,20 +360,13 @@ export interface AdminUnit {
|
|||
fullName: string;
|
||||
}
|
||||
|
||||
export interface AdminSkuFeature {
|
||||
id: number;
|
||||
skuId: number;
|
||||
featureName: string;
|
||||
featureValue: string;
|
||||
}
|
||||
|
||||
export interface AdminProductComboItem {
|
||||
skuId: number;
|
||||
skuName: string | null;
|
||||
features: AdminSkuFeature[];
|
||||
productName: string;
|
||||
images: string[] | null;
|
||||
price: string;
|
||||
export interface AdminSkuVariant {
|
||||
id: number
|
||||
skuId: number
|
||||
name: string
|
||||
value: string
|
||||
unitId: number | null
|
||||
sortOrder: number
|
||||
}
|
||||
|
||||
export interface AdminSku {
|
||||
|
|
@ -390,7 +383,7 @@ export interface AdminSku {
|
|||
flashPrice: string | null
|
||||
createdAt: Date
|
||||
features: AdminSkuFeature[]
|
||||
comboItems: AdminProductComboItem[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface AdminProduct {
|
||||
|
|
@ -401,7 +394,6 @@ export interface AdminProduct {
|
|||
storeId: number | null;
|
||||
incrementStep: number;
|
||||
createdAt: Date;
|
||||
productType: 'item' | 'combo';
|
||||
}
|
||||
|
||||
export interface AdminProductWithRelations extends AdminProduct {
|
||||
|
|
@ -499,6 +491,10 @@ export interface AdminUpdateSlotProductsResult {
|
|||
removed: number;
|
||||
}
|
||||
|
||||
export interface AdminSlotProductIdsResult {
|
||||
skuIds: number[];
|
||||
}
|
||||
|
||||
export type AdminSlotsProductIdsResult = Record<number, number[]>;
|
||||
|
||||
export interface AdminProductReview {
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ export interface UserCartProduct {
|
|||
|
||||
export interface UserCartItem {
|
||||
id: number;
|
||||
skuId: number;
|
||||
productId: number;
|
||||
quantity: number;
|
||||
addedAt: Date;
|
||||
product: UserCartProduct;
|
||||
|
|
@ -286,15 +286,6 @@ export interface UserProductSpecialDeal {
|
|||
validTill: Date;
|
||||
}
|
||||
|
||||
export interface UserProductComboItem {
|
||||
skuId: number;
|
||||
skuName: string | null;
|
||||
unitNotation: string;
|
||||
productName: string;
|
||||
images: string[] | null;
|
||||
price: string;
|
||||
}
|
||||
|
||||
export interface UserProductDetailData {
|
||||
id: number;
|
||||
name: string;
|
||||
|
|
@ -312,8 +303,6 @@ export interface UserProductDetailData {
|
|||
flashPrice: string | null;
|
||||
deliverySlots: UserProductDeliverySlot[];
|
||||
specialDeals: UserProductSpecialDeal[];
|
||||
productType: string;
|
||||
comboItems: UserProductComboItem[];
|
||||
}
|
||||
|
||||
export interface UserProductDetail extends UserProductDetailData {
|
||||
|
|
@ -496,7 +485,7 @@ export interface UserCouponApplicableUser {
|
|||
export interface UserCouponApplicableProduct {
|
||||
id: number;
|
||||
couponId: number;
|
||||
skuId: number;
|
||||
productId: number;
|
||||
}
|
||||
|
||||
export interface UserCoupon {
|
||||
|
|
@ -506,7 +495,7 @@ export interface UserCoupon {
|
|||
discountPercent: string | null;
|
||||
flatDiscount: string | null;
|
||||
minOrder: string | null;
|
||||
skuIds: unknown;
|
||||
productIds: unknown;
|
||||
maxValue: string | null;
|
||||
isApplyForAll: boolean;
|
||||
validTill: Date | null;
|
||||
|
|
|
|||
|
|
@ -210,6 +210,25 @@ export interface token_user {
|
|||
gender: string;
|
||||
}
|
||||
|
||||
export interface ProductSummary {
|
||||
id: number;
|
||||
name: string;
|
||||
shortDescription?: string;
|
||||
price: number;
|
||||
unit: string;
|
||||
isOutOfStock: boolean;
|
||||
nextDeliveryDate: string | null;
|
||||
images: string[];
|
||||
}
|
||||
|
||||
export interface GetSlotsProductIdsPayload {
|
||||
slotIds: number[];
|
||||
}
|
||||
|
||||
export interface GetSlotsProductIdsResponse {
|
||||
[slotId: number]: number[];
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: string;
|
||||
orderId: string;
|
||||
|
|
|
|||
40
packages/ui/src/common-api-hooks/product.api.tsx
Normal file
40
packages/ui/src/common-api-hooks/product.api.tsx
Normal 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,
|
||||
});
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue