enh
This commit is contained in:
parent
6d67a1e759
commit
b5cdc53e50
17 changed files with 400 additions and 123 deletions
|
|
@ -14,6 +14,28 @@ export default function AddProduct() {
|
||||||
|
|
||||||
const handleSubmit = async (values: any, variantImages: ImageUploaderNeoPayload[][], _deletedImageKeys?: string[]) => {
|
const handleSubmit = async (values: any, variantImages: ImageUploaderNeoPayload[][], _deletedImageKeys?: string[]) => {
|
||||||
try {
|
try {
|
||||||
|
for (const variant of values.variants) {
|
||||||
|
const price = parseFloat(variant.price)
|
||||||
|
if (isNaN(price) || price <= 0) {
|
||||||
|
Alert.alert('Error', 'Please enter a valid price for every variant')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const seenSignatures = new Set<string>()
|
||||||
|
for (const variant of values.variants) {
|
||||||
|
const attributes = variant.attributes || []
|
||||||
|
const signature = attributes
|
||||||
|
.map((a: any) => `${(a.featureName ?? '').trim().toLowerCase()}::${a.featureValue.trim().toLowerCase()}`)
|
||||||
|
.sort()
|
||||||
|
.join('|')
|
||||||
|
if (seenSignatures.has(signature)) {
|
||||||
|
Alert.alert('Error', 'Two variants have the same attributes')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seenSignatures.add(signature)
|
||||||
|
}
|
||||||
|
|
||||||
const allBlobs: { blob: Blob; mimeType: string }[] = []
|
const allBlobs: { blob: Blob; mimeType: string }[] = []
|
||||||
const imageCounts: number[] = variantImages.map((imgs) => imgs.length)
|
const imageCounts: number[] = variantImages.map((imgs) => imgs.length)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,28 @@ export default function EditProduct() {
|
||||||
|
|
||||||
const handleSubmit = async (values: any, variantImages: ImageUploaderNeoPayload[][], deletedImageKeys: string[]) => {
|
const handleSubmit = async (values: any, variantImages: ImageUploaderNeoPayload[][], deletedImageKeys: string[]) => {
|
||||||
try {
|
try {
|
||||||
|
for (const variant of values.variants) {
|
||||||
|
const price = parseFloat(variant.price)
|
||||||
|
if (isNaN(price) || price <= 0) {
|
||||||
|
Alert.alert('Error', 'Please enter a valid price for every variant')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const seenSignatures = new Set<string>()
|
||||||
|
for (const variant of values.variants) {
|
||||||
|
const attributes = variant.attributes || []
|
||||||
|
const signature = attributes
|
||||||
|
.map((a: any) => `${(a.featureName ?? '').trim().toLowerCase()}::${a.featureValue.trim().toLowerCase()}`)
|
||||||
|
.sort()
|
||||||
|
.join('|')
|
||||||
|
if (seenSignatures.has(signature)) {
|
||||||
|
Alert.alert('Error', 'Two variants have the same attributes')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seenSignatures.add(signature)
|
||||||
|
}
|
||||||
|
|
||||||
const allBlobs: { blob: Blob; mimeType: string }[] = []
|
const allBlobs: { blob: Blob; mimeType: string }[] = []
|
||||||
const imageCounts: number[] = variantImages.map((imgs) =>
|
const imageCounts: number[] = variantImages.map((imgs) =>
|
||||||
imgs.filter((img) => img.mimeType !== null).length
|
imgs.filter((img) => img.mimeType !== null).length
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useState, useImperativeHandle, forwardRef } from 'react'
|
import React, { useState, useImperativeHandle, forwardRef } from 'react'
|
||||||
import { View, TouchableOpacity, ScrollView } from 'react-native'
|
import { View, TouchableOpacity, ScrollView, Alert } from 'react-native'
|
||||||
import { Formik, FieldArray } from 'formik'
|
import { Formik, FieldArray } from 'formik'
|
||||||
|
import * as Yup from 'yup'
|
||||||
import { MyTextInput, BottomDropdown, MyText, tw, ImageUploaderNeo, ImageUploaderNeoItem, ImageUploaderNeoPayload, Checkbox } from 'common-ui'
|
import { MyTextInput, BottomDropdown, MyText, tw, ImageUploaderNeo, ImageUploaderNeoItem, ImageUploaderNeoPayload, Checkbox } from 'common-ui'
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons'
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons'
|
||||||
import { trpc } from '../trpc-client'
|
import { trpc } from '../trpc-client'
|
||||||
|
|
@ -60,6 +61,60 @@ const defaultVariant = (): Variant => ({
|
||||||
comboItems: [],
|
comboItems: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const variantSignature = (attributes: Attribute[]): string =>
|
||||||
|
attributes
|
||||||
|
.map((a) => `${(a.featureName ?? '').trim().toLowerCase()}::${a.featureValue.trim().toLowerCase()}`)
|
||||||
|
.sort()
|
||||||
|
.join('|')
|
||||||
|
|
||||||
|
const productValidationSchema = Yup.object().shape({
|
||||||
|
name: Yup.string().required('Product name is required'),
|
||||||
|
storeId: Yup.number().required('Store is required').min(1, 'Store is required'),
|
||||||
|
productType: Yup.string().oneOf(['item', 'combo'], 'Product type is required').required('Product type is required'),
|
||||||
|
variants: Yup.array()
|
||||||
|
.min(1, 'At least one variant is required')
|
||||||
|
.of(
|
||||||
|
Yup.object().shape({
|
||||||
|
price: Yup.number()
|
||||||
|
.typeError('Price must be a number')
|
||||||
|
.positive('Price must be a positive number')
|
||||||
|
.required('Price is required'),
|
||||||
|
marketPrice: Yup.number()
|
||||||
|
.typeError('Market price must be a number')
|
||||||
|
.min(0, 'Market price cannot be negative')
|
||||||
|
.nullable()
|
||||||
|
.transform((value, originalValue) => (originalValue === '' ? null : value))
|
||||||
|
.optional(),
|
||||||
|
flashPrice: Yup.number()
|
||||||
|
.typeError('Flash price must be a number')
|
||||||
|
.min(0, 'Flash price cannot be negative')
|
||||||
|
.nullable()
|
||||||
|
.transform((value, originalValue) => (originalValue === '' ? null : value))
|
||||||
|
.optional(),
|
||||||
|
attributes: Yup.array()
|
||||||
|
.min(1, 'At least one attribute is required')
|
||||||
|
.of(
|
||||||
|
Yup.object().shape({
|
||||||
|
featureValue: Yup.string().required('Value is required'),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.test('unique-variants', 'Two variants have the same attributes', function (variants) {
|
||||||
|
if (!Array.isArray(variants)) return true
|
||||||
|
const seen = new Set<string>()
|
||||||
|
for (const variant of variants) {
|
||||||
|
const attrs = variant?.attributes || []
|
||||||
|
const signature = variantSignature(attrs as Attribute[])
|
||||||
|
if (seen.has(signature)) {
|
||||||
|
return this.createError({ message: 'Two variants have the same attributes' })
|
||||||
|
}
|
||||||
|
seen.add(signature)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
mode,
|
mode,
|
||||||
initialValues,
|
initialValues,
|
||||||
|
|
@ -93,6 +148,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
|
validationSchema={productValidationSchema}
|
||||||
onSubmit={(values) => {
|
onSubmit={(values) => {
|
||||||
const images = variantImages.map((imgs) =>
|
const images = variantImages.map((imgs) =>
|
||||||
imgs.map((img) => ({ url: img.imgUrl, mimeType: img.mimeType }))
|
imgs.map((img) => ({ url: img.imgUrl, mimeType: img.mimeType }))
|
||||||
|
|
@ -113,7 +169,8 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
}}
|
}}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
>
|
>
|
||||||
{({ handleChange, handleSubmit, values, setFieldValue }) => (
|
{({ handleChange, handleSubmit, values, setFieldValue, validateForm }) => {
|
||||||
|
return (
|
||||||
<ScrollView style={tw`flex-1`} contentContainerStyle={tw`pb-10`}>
|
<ScrollView style={tw`flex-1`} contentContainerStyle={tw`pb-10`}>
|
||||||
<MyTextInput
|
<MyTextInput
|
||||||
topLabel="Product Name"
|
topLabel="Product Name"
|
||||||
|
|
@ -363,7 +420,25 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
</FieldArray>
|
</FieldArray>
|
||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => handleSubmit()}
|
onPress={async () => {
|
||||||
|
const validationErrors = await validateForm()
|
||||||
|
if (Object.keys(validationErrors).length > 0) {
|
||||||
|
const variantsError = validationErrors.variants
|
||||||
|
const firstVariantErrors = Array.isArray(variantsError)
|
||||||
|
? (variantsError[0] as Record<string, unknown> | undefined) || {}
|
||||||
|
: {}
|
||||||
|
const message = (firstVariantErrors.price as string | undefined)
|
||||||
|
|| (firstVariantErrors.marketPrice as string | undefined)
|
||||||
|
|| (firstVariantErrors.flashPrice as string | undefined)
|
||||||
|
|| (firstVariantErrors.attributes as string | undefined)
|
||||||
|
|| validationErrors.name
|
||||||
|
|| validationErrors.storeId
|
||||||
|
|| variantsError
|
||||||
|
Alert.alert('Check your form', String(message || 'Please fix the highlighted fields'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleSubmit()
|
||||||
|
}}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
style={tw`px-4 py-3 rounded-lg shadow-lg items-center mt-4 ${isLoading ? 'bg-gray-400' : 'bg-blue-500'}`}
|
style={tw`px-4 py-3 rounded-lg shadow-lg items-center mt-4 ${isLoading ? 'bg-gray-400' : 'bg-blue-500'}`}
|
||||||
>
|
>
|
||||||
|
|
@ -372,7 +447,8 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
</MyText>
|
</MyText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
)}
|
)
|
||||||
|
}}
|
||||||
</Formik>
|
</Formik>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import {
|
import {
|
||||||
getOrdersByIdsWithFullData,
|
getOrdersByIdsWithFullData,
|
||||||
getOrderByIdWithFullData,
|
getOrderByIdWithFullData,
|
||||||
|
composeSkuName,
|
||||||
|
composeUnitNotation,
|
||||||
} from '@/src/dbService'
|
} from '@/src/dbService'
|
||||||
import { sendTelegramMessage } from '@/src/lib/telegram-service'
|
import { sendTelegramMessage } from '@/src/lib/telegram-service'
|
||||||
import { queueDataPusher } from '@/src/lib/queue-data-pusher'
|
import { queueDataPusher } from '@/src/lib/queue-data-pusher'
|
||||||
|
|
@ -49,10 +51,11 @@ const formatOrderMessageWithFullData = (ordersData: any[]): string => {
|
||||||
message += '📦 <b>Items:</b>\n';
|
message += '📦 <b>Items:</b>\n';
|
||||||
order.orderItems?.forEach((item: any) => {
|
order.orderItems?.forEach((item: any) => {
|
||||||
const sku = item.sku
|
const sku = item.sku
|
||||||
const features = (sku?.features || []).map((f: any) => f.featureValue).join(' ')
|
const features = sku?.features || []
|
||||||
message += ` • ${sku?.product?.name || 'Unknown'} ${features} x${item.quantity}\n`;
|
const name = composeSkuName(sku?.product?.name || 'Unknown', features)
|
||||||
|
const unit = composeUnitNotation(features)
|
||||||
|
message += ` • ${name} ${unit} x${item.quantity}\n`;
|
||||||
});
|
});
|
||||||
|
|
||||||
message += `\n💰 <b>Total:</b> ₹${order.totalAmount}\n`;
|
message += `\n💰 <b>Total:</b> ₹${order.totalAmount}\n`;
|
||||||
|
|
||||||
message += `🚚 <b>Delivery:</b> ${
|
message += `🚚 <b>Delivery:</b> ${
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ import {
|
||||||
getVendorOrders as getVendorOrdersInDb,
|
getVendorOrders as getVendorOrdersInDb,
|
||||||
updateVendorOrderItemPackaging as updateVendorOrderItemPackagingInDb,
|
updateVendorOrderItemPackaging as updateVendorOrderItemPackagingInDb,
|
||||||
getSlotsAfterDate as getSlotsAfterDateInDb,
|
getSlotsAfterDate as getSlotsAfterDateInDb,
|
||||||
|
composeSkuName,
|
||||||
|
composeUnitNotation,
|
||||||
} from '@/src/dbService'
|
} from '@/src/dbService'
|
||||||
import type {
|
import type {
|
||||||
AdminVendorSnippet,
|
AdminVendorSnippet,
|
||||||
|
|
@ -417,11 +419,11 @@ export const vendorSnippetsRouter = router({
|
||||||
const products = attachedOrderItems.map(item => ({
|
const products = attachedOrderItems.map(item => ({
|
||||||
orderItemId: item.id,
|
orderItemId: item.id,
|
||||||
productId: item.skuId,
|
productId: item.skuId,
|
||||||
productName: item.sku?.product?.name || 'Unknown',
|
productName: composeSkuName(item.sku?.product?.name || 'Unknown', item.sku?.features || []),
|
||||||
quantity: parseFloat(item.quantity),
|
quantity: parseFloat(item.quantity),
|
||||||
productSize: 1,
|
productSize: 1,
|
||||||
price: parseFloat((item.price ?? 0).toString()),
|
price: parseFloat((item.price ?? 0).toString()),
|
||||||
unit: (item.sku?.features || []).map((f: any) => f.featureValue).join(' ') || 'unit',
|
unit: composeUnitNotation(item.sku?.features || []) || 'unit',
|
||||||
subtotal: parseFloat((item.price ?? 0).toString()) * parseFloat(item.quantity),
|
subtotal: parseFloat((item.price ?? 0).toString()) * parseFloat(item.quantity),
|
||||||
is_packaged: item.is_packaged,
|
is_packaged: item.is_packaged,
|
||||||
is_package_verified: item.is_package_verified,
|
is_package_verified: item.is_package_verified,
|
||||||
|
|
@ -488,9 +490,9 @@ export const vendorSnippetsRouter = router({
|
||||||
orderDate: order.createdAt ? order.createdAt.toISOString() : new Date(0).toISOString(),
|
orderDate: order.createdAt ? order.createdAt.toISOString() : new Date(0).toISOString(),
|
||||||
totalQuantity: order.orderItems.reduce((sum, item) => sum + parseFloat(item.quantity || '0'), 0),
|
totalQuantity: order.orderItems.reduce((sum, item) => sum + parseFloat(item.quantity || '0'), 0),
|
||||||
products: order.orderItems.map(item => ({
|
products: order.orderItems.map(item => ({
|
||||||
name: item.sku?.product?.name || 'Unknown',
|
name: composeSkuName(item.sku?.product?.name || 'Unknown', item.sku?.features || []),
|
||||||
quantity: parseFloat(item.quantity || '0'),
|
quantity: parseFloat(item.quantity || '0'),
|
||||||
unit: (item.sku?.features || []).map((f: any) => f.featureValue).join(' ') || 'unit',
|
unit: composeUnitNotation(item.sku?.features || []) || 'unit',
|
||||||
})),
|
})),
|
||||||
}))
|
}))
|
||||||
}),
|
}),
|
||||||
|
|
@ -602,10 +604,10 @@ export const vendorSnippetsRouter = router({
|
||||||
const products = attachedOrderItems.map(item => ({
|
const products = attachedOrderItems.map(item => ({
|
||||||
orderItemId: item.id,
|
orderItemId: item.id,
|
||||||
productId: item.skuId,
|
productId: item.skuId,
|
||||||
productName: item.sku?.product?.name || 'Unknown',
|
productName: composeSkuName(item.sku?.product?.name || 'Unknown', item.sku?.features || []),
|
||||||
quantity: parseFloat(item.quantity),
|
quantity: parseFloat(item.quantity),
|
||||||
price: parseFloat((item.price ?? 0).toString()),
|
price: parseFloat((item.price ?? 0).toString()),
|
||||||
unit: (item.sku?.features || []).map((f: any) => f.featureValue).join(' ') || 'unit',
|
unit: composeUnitNotation(item.sku?.features || []) || 'unit',
|
||||||
subtotal: parseFloat((item.price ?? 0).toString()) * parseFloat(item.quantity),
|
subtotal: parseFloat((item.price ?? 0).toString()) * parseFloat(item.quantity),
|
||||||
productSize: 1,
|
productSize: 1,
|
||||||
is_packaged: item.is_packaged,
|
is_packaged: item.is_packaged,
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,8 @@ export async function scaffoldProducts() {
|
||||||
isFlashAvailable: product.isFlashAvailable,
|
isFlashAvailable: product.isFlashAvailable,
|
||||||
nextDeliveryDate: nextDeliveryDate ? nextDeliveryDate.toISOString() : null,
|
nextDeliveryDate: nextDeliveryDate ? nextDeliveryDate.toISOString() : null,
|
||||||
images: product.images,
|
images: product.images,
|
||||||
flashPrice: product.flashPrice
|
flashPrice: product.flashPrice,
|
||||||
|
productType: product.productType || 'item'
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
getUserProductReviews as getUserProductReviewsInDb,
|
getUserProductReviews as getUserProductReviewsInDb,
|
||||||
getUserProductByIdBasic as getUserProductByIdBasicInDb,
|
getUserProductByIdBasic as getUserProductByIdBasicInDb,
|
||||||
createUserProductReview as createUserProductReviewInDb,
|
createUserProductReview as createUserProductReviewInDb,
|
||||||
|
getOffersAndCombos as getOffersAndCombosInDb,
|
||||||
} from '@/src/dbService'
|
} from '@/src/dbService'
|
||||||
import type {
|
import type {
|
||||||
UserProductDetail,
|
UserProductDetail,
|
||||||
|
|
@ -181,4 +182,21 @@ export const productRouter = router({
|
||||||
return transformedProducts
|
return transformedProducts
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
getOffersPage: publicProcedure
|
||||||
|
.query(async () => {
|
||||||
|
const data = await getOffersAndCombosInDb();
|
||||||
|
|
||||||
|
const signImages = (products: typeof data.combos) =>
|
||||||
|
products.map((product) => ({
|
||||||
|
...product,
|
||||||
|
images: scaffoldAssetUrl((product.images as string[]) || []),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
combos: signImages(data.combos),
|
||||||
|
offers: signImages(data.offers),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,17 +17,81 @@ import FloatingCartBar from "@/components/floating-cart-bar";
|
||||||
import TabLayoutWrapper from "@/components/TabLayoutWrapper";
|
import TabLayoutWrapper from "@/components/TabLayoutWrapper";
|
||||||
|
|
||||||
const { width: screenWidth } = Dimensions.get("window");
|
const { width: screenWidth } = Dimensions.get("window");
|
||||||
const itemWidth = (screenWidth - 48) / 2;
|
const itemWidth = screenWidth * 0.45;
|
||||||
|
|
||||||
export default function OrderAgain() {
|
const rowListContent = { paddingBottom: 16 };
|
||||||
|
|
||||||
|
interface OffersRowProps {
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
products: any[];
|
||||||
|
onProductPress: (id: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OffersRow = ({ title, subtitle, products, onProductPress }: OffersRowProps) => {
|
||||||
|
const renderItem = ({ item }: { item: any }) => (
|
||||||
|
<View style={tw`mr-4`}>
|
||||||
|
<ProductCard
|
||||||
|
item={item}
|
||||||
|
itemWidth={itemWidth}
|
||||||
|
onPress={() => onProductPress(item.id)}
|
||||||
|
showDeliveryInfo={false}
|
||||||
|
useAddToCartDialog={true}
|
||||||
|
miniView={true}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={tw`bg-white`}>
|
||||||
|
<View style={tw`mb-2 pt-4 px-4`}>
|
||||||
|
<MyText style={tw`text-2xl font-extrabold text-gray-900 tracking-tight`}>
|
||||||
|
{title}
|
||||||
|
</MyText>
|
||||||
|
<MyText style={tw`text-sm text-gray-500 font-medium`}>
|
||||||
|
{subtitle}
|
||||||
|
</MyText>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{products.length === 0 ? (
|
||||||
|
<View style={tw`items-center justify-center py-10`}>
|
||||||
|
<MaterialIcons name="local-offer" size={28} color="#9CA3AF" />
|
||||||
|
<MyText style={tw`text-gray-500 mt-2`}>
|
||||||
|
No {title.toLowerCase()} available right now
|
||||||
|
</MyText>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={tw`relative`}>
|
||||||
|
<MyFlatList
|
||||||
|
data={products}
|
||||||
|
keyExtractor={(item) => item.id.toString()}
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
contentContainerStyle={rowListContent}
|
||||||
|
renderItem={renderItem}
|
||||||
|
removeClippedSubviews={true}
|
||||||
|
/>
|
||||||
|
<LinearGradient
|
||||||
|
colors={["transparent", "rgba(0,0,0,0.08)"]}
|
||||||
|
start={{ x: 0, y: 0.5 }}
|
||||||
|
end={{ x: 1, y: 0.5 }}
|
||||||
|
style={tw`absolute right-0 top-0 bottom-4 w-12 rounded-l-xl`}
|
||||||
|
pointerEvents="none"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Offers() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { data: recentProductsData, isLoading, error, refetch } =
|
const { data, isLoading, error, refetch } =
|
||||||
trpc.user.order.getRecentlyOrderedProducts.useQuery({
|
trpc.user.product.getOffersPage.useQuery();
|
||||||
limit: 20,
|
|
||||||
});
|
|
||||||
|
|
||||||
const recentProducts = recentProductsData?.products || [];
|
const combos = data?.combos || [];
|
||||||
|
const offers = data?.offers || [];
|
||||||
|
|
||||||
useManualRefresh(() => {
|
useManualRefresh(() => {
|
||||||
refetch();
|
refetch();
|
||||||
|
|
@ -41,9 +105,9 @@ export default function OrderAgain() {
|
||||||
return (
|
return (
|
||||||
<AppContainer>
|
<AppContainer>
|
||||||
<View style={tw`flex-1 justify-center items-center bg-gray-50`}>
|
<View style={tw`flex-1 justify-center items-center bg-gray-50`}>
|
||||||
<MaterialIcons name="refresh" size={48} color="#3B82F6" />
|
<MaterialIcons name="local-offer" size={48} color="#3B82F6" />
|
||||||
<MyText style={tw`text-gray-500 font-medium mt-4`}>
|
<MyText style={tw`text-gray-500 font-medium mt-4`}>
|
||||||
Loading your recent orders...
|
Loading offers...
|
||||||
</MyText>
|
</MyText>
|
||||||
</View>
|
</View>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
|
|
@ -57,90 +121,33 @@ export default function OrderAgain() {
|
||||||
<MaterialIcons name="error-outline" size={48} color="#EF4444" />
|
<MaterialIcons name="error-outline" size={48} color="#EF4444" />
|
||||||
<MyText style={tw`text-gray-900 text-lg font-bold mt-4`}>Oops!</MyText>
|
<MyText style={tw`text-gray-900 text-lg font-bold mt-4`}>Oops!</MyText>
|
||||||
<MyText style={tw`text-gray-500 mt-2`}>
|
<MyText style={tw`text-gray-500 mt-2`}>
|
||||||
Failed to load recent orders
|
Failed to load offers
|
||||||
</MyText>
|
</MyText>
|
||||||
</View>
|
</View>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleProductPress = (id: number) => {
|
||||||
|
router.push(`/(drawer)/(tabs)/order-again/product-detail/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabLayoutWrapper>
|
<TabLayoutWrapper>
|
||||||
<ScrollView style={tw`flex-1`} contentContainerStyle={tw`pb-20`}>
|
<ScrollView style={tw`flex-1`} contentContainerStyle={tw`pb-20`}>
|
||||||
<LinearGradient
|
<OffersRow
|
||||||
colors={["#194185", "#1570EF"]}
|
title="Combos"
|
||||||
start={{ x: 0, y: 0 }}
|
subtitle="Bundle your favorites and save"
|
||||||
end={{ x: 1, y: 1 }}
|
products={combos}
|
||||||
style={tw`pb-8 pt-6 px-5 rounded-b-[32px] shadow-lg mb-4`}
|
onProductPress={handleProductPress}
|
||||||
>
|
/>
|
||||||
<View style={tw`flex-row justify-between items-start mb-6`}>
|
|
||||||
<View style={tw`flex-1 mr-4`}>
|
|
||||||
<View style={tw`flex-row items-center mb-1`}>
|
|
||||||
<View style={tw`bg-white/20 p-1 rounded-full mr-2`}>
|
|
||||||
<MaterialIcons name="refresh" size={14} color="#FFF" />
|
|
||||||
</View>
|
|
||||||
<MyText
|
|
||||||
style={tw`text-brand100 text-xs font-bold uppercase tracking-widest`}
|
|
||||||
>
|
|
||||||
Order Again
|
|
||||||
</MyText>
|
|
||||||
</View>
|
|
||||||
<MyText
|
|
||||||
style={tw`text-white text-sm font-medium opacity-90 ml-1`}
|
|
||||||
numberOfLines={1}
|
|
||||||
>
|
|
||||||
Reorder your favorite items quickly
|
|
||||||
</MyText>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</LinearGradient>
|
|
||||||
|
|
||||||
<View style={tw`bg-white`}>
|
<OffersRow
|
||||||
<View style={tw`flex-row items-center mb-2 px-4 pt-4`}>
|
title="Offers"
|
||||||
<MyText style={tw`text-lg font-bold text-gray-900`}>
|
subtitle="Great prices on select items"
|
||||||
Recently Ordered
|
products={offers}
|
||||||
</MyText>
|
onProductPress={handleProductPress}
|
||||||
</View>
|
/>
|
||||||
|
|
||||||
{recentProducts.length === 0 ? (
|
|
||||||
<View style={tw`items-center justify-center py-12`}>
|
|
||||||
<View
|
|
||||||
style={tw`w-20 h-20 bg-gray-100 rounded-full items-center justify-center mb-4`}
|
|
||||||
>
|
|
||||||
<MaterialIcons name="shopping-bag" size={32} color="#9CA3AF" />
|
|
||||||
</View>
|
|
||||||
<MyText style={tw`text-lg font-bold text-gray-900`}>
|
|
||||||
No recent orders
|
|
||||||
</MyText>
|
|
||||||
<MyText style={tw`text-gray-500 mt-2 text-center px-8`}>
|
|
||||||
Items you've ordered recently will appear here
|
|
||||||
</MyText>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<View style={tw`px-4 pb-4`}>
|
|
||||||
<View style={tw`flex-row flex-wrap justify-between`}>
|
|
||||||
{recentProducts.map((item, index) => (
|
|
||||||
<View
|
|
||||||
key={item.id}
|
|
||||||
style={tw`mb-4 ${index % 2 === 0 ? "mr-2" : ""}`}
|
|
||||||
>
|
|
||||||
<ProductCard
|
|
||||||
item={item}
|
|
||||||
itemWidth={itemWidth}
|
|
||||||
onPress={() =>
|
|
||||||
router.push(
|
|
||||||
`/(drawer)/(tabs)/order-again/product-detail/${item.id}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
showDeliveryInfo={false}
|
|
||||||
useAddToCartDialog={true}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
<View style={tw`absolute bottom-2 left-4 right-4`}>
|
<View style={tw`absolute bottom-2 left-4 right-4`}>
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,9 @@ const ProductCard: React.FC<ProductCardProps> = ({
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={tw`flex-row items-center mb-2`}>
|
<View style={tw`flex-row items-center mb-2`}>
|
||||||
<MyText style={tw`text-gray-500 text-xs font-medium`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{item.unitNotation}</MyText></MyText>
|
{item.productType !== 'combo' && (
|
||||||
|
<MyText style={tw`text-gray-500 text-xs font-medium`}>Quantity: <MyText style={tw`text-[#f81260] font-semibold`}>{item.unitNotation}</MyText></MyText>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{showDeliveryInfo && displayDeliveryDate && (
|
{showDeliveryInfo && displayDeliveryDate && (
|
||||||
|
|
|
||||||
|
|
@ -233,8 +233,11 @@ export {
|
||||||
createProductReview as createUserProductReview,
|
createProductReview as createUserProductReview,
|
||||||
getAllProductsWithUnits,
|
getAllProductsWithUnits,
|
||||||
getAllSkusSummary,
|
getAllSkusSummary,
|
||||||
|
getOffersAndCombos,
|
||||||
type ProductSummaryData,
|
type ProductSummaryData,
|
||||||
type SkuSummary,
|
type SkuSummary,
|
||||||
|
type OffersPageData,
|
||||||
|
type OffersPageProductData,
|
||||||
} from './src/user-apis/product'
|
} from './src/user-apis/product'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
@ -377,6 +380,15 @@ export {
|
||||||
deleteOrdersWithRelations,
|
deleteOrdersWithRelations,
|
||||||
} from './src/lib/delete-orders'
|
} from './src/lib/delete-orders'
|
||||||
|
|
||||||
|
// SKU Features Helper
|
||||||
|
export {
|
||||||
|
cleanFeatureValue,
|
||||||
|
splitQuantityFeature,
|
||||||
|
composeUnitNotation,
|
||||||
|
composeSkuName,
|
||||||
|
type SkuFeatureLike,
|
||||||
|
} from './src/lib/sku-features'
|
||||||
|
|
||||||
// Upload URL Helpers
|
// Upload URL Helpers
|
||||||
export {
|
export {
|
||||||
createUploadUrlStatus,
|
createUploadUrlStatus,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import type {
|
||||||
} from '@packages/shared'
|
} from '@packages/shared'
|
||||||
import type { InferSelectModel } from 'drizzle-orm'
|
import type { InferSelectModel } from 'drizzle-orm'
|
||||||
import { coerceDate } from '../lib/date'
|
import { coerceDate } from '../lib/date'
|
||||||
|
import { composeSkuName, composeUnitNotation } from '../lib/sku-features'
|
||||||
|
|
||||||
const isPaymentStatus = (value: string): value is PaymentStatus =>
|
const isPaymentStatus = (value: string): value is PaymentStatus =>
|
||||||
value === 'pending' || value === 'success' || value === 'cod' || value === 'failed'
|
value === 'pending' || value === 'success' || value === 'cod' || value === 'failed'
|
||||||
|
|
@ -236,12 +237,12 @@ export async function getOrderDetails(orderId: number): Promise<AdminOrderDetail
|
||||||
isDelivered: orderStatusRecord?.isDelivered || false,
|
isDelivered: orderStatusRecord?.isDelivered || false,
|
||||||
items: orderData.orderItems.map((item: any) => ({
|
items: orderData.orderItems.map((item: any) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.sku.product?.name ?? 'Unknown',
|
name: composeSkuName(item.sku.product?.name ?? 'Unknown', item.sku.features || []),
|
||||||
skuName: item.sku.name ?? null,
|
skuName: item.sku.name ?? null,
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
productSize: 1,
|
productSize: 1,
|
||||||
price: item.price,
|
price: item.price,
|
||||||
unit: (item.sku.features || []).map((f: any) => f.featureValue).join(' '),
|
unit: composeUnitNotation(item.sku.features || []),
|
||||||
amount: parseFloat(item.price.toString()) * parseFloat(item.quantity || '0'),
|
amount: parseFloat(item.price.toString()) * parseFloat(item.quantity || '0'),
|
||||||
isPackaged: item.is_packaged,
|
isPackaged: item.is_packaged,
|
||||||
isPackageVerified: item.is_package_verified,
|
isPackageVerified: item.is_package_verified,
|
||||||
|
|
@ -375,12 +376,12 @@ export async function getSlotOrders(slotId: string): Promise<AdminGetSlotOrdersR
|
||||||
|
|
||||||
const items = order.orderItems.map((item: any) => ({
|
const items = order.orderItems.map((item: any) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.sku.product?.name ?? 'Unknown',
|
name: composeSkuName(item.sku.product?.name ?? 'Unknown', item.sku.features || []),
|
||||||
skuName: item.sku.name ?? null,
|
skuName: item.sku.name ?? null,
|
||||||
quantity: parseFloat(item.quantity),
|
quantity: parseFloat(item.quantity),
|
||||||
price: parseFloat(item.price.toString()),
|
price: parseFloat(item.price.toString()),
|
||||||
amount: parseFloat(item.quantity) * parseFloat(item.price.toString()),
|
amount: parseFloat(item.quantity) * parseFloat(item.price.toString()),
|
||||||
unit: (item.sku.features || []).map((f: any) => f.featureValue).join(' '),
|
unit: composeUnitNotation(item.sku.features || []),
|
||||||
isPackaged: item.is_packaged,
|
isPackaged: item.is_packaged,
|
||||||
isPackageVerified: item.is_package_verified,
|
isPackageVerified: item.is_package_verified,
|
||||||
features: (item.sku.features || []).map((f: any) => ({
|
features: (item.sku.features || []).map((f: any) => ({
|
||||||
|
|
@ -545,12 +546,12 @@ export async function getAllOrders(input: GetAllOrdersInput): Promise<AdminGetAl
|
||||||
const items = order.orderItems
|
const items = order.orderItems
|
||||||
.map((item: any) => ({
|
.map((item: any) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.sku.product?.name ?? 'Unknown',
|
name: composeSkuName(item.sku.product?.name ?? 'Unknown', item.sku.features || []),
|
||||||
skuName: item.sku.name ?? null,
|
skuName: item.sku.name ?? null,
|
||||||
quantity: parseFloat(item.quantity),
|
quantity: parseFloat(item.quantity),
|
||||||
price: parseFloat(item.price.toString()),
|
price: parseFloat(item.price.toString()),
|
||||||
amount: parseFloat(item.quantity) * parseFloat(item.price.toString()),
|
amount: parseFloat(item.quantity) * parseFloat(item.price.toString()),
|
||||||
unit: (item.sku.features || []).map((f: any) => f.featureValue).join(' '),
|
unit: composeUnitNotation(item.sku.features || []),
|
||||||
productSize: 1,
|
productSize: 1,
|
||||||
isPackaged: item.is_packaged,
|
isPackaged: item.is_packaged,
|
||||||
isPackageVerified: item.is_package_verified,
|
isPackageVerified: item.is_package_verified,
|
||||||
|
|
|
||||||
44
packages/db_helper_sqlite/src/lib/sku-features.ts
Normal file
44
packages/db_helper_sqlite/src/lib/sku-features.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
export interface SkuFeatureLike {
|
||||||
|
featureName?: string | null
|
||||||
|
featureValue: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cleanFeatureValue = (value: string): string =>
|
||||||
|
value.replace(/\.0(?=\D|$)/g, '')
|
||||||
|
|
||||||
|
export function splitQuantityFeature(features: SkuFeatureLike[]): {
|
||||||
|
quantity: string
|
||||||
|
others: string[]
|
||||||
|
} {
|
||||||
|
if (!features || features.length === 0) {
|
||||||
|
return { quantity: '', others: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const quantityFeature = features.find((f) => f.featureName === 'quantity')
|
||||||
|
const otherFeatures = features.filter((f) => f.featureName !== 'quantity')
|
||||||
|
|
||||||
|
return {
|
||||||
|
quantity: quantityFeature ? cleanFeatureValue(quantityFeature.featureValue) : '',
|
||||||
|
others: otherFeatures.map((f) => cleanFeatureValue(f.featureValue)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the unit notation for a SKU: the quantity feature value only.
|
||||||
|
* Falls back to joining all feature values when no quantity feature exists.
|
||||||
|
*/
|
||||||
|
export function composeUnitNotation(features: SkuFeatureLike[]): string {
|
||||||
|
const { quantity, others } = splitQuantityFeature(features)
|
||||||
|
if (quantity) return quantity
|
||||||
|
if (others.length > 0) return others.join(' ')
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the display name for a SKU: the product name followed by the
|
||||||
|
* values of all non-quantity features (values only).
|
||||||
|
*/
|
||||||
|
export function composeSkuName(baseName: string, features: SkuFeatureLike[]): string {
|
||||||
|
const { others } = splitQuantityFeature(features)
|
||||||
|
return [baseName, ...others].filter(Boolean).join(' ')
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
userIncidents,
|
userIncidents,
|
||||||
} from '../db/schema'
|
} from '../db/schema'
|
||||||
import { eq, and, gt, sql, isNotNull, asc, inArray } from 'drizzle-orm'
|
import { eq, and, gt, sql, isNotNull, asc, inArray } from 'drizzle-orm'
|
||||||
|
import { composeSkuName, composeUnitNotation } from '../lib/sku-features'
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// BANNER STORE HELPERS
|
// BANNER STORE HELPERS
|
||||||
|
|
@ -117,7 +118,7 @@ export async function getAllProductsForCache(): Promise<ProductBasicData[]> {
|
||||||
return {
|
return {
|
||||||
id: sku.id,
|
id: sku.id,
|
||||||
productId: sku.productId,
|
productId: sku.productId,
|
||||||
name: sku.product?.name ?? 'Unknown',
|
name: composeSkuName(sku.product?.name ?? 'Unknown', features),
|
||||||
skuName: sku.name ?? null,
|
skuName: sku.name ?? null,
|
||||||
shortDescription: sku.product?.shortDescription ?? null,
|
shortDescription: sku.product?.shortDescription ?? null,
|
||||||
longDescription: sku.product?.longDescription ?? null,
|
longDescription: sku.product?.longDescription ?? null,
|
||||||
|
|
@ -126,7 +127,7 @@ export async function getAllProductsForCache(): Promise<ProductBasicData[]> {
|
||||||
images: sku.images,
|
images: sku.images,
|
||||||
isOutOfStock: sku.isOutOfStock,
|
isOutOfStock: sku.isOutOfStock,
|
||||||
storeId: sku.product?.storeId ?? null,
|
storeId: sku.product?.storeId ?? null,
|
||||||
unitNotation: features.map((f) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
|
unitNotation: composeUnitNotation(features),
|
||||||
incrementStep: sku.product?.incrementStep ?? 1,
|
incrementStep: sku.product?.incrementStep ?? 1,
|
||||||
productQuantity: 1,
|
productQuantity: 1,
|
||||||
isFlashAvailable: sku.isFlashAvailable,
|
isFlashAvailable: sku.isFlashAvailable,
|
||||||
|
|
@ -204,10 +205,10 @@ export async function getAllProductCombosForCache(): Promise<ProductComboCacheDa
|
||||||
return {
|
return {
|
||||||
comboSkuId: ci.comboSkuId,
|
comboSkuId: ci.comboSkuId,
|
||||||
skuId: ci.skuId,
|
skuId: ci.skuId,
|
||||||
productName: ci.sku?.product?.name ?? 'Unknown',
|
productName: composeSkuName(ci.sku?.product?.name ?? 'Unknown', features),
|
||||||
skuName: ci.sku?.name ?? null,
|
skuName: ci.sku?.name ?? null,
|
||||||
images: ci.sku?.images,
|
images: ci.sku?.images,
|
||||||
unitNotation: features.map((f) => f.featureValue).join(' '),
|
unitNotation: composeUnitNotation(features),
|
||||||
price: String(ci.sku?.price ?? '0'),
|
price: String(ci.sku?.price ?? '0'),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -332,13 +333,13 @@ export async function getAllSlotsWithProductsForCache(): Promise<SlotWithProduct
|
||||||
return {
|
return {
|
||||||
id: sku.id,
|
id: sku.id,
|
||||||
productId: sku.productId,
|
productId: sku.productId,
|
||||||
name: sku.product?.name ?? 'Unknown',
|
name: composeSkuName(sku.product?.name ?? 'Unknown', features),
|
||||||
skuName: sku.name ?? null,
|
skuName: sku.name ?? null,
|
||||||
productQuantity: 1,
|
productQuantity: 1,
|
||||||
shortDescription: sku.product?.shortDescription ?? null,
|
shortDescription: sku.product?.shortDescription ?? null,
|
||||||
price: String(sku.price ?? '0'),
|
price: String(sku.price ?? '0'),
|
||||||
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
|
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
|
||||||
unitNotation: features.map((f: any) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
|
unitNotation: composeUnitNotation(features),
|
||||||
store: sku.product?.store ? {
|
store: sku.product?.store ? {
|
||||||
id: sku.product.store.id,
|
id: sku.product.store.id,
|
||||||
name: sku.product.store.name,
|
name: sku.product.store.name,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { db } from '../db/db_index'
|
||||||
import { cartItems, productSkus } from '../db/schema'
|
import { cartItems, productSkus } from '../db/schema'
|
||||||
import { and, eq, sql } from 'drizzle-orm'
|
import { and, eq, sql } from 'drizzle-orm'
|
||||||
import type { UserCartItem } from '@packages/shared'
|
import type { UserCartItem } from '@packages/shared'
|
||||||
|
import { composeSkuName, composeUnitNotation } from '../lib/sku-features'
|
||||||
|
|
||||||
const getStringArray = (value: unknown): string[] => {
|
const getStringArray = (value: unknown): string[] => {
|
||||||
if (!Array.isArray(value)) return []
|
if (!Array.isArray(value)) return []
|
||||||
|
|
@ -33,10 +34,10 @@ export async function getCartItemsWithProducts(userId: number): Promise<UserCart
|
||||||
addedAt: item.addedAt,
|
addedAt: item.addedAt,
|
||||||
product: {
|
product: {
|
||||||
id: sku?.id ?? 0,
|
id: sku?.id ?? 0,
|
||||||
name: sku?.product?.name ?? 'Unknown',
|
name: composeSkuName(sku?.product?.name ?? 'Unknown', features),
|
||||||
price: String(priceValue),
|
price: String(priceValue),
|
||||||
productQuantity: 1,
|
productQuantity: 1,
|
||||||
unit: features.map((f) => f.featureValue).join(' '),
|
unit: composeUnitNotation(features),
|
||||||
isOutOfStock: sku?.isOutOfStock ?? false,
|
isOutOfStock: sku?.isOutOfStock ?? false,
|
||||||
images: getStringArray(sku?.images),
|
images: getStringArray(sku?.images),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import type {
|
||||||
} from '@packages/shared'
|
} from '@packages/shared'
|
||||||
import { coerceDate } from '../lib/date'
|
import { coerceDate } from '../lib/date'
|
||||||
import { runBatched } from '../lib/run-batched'
|
import { runBatched } from '../lib/run-batched'
|
||||||
|
import { composeSkuName, composeUnitNotation } from '../lib/sku-features'
|
||||||
|
|
||||||
export interface OrderItemInput {
|
export interface OrderItemInput {
|
||||||
productId: number
|
productId: number
|
||||||
|
|
@ -686,12 +687,12 @@ export async function getProductsForRecentOrders(
|
||||||
const features = sku.features || []
|
const features = sku.features || []
|
||||||
return {
|
return {
|
||||||
id: sku.id,
|
id: sku.id,
|
||||||
name: sku.product?.name ?? 'Unknown',
|
name: composeSkuName(sku.product?.name ?? 'Unknown', features),
|
||||||
shortDescription: sku.product?.shortDescription ?? null,
|
shortDescription: sku.product?.shortDescription ?? null,
|
||||||
price: String(sku.price ?? '0'),
|
price: String(sku.price ?? '0'),
|
||||||
images: sku.images,
|
images: sku.images,
|
||||||
isOutOfStock: sku.isOutOfStock,
|
isOutOfStock: sku.isOutOfStock,
|
||||||
unitShortNotation: features.map((f) => f.featureValue).join(' '),
|
unitShortNotation: composeUnitNotation(features),
|
||||||
incrementStep: sku.product?.incrementStep ?? 1,
|
incrementStep: sku.product?.incrementStep ?? 1,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { db } from '../db/db_index'
|
||||||
import { deliverySlotInfo, productInfo, productCombos, productSkus, productReviews, productTags, specialDeals, storeInfo, users } from '../db/schema'
|
import { deliverySlotInfo, productInfo, productCombos, productSkus, productReviews, productTags, specialDeals, storeInfo, users } from '../db/schema'
|
||||||
import { and, desc, eq, gt, sql } from 'drizzle-orm'
|
import { and, desc, eq, gt, sql } from 'drizzle-orm'
|
||||||
import type { UserProductDetailData, UserProductReview } from '@packages/shared'
|
import type { UserProductDetailData, UserProductReview } from '@packages/shared'
|
||||||
|
import { composeSkuName, composeUnitNotation } from '../lib/sku-features'
|
||||||
|
|
||||||
const getStringArray = (value: unknown): string[] | null => {
|
const getStringArray = (value: unknown): string[] | null => {
|
||||||
if (!Array.isArray(value)) return null
|
if (!Array.isArray(value)) return null
|
||||||
|
|
@ -56,8 +57,8 @@ export async function getProductDetailById(skuId: number): Promise<UserProductDe
|
||||||
return {
|
return {
|
||||||
skuId: ci.skuId,
|
skuId: ci.skuId,
|
||||||
skuName: ci.sku?.name ?? null,
|
skuName: ci.sku?.name ?? null,
|
||||||
unitNotation: ciFeatures.map((f) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
|
unitNotation: composeUnitNotation(ciFeatures),
|
||||||
productName: ci.sku?.product?.name ?? 'Unknown',
|
productName: composeSkuName(ci.sku?.product?.name ?? 'Unknown', ciFeatures),
|
||||||
images: getStringArray(ci.sku?.images),
|
images: getStringArray(ci.sku?.images),
|
||||||
price: String(ci.sku?.price ?? '0'),
|
price: String(ci.sku?.price ?? '0'),
|
||||||
}
|
}
|
||||||
|
|
@ -65,12 +66,12 @@ export async function getProductDetailById(skuId: number): Promise<UserProductDe
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: sku.id,
|
id: sku.id,
|
||||||
name: product?.name ?? 'Unknown',
|
name: composeSkuName(product?.name ?? 'Unknown', features),
|
||||||
shortDescription: product?.shortDescription ?? null,
|
shortDescription: product?.shortDescription ?? null,
|
||||||
longDescription: product?.longDescription ?? null,
|
longDescription: product?.longDescription ?? null,
|
||||||
price: String(sku.price ?? '0'),
|
price: String(sku.price ?? '0'),
|
||||||
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
|
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
|
||||||
unitNotation: features.map((f) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
|
unitNotation: composeUnitNotation(features),
|
||||||
images: getStringArray(sku.images),
|
images: getStringArray(sku.images),
|
||||||
isOutOfStock: sku.isOutOfStock,
|
isOutOfStock: sku.isOutOfStock,
|
||||||
store: storeData ? {
|
store: storeData ? {
|
||||||
|
|
@ -215,7 +216,7 @@ export async function getAllProductsWithUnits(tagId?: number): Promise<ProductSu
|
||||||
const features = sku.features || []
|
const features = sku.features || []
|
||||||
return {
|
return {
|
||||||
id: sku.product?.id ?? 0,
|
id: sku.product?.id ?? 0,
|
||||||
name: sku.product?.name ?? 'Unknown',
|
name: composeSkuName(sku.product?.name ?? 'Unknown', features),
|
||||||
skuId: sku.id,
|
skuId: sku.id,
|
||||||
skuName: sku.name ?? null,
|
skuName: sku.name ?? null,
|
||||||
shortDescription: sku.product?.shortDescription ?? null,
|
shortDescription: sku.product?.shortDescription ?? null,
|
||||||
|
|
@ -223,7 +224,7 @@ 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(' '),
|
unitShortNotation: composeUnitNotation(features),
|
||||||
productQuantity: 1,
|
productQuantity: 1,
|
||||||
features: features.map((f) => ({ featureName: f.featureName, featureValue: f.featureValue })),
|
features: features.map((f) => ({ featureName: f.featureName, featureValue: f.featureValue })),
|
||||||
}
|
}
|
||||||
|
|
@ -296,3 +297,65 @@ export async function getAllSkusSummary(): Promise<SkuSummary[]> {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OffersPageProductData {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
price: string
|
||||||
|
marketPrice: string | null
|
||||||
|
unitNotation: string
|
||||||
|
images: unknown
|
||||||
|
isOutOfStock: boolean
|
||||||
|
incrementStep: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OffersPageData {
|
||||||
|
combos: OffersPageProductData[]
|
||||||
|
offers: OffersPageProductData[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapOffersPageProduct = (sku: {
|
||||||
|
id: number
|
||||||
|
price: string | null
|
||||||
|
marketPrice: string | null
|
||||||
|
images: unknown
|
||||||
|
isOutOfStock: boolean
|
||||||
|
product: { name: string; incrementStep: number | null } | null
|
||||||
|
features: Array<{ featureValue: string }>
|
||||||
|
}): OffersPageProductData => {
|
||||||
|
const features = sku.features || []
|
||||||
|
return {
|
||||||
|
id: sku.id,
|
||||||
|
name: composeSkuName(sku.product?.name ?? 'Unknown', features),
|
||||||
|
price: String(sku.price ?? '0'),
|
||||||
|
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
|
||||||
|
unitNotation: composeUnitNotation(features),
|
||||||
|
images: sku.images,
|
||||||
|
isOutOfStock: sku.isOutOfStock,
|
||||||
|
incrementStep: sku.product?.incrementStep ?? 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOffersAndCombos(): Promise<OffersPageData> {
|
||||||
|
const skus = await db.query.productSkus.findMany({
|
||||||
|
where: eq(productSkus.isSuspended, false),
|
||||||
|
with: {
|
||||||
|
product: true,
|
||||||
|
features: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const combos: OffersPageProductData[] = []
|
||||||
|
const offers: OffersPageProductData[] = []
|
||||||
|
|
||||||
|
for (const sku of skus) {
|
||||||
|
if (sku.product?.productType === 'combo') {
|
||||||
|
combos.push(mapOffersPageProduct(sku))
|
||||||
|
}
|
||||||
|
if (sku.isOffer) {
|
||||||
|
offers.push(mapOffersPageProduct(sku))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { combos, offers }
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { productInfo, productSkus, storeInfo } from '../db/schema'
|
||||||
import { and, asc, eq, inArray } from 'drizzle-orm'
|
import { and, asc, eq, inArray } 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'
|
||||||
|
import { composeSkuName, composeUnitNotation } from '../lib/sku-features'
|
||||||
|
|
||||||
type StoreRow = InferSelectModel<typeof storeInfo>
|
type StoreRow = InferSelectModel<typeof storeInfo>
|
||||||
|
|
||||||
|
|
@ -91,13 +92,13 @@ export async function getStoreDetail(storeId: number): Promise<UserStoreDetailDa
|
||||||
const features = sku.features || []
|
const features = sku.features || []
|
||||||
return {
|
return {
|
||||||
id: sku.id,
|
id: sku.id,
|
||||||
name: sku.product?.name ?? 'Unknown',
|
name: composeSkuName(sku.product?.name ?? 'Unknown', features),
|
||||||
shortDescription: sku.product?.shortDescription ?? null,
|
shortDescription: sku.product?.shortDescription ?? null,
|
||||||
price: String(sku.price ?? '0'),
|
price: String(sku.price ?? '0'),
|
||||||
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
|
marketPrice: sku.marketPrice ? String(sku.marketPrice) : null,
|
||||||
incrementStep: sku.product?.incrementStep ?? 1,
|
incrementStep: sku.product?.incrementStep ?? 1,
|
||||||
unit: features.map((f) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
|
unit: composeUnitNotation(features),
|
||||||
unitNotation: features.map((f) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '),
|
unitNotation: composeUnitNotation(features),
|
||||||
images: getStringArray(sku.images),
|
images: getStringArray(sku.images),
|
||||||
isOutOfStock: sku.isOutOfStock,
|
isOutOfStock: sku.isOutOfStock,
|
||||||
productQuantity: 1,
|
productQuantity: 1,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue