From b5cdc53e50b4ccdcdf395f2d675b541a595498cb Mon Sep 17 00:00:00 2001 From: shafi54 <108669266+shafi-aviz@users.noreply.github.com> Date: Sun, 2 Aug 2026 10:03:21 +0530 Subject: [PATCH] enh --- apps/admin-ui/app/(drawer)/products/add.tsx | 22 +++ apps/admin-ui/app/(drawer)/products/edit.tsx | 22 +++ apps/admin-ui/src/components/ProductForm.tsx | 84 ++++++++- apps/backend/src/lib/post-order-handler.ts | 9 +- .../apis/admin-apis/apis/vendor-snippets.ts | 14 +- .../src/trpc/apis/common-apis/common.ts | 3 +- .../src/trpc/apis/user-apis/apis/product.ts | 18 ++ .../app/(drawer)/(tabs)/order-again/index.tsx | 173 +++++++++--------- apps/user-ui/components/ProductCard.tsx | 4 +- packages/db_helper_sqlite/index.ts | 12 ++ .../db_helper_sqlite/src/admin-apis/order.ts | 13 +- .../db_helper_sqlite/src/lib/sku-features.ts | 44 +++++ .../src/stores/store-helpers.ts | 13 +- .../db_helper_sqlite/src/user-apis/cart.ts | 5 +- .../db_helper_sqlite/src/user-apis/order.ts | 5 +- .../db_helper_sqlite/src/user-apis/product.ts | 75 +++++++- .../db_helper_sqlite/src/user-apis/stores.ts | 7 +- 17 files changed, 400 insertions(+), 123 deletions(-) create mode 100644 packages/db_helper_sqlite/src/lib/sku-features.ts diff --git a/apps/admin-ui/app/(drawer)/products/add.tsx b/apps/admin-ui/app/(drawer)/products/add.tsx index 294bdd4..2a4427c 100644 --- a/apps/admin-ui/app/(drawer)/products/add.tsx +++ b/apps/admin-ui/app/(drawer)/products/add.tsx @@ -14,6 +14,28 @@ export default function AddProduct() { const handleSubmit = async (values: any, variantImages: ImageUploaderNeoPayload[][], _deletedImageKeys?: string[]) => { 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() + 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 imageCounts: number[] = variantImages.map((imgs) => imgs.length) diff --git a/apps/admin-ui/app/(drawer)/products/edit.tsx b/apps/admin-ui/app/(drawer)/products/edit.tsx index 1012943..4ef6f28 100644 --- a/apps/admin-ui/app/(drawer)/products/edit.tsx +++ b/apps/admin-ui/app/(drawer)/products/edit.tsx @@ -78,6 +78,28 @@ export default function EditProduct() { const handleSubmit = async (values: any, variantImages: ImageUploaderNeoPayload[][], deletedImageKeys: string[]) => { 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() + 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 imageCounts: number[] = variantImages.map((imgs) => imgs.filter((img) => img.mimeType !== null).length diff --git a/apps/admin-ui/src/components/ProductForm.tsx b/apps/admin-ui/src/components/ProductForm.tsx index 85e5697..8d0db2c 100644 --- a/apps/admin-ui/src/components/ProductForm.tsx +++ b/apps/admin-ui/src/components/ProductForm.tsx @@ -1,6 +1,7 @@ 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 * as Yup from 'yup' import { MyTextInput, BottomDropdown, MyText, tw, ImageUploaderNeo, ImageUploaderNeoItem, ImageUploaderNeoPayload, Checkbox } from 'common-ui' import MaterialIcons from '@expo/vector-icons/MaterialIcons' import { trpc } from '../trpc-client' @@ -60,6 +61,60 @@ const defaultVariant = (): Variant => ({ 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() + 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(({ mode, initialValues, @@ -93,6 +148,7 @@ const ProductForm = forwardRef(({ return ( { const images = variantImages.map((imgs) => imgs.map((img) => ({ url: img.imgUrl, mimeType: img.mimeType })) @@ -113,7 +169,8 @@ const ProductForm = forwardRef(({ }} enableReinitialize > - {({ handleChange, handleSubmit, values, setFieldValue }) => ( + {({ handleChange, handleSubmit, values, setFieldValue, validateForm }) => { + return ( (({ 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 | 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} 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(({ - )} + ) + }} ) }) diff --git a/apps/backend/src/lib/post-order-handler.ts b/apps/backend/src/lib/post-order-handler.ts index 13abcd0..b06e504 100644 --- a/apps/backend/src/lib/post-order-handler.ts +++ b/apps/backend/src/lib/post-order-handler.ts @@ -1,6 +1,8 @@ import { getOrdersByIdsWithFullData, getOrderByIdWithFullData, + composeSkuName, + composeUnitNotation, } from '@/src/dbService' import { sendTelegramMessage } from '@/src/lib/telegram-service' import { queueDataPusher } from '@/src/lib/queue-data-pusher' @@ -49,10 +51,11 @@ const formatOrderMessageWithFullData = (ordersData: any[]): string => { message += 'šŸ“¦ Items:\n'; order.orderItems?.forEach((item: any) => { const sku = item.sku - const features = (sku?.features || []).map((f: any) => f.featureValue).join(' ') - message += ` • ${sku?.product?.name || 'Unknown'} ${features} x${item.quantity}\n`; + const features = sku?.features || [] + const name = composeSkuName(sku?.product?.name || 'Unknown', features) + const unit = composeUnitNotation(features) + message += ` • ${name} ${unit} x${item.quantity}\n`; }); - message += `\nšŸ’° Total: ₹${order.totalAmount}\n`; message += `🚚 Delivery: ${ diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/vendor-snippets.ts b/apps/backend/src/trpc/apis/admin-apis/apis/vendor-snippets.ts index d077771..1ffc792 100644 --- a/apps/backend/src/trpc/apis/admin-apis/apis/vendor-snippets.ts +++ b/apps/backend/src/trpc/apis/admin-apis/apis/vendor-snippets.ts @@ -16,6 +16,8 @@ import { getVendorOrders as getVendorOrdersInDb, updateVendorOrderItemPackaging as updateVendorOrderItemPackagingInDb, getSlotsAfterDate as getSlotsAfterDateInDb, + composeSkuName, + composeUnitNotation, } from '@/src/dbService' import type { AdminVendorSnippet, @@ -417,11 +419,11 @@ export const vendorSnippetsRouter = router({ const products = attachedOrderItems.map(item => ({ orderItemId: item.id, productId: item.skuId, - productName: item.sku?.product?.name || 'Unknown', + productName: composeSkuName(item.sku?.product?.name || 'Unknown', item.sku?.features || []), quantity: parseFloat(item.quantity), productSize: 1, 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), is_packaged: item.is_packaged, 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(), totalQuantity: order.orderItems.reduce((sum, item) => sum + parseFloat(item.quantity || '0'), 0), 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'), - 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 => ({ orderItemId: item.id, productId: item.skuId, - productName: item.sku?.product?.name || 'Unknown', + productName: composeSkuName(item.sku?.product?.name || 'Unknown', item.sku?.features || []), quantity: parseFloat(item.quantity), 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), productSize: 1, is_packaged: item.is_packaged, diff --git a/apps/backend/src/trpc/apis/common-apis/common.ts b/apps/backend/src/trpc/apis/common-apis/common.ts index b9522fc..be00330 100644 --- a/apps/backend/src/trpc/apis/common-apis/common.ts +++ b/apps/backend/src/trpc/apis/common-apis/common.ts @@ -54,7 +54,8 @@ export async function scaffoldProducts() { isFlashAvailable: product.isFlashAvailable, nextDeliveryDate: nextDeliveryDate ? nextDeliveryDate.toISOString() : null, images: product.images, - flashPrice: product.flashPrice + flashPrice: product.flashPrice, + productType: product.productType || 'item' }; }) ); diff --git a/apps/backend/src/trpc/apis/user-apis/apis/product.ts b/apps/backend/src/trpc/apis/user-apis/apis/product.ts index d65d225..be8145a 100644 --- a/apps/backend/src/trpc/apis/user-apis/apis/product.ts +++ b/apps/backend/src/trpc/apis/user-apis/apis/product.ts @@ -9,6 +9,7 @@ import { getUserProductReviews as getUserProductReviewsInDb, getUserProductByIdBasic as getUserProductByIdBasicInDb, createUserProductReview as createUserProductReviewInDb, + getOffersAndCombos as getOffersAndCombosInDb, } from '@/src/dbService' import type { UserProductDetail, @@ -181,4 +182,21 @@ export const productRouter = router({ 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), + }; + }), + }); diff --git a/apps/user-ui/app/(drawer)/(tabs)/order-again/index.tsx b/apps/user-ui/app/(drawer)/(tabs)/order-again/index.tsx index 7bd04bc..b18b9dd 100644 --- a/apps/user-ui/app/(drawer)/(tabs)/order-again/index.tsx +++ b/apps/user-ui/app/(drawer)/(tabs)/order-again/index.tsx @@ -17,17 +17,81 @@ import FloatingCartBar from "@/components/floating-cart-bar"; import TabLayoutWrapper from "@/components/TabLayoutWrapper"; 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 }) => ( + + onProductPress(item.id)} + showDeliveryInfo={false} + useAddToCartDialog={true} + miniView={true} + /> + + ); + + return ( + + + + {title} + + + {subtitle} + + + + {products.length === 0 ? ( + + + + No {title.toLowerCase()} available right now + + + ) : ( + + item.id.toString()} + horizontal + showsHorizontalScrollIndicator={false} + contentContainerStyle={rowListContent} + renderItem={renderItem} + removeClippedSubviews={true} + /> + + + )} + + ); +}; + +export default function Offers() { const router = useRouter(); - const { data: recentProductsData, isLoading, error, refetch } = - trpc.user.order.getRecentlyOrderedProducts.useQuery({ - limit: 20, - }); + const { data, isLoading, error, refetch } = + trpc.user.product.getOffersPage.useQuery(); - const recentProducts = recentProductsData?.products || []; + const combos = data?.combos || []; + const offers = data?.offers || []; useManualRefresh(() => { refetch(); @@ -41,9 +105,9 @@ export default function OrderAgain() { return ( - + - Loading your recent orders... + Loading offers... @@ -57,90 +121,33 @@ export default function OrderAgain() { Oops! - Failed to load recent orders + Failed to load offers ); } + const handleProductPress = (id: number) => { + router.push(`/(drawer)/(tabs)/order-again/product-detail/${id}`); + }; + return ( - - - - - - - - - Order Again - - - - Reorder your favorite items quickly - - - - + - - - - Recently Ordered - - - - {recentProducts.length === 0 ? ( - - - - - - No recent orders - - - Items you've ordered recently will appear here - - - ) : ( - - - {recentProducts.map((item, index) => ( - - - router.push( - `/(drawer)/(tabs)/order-again/product-detail/${item.id}` - ) - } - showDeliveryInfo={false} - useAddToCartDialog={true} - /> - - ))} - - - )} - + diff --git a/apps/user-ui/components/ProductCard.tsx b/apps/user-ui/components/ProductCard.tsx index 91dcad6..4c16022 100644 --- a/apps/user-ui/components/ProductCard.tsx +++ b/apps/user-ui/components/ProductCard.tsx @@ -222,7 +222,9 @@ const ProductCard: React.FC = ({ )} - Quantity: {item.unitNotation} + {item.productType !== 'combo' && ( + Quantity: {item.unitNotation} + )} {showDeliveryInfo && displayDeliveryDate && ( diff --git a/packages/db_helper_sqlite/index.ts b/packages/db_helper_sqlite/index.ts index ce79c6a..53b73f6 100644 --- a/packages/db_helper_sqlite/index.ts +++ b/packages/db_helper_sqlite/index.ts @@ -233,8 +233,11 @@ export { createProductReview as createUserProductReview, getAllProductsWithUnits, getAllSkusSummary, + getOffersAndCombos, type ProductSummaryData, type SkuSummary, + type OffersPageData, + type OffersPageProductData, } from './src/user-apis/product' export { @@ -377,6 +380,15 @@ export { deleteOrdersWithRelations, } from './src/lib/delete-orders' +// SKU Features Helper +export { + cleanFeatureValue, + splitQuantityFeature, + composeUnitNotation, + composeSkuName, + type SkuFeatureLike, +} from './src/lib/sku-features' + // Upload URL Helpers export { createUploadUrlStatus, diff --git a/packages/db_helper_sqlite/src/admin-apis/order.ts b/packages/db_helper_sqlite/src/admin-apis/order.ts index 259a68b..3165739 100644 --- a/packages/db_helper_sqlite/src/admin-apis/order.ts +++ b/packages/db_helper_sqlite/src/admin-apis/order.ts @@ -28,6 +28,7 @@ import type { } from '@packages/shared' import type { InferSelectModel } from 'drizzle-orm' import { coerceDate } from '../lib/date' +import { composeSkuName, composeUnitNotation } from '../lib/sku-features' const isPaymentStatus = (value: string): value is PaymentStatus => value === 'pending' || value === 'success' || value === 'cod' || value === 'failed' @@ -236,12 +237,12 @@ export async function getOrderDetails(orderId: number): Promise ({ id: item.id, - name: item.sku.product?.name ?? 'Unknown', + name: composeSkuName(item.sku.product?.name ?? 'Unknown', item.sku.features || []), skuName: item.sku.name ?? null, quantity: item.quantity, productSize: 1, 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'), isPackaged: item.is_packaged, isPackageVerified: item.is_package_verified, @@ -375,12 +376,12 @@ export async function getSlotOrders(slotId: string): Promise ({ id: item.id, - name: item.sku.product?.name ?? 'Unknown', + name: composeSkuName(item.sku.product?.name ?? 'Unknown', item.sku.features || []), skuName: item.sku.name ?? null, quantity: parseFloat(item.quantity), price: 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, isPackageVerified: item.is_package_verified, features: (item.sku.features || []).map((f: any) => ({ @@ -545,12 +546,12 @@ export async function getAllOrders(input: GetAllOrdersInput): Promise ({ id: item.id, - name: item.sku.product?.name ?? 'Unknown', + name: composeSkuName(item.sku.product?.name ?? 'Unknown', item.sku.features || []), skuName: item.sku.name ?? null, quantity: parseFloat(item.quantity), price: 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, isPackaged: item.is_packaged, isPackageVerified: item.is_package_verified, diff --git a/packages/db_helper_sqlite/src/lib/sku-features.ts b/packages/db_helper_sqlite/src/lib/sku-features.ts new file mode 100644 index 0000000..483933a --- /dev/null +++ b/packages/db_helper_sqlite/src/lib/sku-features.ts @@ -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(' ') +} diff --git a/packages/db_helper_sqlite/src/stores/store-helpers.ts b/packages/db_helper_sqlite/src/stores/store-helpers.ts index 6f3aeb2..a0c7b29 100644 --- a/packages/db_helper_sqlite/src/stores/store-helpers.ts +++ b/packages/db_helper_sqlite/src/stores/store-helpers.ts @@ -15,6 +15,7 @@ import { userIncidents, } from '../db/schema' import { eq, and, gt, sql, isNotNull, asc, inArray } from 'drizzle-orm' +import { composeSkuName, composeUnitNotation } from '../lib/sku-features' // ============================================================================ // BANNER STORE HELPERS @@ -117,7 +118,7 @@ export async function getAllProductsForCache(): Promise { return { id: sku.id, productId: sku.productId, - name: sku.product?.name ?? 'Unknown', + name: composeSkuName(sku.product?.name ?? 'Unknown', features), skuName: sku.name ?? null, shortDescription: sku.product?.shortDescription ?? null, longDescription: sku.product?.longDescription ?? null, @@ -126,7 +127,7 @@ export async function getAllProductsForCache(): Promise { images: sku.images, isOutOfStock: sku.isOutOfStock, storeId: sku.product?.storeId ?? null, - unitNotation: features.map((f) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '), + unitNotation: composeUnitNotation(features), incrementStep: sku.product?.incrementStep ?? 1, productQuantity: 1, isFlashAvailable: sku.isFlashAvailable, @@ -204,10 +205,10 @@ export async function getAllProductCombosForCache(): Promise f.featureValue).join(' '), + unitNotation: composeUnitNotation(features), price: String(ci.sku?.price ?? '0'), } }) @@ -332,13 +333,13 @@ export async function getAllSlotsWithProductsForCache(): Promise f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '), + unitNotation: composeUnitNotation(features), store: sku.product?.store ? { id: sku.product.store.id, name: sku.product.store.name, diff --git a/packages/db_helper_sqlite/src/user-apis/cart.ts b/packages/db_helper_sqlite/src/user-apis/cart.ts index 5ae13f2..521626d 100644 --- a/packages/db_helper_sqlite/src/user-apis/cart.ts +++ b/packages/db_helper_sqlite/src/user-apis/cart.ts @@ -2,6 +2,7 @@ import { db } from '../db/db_index' import { cartItems, productSkus } from '../db/schema' import { and, eq, sql } from 'drizzle-orm' import type { UserCartItem } from '@packages/shared' +import { composeSkuName, composeUnitNotation } from '../lib/sku-features' const getStringArray = (value: unknown): string[] => { if (!Array.isArray(value)) return [] @@ -33,10 +34,10 @@ export async function getCartItemsWithProducts(userId: number): Promise f.featureValue).join(' '), + unit: composeUnitNotation(features), isOutOfStock: sku?.isOutOfStock ?? false, images: getStringArray(sku?.images), }, diff --git a/packages/db_helper_sqlite/src/user-apis/order.ts b/packages/db_helper_sqlite/src/user-apis/order.ts index cd88f08..341034f 100644 --- a/packages/db_helper_sqlite/src/user-apis/order.ts +++ b/packages/db_helper_sqlite/src/user-apis/order.ts @@ -23,6 +23,7 @@ import type { } from '@packages/shared' import { coerceDate } from '../lib/date' import { runBatched } from '../lib/run-batched' +import { composeSkuName, composeUnitNotation } from '../lib/sku-features' export interface OrderItemInput { productId: number @@ -686,12 +687,12 @@ export async function getProductsForRecentOrders( const features = sku.features || [] return { id: sku.id, - name: sku.product?.name ?? 'Unknown', + name: composeSkuName(sku.product?.name ?? 'Unknown', features), shortDescription: sku.product?.shortDescription ?? null, price: String(sku.price ?? '0'), images: sku.images, isOutOfStock: sku.isOutOfStock, - unitShortNotation: features.map((f) => f.featureValue).join(' '), + unitShortNotation: composeUnitNotation(features), incrementStep: sku.product?.incrementStep ?? 1, } }) diff --git a/packages/db_helper_sqlite/src/user-apis/product.ts b/packages/db_helper_sqlite/src/user-apis/product.ts index 3fbb777..6548d9b 100644 --- a/packages/db_helper_sqlite/src/user-apis/product.ts +++ b/packages/db_helper_sqlite/src/user-apis/product.ts @@ -2,6 +2,7 @@ import { db } from '../db/db_index' import { deliverySlotInfo, productInfo, productCombos, productSkus, productReviews, productTags, specialDeals, storeInfo, users } from '../db/schema' import { and, desc, eq, gt, sql } from 'drizzle-orm' import type { UserProductDetailData, UserProductReview } from '@packages/shared' +import { composeSkuName, composeUnitNotation } from '../lib/sku-features' const getStringArray = (value: unknown): string[] | null => { if (!Array.isArray(value)) return null @@ -56,8 +57,8 @@ export async function getProductDetailById(skuId: number): Promise f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '), - productName: ci.sku?.product?.name ?? 'Unknown', + unitNotation: composeUnitNotation(ciFeatures), + productName: composeSkuName(ci.sku?.product?.name ?? 'Unknown', ciFeatures), images: getStringArray(ci.sku?.images), price: String(ci.sku?.price ?? '0'), } @@ -65,12 +66,12 @@ export async function getProductDetailById(skuId: number): Promise f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '), + unitNotation: composeUnitNotation(features), images: getStringArray(sku.images), isOutOfStock: sku.isOutOfStock, store: storeData ? { @@ -215,7 +216,7 @@ export async function getAllProductsWithUnits(tagId?: number): Promise f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '), + unitShortNotation: composeUnitNotation(features), productQuantity: 1, features: features.map((f) => ({ featureName: f.featureName, featureValue: f.featureValue })), } @@ -296,3 +297,65 @@ export async function getAllSkusSummary(): Promise { } }) } + +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 { + 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 } +} diff --git a/packages/db_helper_sqlite/src/user-apis/stores.ts b/packages/db_helper_sqlite/src/user-apis/stores.ts index 894e664..dd9019a 100644 --- a/packages/db_helper_sqlite/src/user-apis/stores.ts +++ b/packages/db_helper_sqlite/src/user-apis/stores.ts @@ -3,6 +3,7 @@ import { productInfo, productSkus, storeInfo } from '../db/schema' import { and, asc, eq, inArray } from 'drizzle-orm' import type { InferSelectModel } from 'drizzle-orm' import type { UserStoreDetailData, UserStoreProductData, UserStoreSummaryData, StoreSummary } from '@packages/shared' +import { composeSkuName, composeUnitNotation } from '../lib/sku-features' type StoreRow = InferSelectModel @@ -91,13 +92,13 @@ export async function getStoreDetail(storeId: number): Promise f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '), - unitNotation: features.map((f) => f.featureValue.replace(/\.0(?=\D|$)/g, '')).join(' '), + unit: composeUnitNotation(features), + unitNotation: composeUnitNotation(features), images: getStringArray(sku.images), isOutOfStock: sku.isOutOfStock, productQuantity: 1,