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