Compare commits

..

2 commits

Author SHA1 Message Date
shafi54
21002332fb enh 2026-08-15 00:35:47 +05:30
shafi54
9d2e01c6d2 enh 2026-08-15 00:30:04 +05:30
15 changed files with 181 additions and 86 deletions

View file

@ -416,7 +416,7 @@ export default function CustomizePopularItems() {
multiple={true} multiple={true}
label="Select Products" label="Select Products"
placeholder="Choose products..." placeholder="Choose products..."
labelFormat={(product) => `${product.name} - ₹${product.price}`} labelFormat={(product) => `${product.productName} - ₹${product.price}`}
/> />
<View style={tw`flex-row gap-3 mt-6`}> <View style={tw`flex-row gap-3 mt-6`}>

View file

@ -185,7 +185,7 @@ export default function BannerForm({
multiple={true} multiple={true}
label="Select Products" label="Select Products"
placeholder="Select products for banner (optional)" placeholder="Select products for banner (optional)"
labelFormat={(product) => `${product.name} (${product.price})`} labelFormat={(product) => `${product.productName} (₹${product.price})`}
/> />
</View> </View>

View file

@ -119,7 +119,7 @@ const ProductGroupForm: React.FC<ProductGroupFormProps> = ({
multiple={true} multiple={true}
label="Products" label="Products"
placeholder="Select products" placeholder="Select products"
labelFormat={(product) => `${product.name}${product.shortDescription ? ` - ${product.shortDescription}` : ''}`} labelFormat={(product) => product.label}
/> />
{/* Actions */} {/* Actions */}

View file

@ -9,6 +9,8 @@ interface SkuSummary {
productId: number; productId: number;
productName: string; productName: string;
label: string; label: string;
storeId: number | null;
price: string;
} }
interface Group { interface Group {

View file

@ -45,12 +45,12 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]); const [selectedImages, setSelectedImages] = useState<{ blob: Blob; mimeType: string }[]>([]);
const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]); const [displayImages, setDisplayImages] = useState<{ uri?: string }[]>([]);
// For edit mode, pre-select products belonging to this store // For edit mode, pre-select SKUs belonging to this store's products
const initialSelectedProducts = useMemo(() => { const initialSelectedProducts = useMemo(() => {
if (mode !== 'edit' || !productsData?.products) return []; if (mode !== 'edit' || !productsData?.products) return [];
return productsData.products return productsData.products
.filter(p => p.storeId === storeId) .filter(p => p.storeId === storeId)
.map(p => p.id); .flatMap(p => (p.skus || []).map(sku => sku.id));
}, [mode, productsData?.products, storeId]); }, [mode, productsData?.products, storeId]);
useEffect(() => { useEffect(() => {
@ -166,8 +166,8 @@ const StoreForm = forwardRef<StoreFormRef, StoreFormProps>((props, ref) => {
multiple={true} multiple={true}
label="Products" label="Products"
placeholder="Select products" placeholder="Select products"
isDisabled={(product) => product.storeId !== null && product.storeId !== storeId} isDisabled={(sku) => sku.storeId !== null && sku.storeId !== storeId}
labelFormat={(product) => `${product.name} - ₹${product.price}`} labelFormat={(sku) => `${sku.productName} - ₹${sku.price}`}
/> />
<View style={tw`mb-6`}> <View style={tw`mb-6`}>
<MyText style={tw`text-sm font-bold text-gray-700 mb-3 uppercase tracking-wider`}>Store Image</MyText> <MyText style={tw`text-sm font-bold text-gray-700 mb-3 uppercase tracking-wider`}>Store Image</MyText>

View file

@ -185,7 +185,7 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
multiple={true} multiple={true}
label="Select Products" label="Select Products"
placeholder="Select products" placeholder="Select products"
labelFormat={(product) => `${product.name} (${product.unit})`} labelFormat={(product) => product.label}
/> />
{formik.errors.skuIds && formik.touched.skuIds && ( {formik.errors.skuIds && formik.touched.skuIds && (
<MyText style={tw`text-red-500 text-sm mt-1`}>{formik.errors.skuIds}</MyText> <MyText style={tw`text-red-500 text-sm mt-1`}>{formik.errors.skuIds}</MyText>

View file

@ -123,21 +123,37 @@ const productValidationSchema = Yup.object().shape({
} }
return true return true
}) })
.test('quantity-feature', 'Every SKU must have a quantity feature', function (variants) { .test('quantity-feature', 'Each SKU must have exactly one quantity feature', function (variants) {
if (!Array.isArray(variants)) return true if (!Array.isArray(variants)) return true
for (const variant of variants) { for (const variant of variants) {
const attrs = variant?.attributes || [] const attrs = variant?.attributes || []
const hasQuantity = attrs.some( const quantityCount = attrs.filter(
(a) => ((a as Attribute)?.featureName ?? '').trim().toLowerCase() === 'quantity' (a) => ((a as Attribute)?.featureName ?? '').trim().toLowerCase() === 'quantity'
) ).length
if (!hasQuantity) { if (quantityCount === 0) {
return this.createError({ message: 'Every SKU must have a quantity feature' }) return this.createError({ message: 'Every SKU must have a quantity feature' })
} }
if (quantityCount > 1) {
return this.createError({ message: 'Each SKU can have only one quantity feature' })
}
} }
return true return true
}), }),
}) })
// Collect every string message from a Formik errors tree (objects, arrays, strings).
// Variant-level errors (e.g. 'Each SKU can have only one quantity feature') can get
// hidden by Formik when index-level errors are also present, so we walk everything.
const collectErrorMessages = (errors: unknown, depth = 0): string[] => {
if (depth > 6) return []
if (typeof errors === 'string') return [errors]
if (Array.isArray(errors)) return errors.flatMap((e) => collectErrorMessages(e, depth + 1))
if (errors && typeof errors === 'object') {
return Object.values(errors).flatMap((e) => collectErrorMessages(e, depth + 1))
}
return []
}
const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
mode, mode,
initialValues, initialValues,
@ -188,6 +204,20 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
initialValues={formInitialValues} initialValues={formInitialValues}
validationSchema={productValidationSchema} validationSchema={productValidationSchema}
onSubmit={(values) => { onSubmit={(values) => {
// Normalize feature names before sending to backend: the mandatory
// 'quantity' feature must always go out lowercase (case-insensitive
// matching means 'Quantity'/'QUANTITY' are treated the same), and
// names are trimmed like the backend does.
const normalizedValues: ProductFormData = {
...values,
variants: values.variants.map((v) => ({
...v,
attributes: v.attributes.map((a) => ({
...a,
featureName: isQuantityFeature(a) ? 'quantity' : (a.featureName ?? '').trim() || null,
})),
})),
}
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 }))
) )
@ -203,7 +233,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
}) })
}) })
} }
onSubmit(values, images, deletedKeys) onSubmit(normalizedValues, images, deletedKeys)
}} }}
enableReinitialize enableReinitialize
> >
@ -323,13 +353,24 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
<MyText style={tw`text-gray-600 text-xs ml-0.5`}>Add</MyText> <MyText style={tw`text-gray-600 text-xs ml-0.5`}>Add</MyText>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
{variant.attributes.map((attr, aIndex) => ( {variant.attributes.map((attr, aIndex) => {
const quantityCount = variant.attributes.filter(isQuantityFeature).length
// Quantity features are locked (non-editable), but if there
// are duplicates (e.g. 'quantity' + 'Quantity') the user must
// be able to delete the extras to fix the form.
const canDelete = variant.attributes.length > 1 && (!isQuantityFeature(attr) || quantityCount > 1)
return (
<View key={aIndex} style={tw`flex-row items-center mb-2 gap-2`}> <View key={aIndex} style={tw`flex-row items-center mb-2 gap-2`}>
<View style={tw`flex-1`}> <View style={tw`flex-1`}>
<MyTextInput <MyTextInput
placeholder="Name" placeholder="Name"
value={attr.featureName ?? ''} value={attr.featureName ?? ''}
onChangeText={handleChange(`variants.${vIndex}.attributes.${aIndex}.featureName`)} onChangeText={(text) =>
setFieldValue(
`variants.${vIndex}.attributes.${aIndex}.featureName`,
text.trim().toLowerCase() === 'quantity' ? 'quantity' : text
)
}
editable={!isQuantityFeature(attr)} editable={!isQuantityFeature(attr)}
style={isQuantityFeature(attr) ? tw`text-gray-400` : undefined} style={isQuantityFeature(attr) ? tw`text-gray-400` : undefined}
/> />
@ -341,13 +382,14 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
onChangeText={handleChange(`variants.${vIndex}.attributes.${aIndex}.featureValue`)} onChangeText={handleChange(`variants.${vIndex}.attributes.${aIndex}.featureValue`)}
/> />
</View> </View>
{variant.attributes.length > 1 && !isQuantityFeature(attr) && ( {canDelete && (
<TouchableOpacity onPress={() => removeAttr(aIndex)}> <TouchableOpacity onPress={() => removeAttr(aIndex)}>
<MaterialIcons name="close" size={18} color="#EF4444" /> <MaterialIcons name="close" size={18} color="#EF4444" />
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
))} )
})}
<View style={tw`flex-row items-center self-start`}> <View style={tw`flex-row items-center self-start`}>
<TouchableOpacity <TouchableOpacity
onPress={() => pushAttr(defaultAttribute())} onPress={() => pushAttr(defaultAttribute())}
@ -534,18 +576,10 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
onPress={async () => { onPress={async () => {
const validationErrors = await validateForm() const validationErrors = await validateForm()
if (Object.keys(validationErrors).length > 0) { if (Object.keys(validationErrors).length > 0) {
const variantsError = validationErrors.variants const variantsMessages = collectErrorMessages(validationErrors.variants)
const firstVariantErrors = Array.isArray(variantsError) const allMessages = collectErrorMessages(validationErrors)
? (variantsError[0] as Record<string, unknown> | undefined) || {} const message = variantsMessages[0] || allMessages[0] || 'Please fix the highlighted fields'
: {} Alert.alert('Check your form', String(message))
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 return
} }
handleSubmit() handleSubmit()
@ -554,7 +588,12 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
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'}`}
> >
<MyText style={tw`text-white text-lg font-bold`}> <MyText style={tw`text-white text-lg font-bold`}>
{isLoading ? 'Creating...' : 'Create Product'} {(() => {
if (mode === 'edit') {
return isLoading ? 'Saving...' : 'Save Changes'
}
return isLoading ? 'Creating...' : 'Create Product'
})()}
</MyText> </MyText>
</TouchableOpacity> </TouchableOpacity>
</ScrollView> </ScrollView>

View file

@ -606,31 +606,12 @@ export const productRouter = router({
getGroups: protectedProcedure getGroups: protectedProcedure
.query(async (): Promise<AdminProductGroupsResult> => { .query(async (): Promise<AdminProductGroupsResult> => {
// getAllProductGroupsInDb already returns groups with products mapped
// as AdminSku[] (SKU ids per the ProductsSelector).
const groups = await getAllProductGroupsInDb() const groups = await getAllProductGroupsInDb()
/*
// Old implementation - direct DB queries:
const groups = await db.query.productGroupInfo.findMany({
with: {
memberships: {
with: {
product: true,
},
},
},
orderBy: desc(productGroupInfo.createdAt),
});
*/
return { return {
groups: groups.map(group => ({ groups,
...group,
products: group.memberships.map((m: any) => ({
...(m.product as AdminProduct),
images: (m.product.images as string[]) || null,
})),
productCount: group.memberships.length,
})),
} }
}), }),

View file

@ -9,7 +9,7 @@ routes = [
[[d1_databases]] [[d1_databases]]
binding = "DB" binding = "DB"
database_name = "freshyo-backend-dev" database_name = "freshyo-backend-dev"
database_id = "b0c7a47d-3807-4e13-8b3e-fb43a45b02b2" database_id = "b2528bdb-0ec0-478e-8765-b118918153c0"
#database_name = "freshyo-dev" #database_name = "freshyo-dev"
#database_id = "3cad440f-dc14-4baa-ab05-04eb08e206de" #database_id = "3cad440f-dc14-4baa-ab05-04eb08e206de"
migrations_dir="../../packages/db_helper_sqlite/drizzle" migrations_dir="../../packages/db_helper_sqlite/drizzle"

View file

@ -187,6 +187,45 @@ DROP TABLE `coupon_applicable_products`;
ALTER TABLE `__new_coupon_applicable_products` RENAME TO `coupon_applicable_products`; ALTER TABLE `__new_coupon_applicable_products` RENAME TO `coupon_applicable_products`;
CREATE UNIQUE INDEX `unique_coupon_sku` ON `coupon_applicable_products` (`coupon_id`,`sku_id`); CREATE UNIQUE INDEX `unique_coupon_sku` ON `coupon_applicable_products` (`coupon_id`,`sku_id`);
-- 5b. Recreate tag + group memberships to reference product_skus.id (SKU ids)
-- instead of product_info.id. ProductsSelector now saves SKU ids into these.
CREATE TABLE `__new_product_tags` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`product_id` integer NOT NULL,
`tag_id` integer NOT NULL,
`assigned_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY (`product_id`) REFERENCES `product_skus`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`tag_id`) REFERENCES `product_tag_info`(`id`) ON UPDATE no action ON DELETE no action
);
INSERT INTO `__new_product_tags` (`id`, `product_id`, `tag_id`, `assigned_at`)
SELECT
`pt`.`id`, `m`.`sku_id`, `pt`.`tag_id`, `pt`.`assigned_at`
FROM `product_tags` `pt`
JOIN `__product_to_sku` `m` ON `m`.`product_id` = `pt`.`product_id`;
DROP TABLE `product_tags`;
ALTER TABLE `__new_product_tags` RENAME TO `product_tags`;
CREATE UNIQUE INDEX `unique_product_tag` ON `product_tags` (`product_id`,`tag_id`);
CREATE TABLE `__new_product_group_membership` (
`product_id` integer NOT NULL,
`group_id` integer NOT NULL,
`added_at` text DEFAULT CURRENT_TIMESTAMP NOT NULL,
PRIMARY KEY (`product_id`,`group_id`),
FOREIGN KEY (`product_id`) REFERENCES `product_skus`(`id`) ON UPDATE no action ON DELETE no action,
FOREIGN KEY (`group_id`) REFERENCES `product_group_info`(`id`) ON UPDATE no action ON DELETE no action
);
INSERT INTO `__new_product_group_membership` (`product_id`, `group_id`, `added_at`)
SELECT
`m`.`sku_id`, `pgm`.`group_id`, `pgm`.`added_at`
FROM `product_group_membership` `pgm`
JOIN `__product_to_sku` `m` ON `m`.`product_id` = `pgm`.`product_id`;
DROP TABLE `product_group_membership`;
ALTER TABLE `__new_product_group_membership` RENAME TO `product_group_membership`;
-- 6. Remap JSON product id arrays to sku id arrays (before renaming the columns). -- 6. Remap JSON product id arrays to sku id arrays (before renaming the columns).
UPDATE `delivery_slot_info` AS `target` UPDATE `delivery_slot_info` AS `target`
SET `product_ids` = COALESCE( SET `product_ids` = COALESCE(

View file

@ -660,19 +660,21 @@ export async function getAllProductTags(): Promise<AdminProductTagWithProducts[]
with: { with: {
products: { products: {
with: { with: {
product: true, sku: {
with: { features: true, marketStats: true },
},
}, },
}, },
}, },
}) as Array<ProductTagInfoRow & { products: Array<ProductTagRow & { product: ProductRow }> }> }) as Array<ProductTagInfoRow & { products: Array<ProductTagRow & { sku: SkuRow & { features: SkuFeatureRow[]; marketStats: MarketStatsRow | null } }> }>
return tags.map((tag: ProductTagInfoRow & { products: Array<ProductTagRow & { product: ProductRow }> }) => ({ return tags.map((tag: any) => ({
...mapTagInfo(tag), ...mapTagInfo(tag),
products: tag.products.map((assignment: ProductTagRow & { product: ProductRow }) => ({ products: tag.products.map((assignment: any) => ({
productId: assignment.productId, productId: assignment.productId,
tagId: assignment.tagId, tagId: assignment.tagId,
assignedAt: assignment.assignedAt, assignedAt: assignment.assignedAt,
product: mapProduct(assignment.product), product: mapSku(assignment.sku, assignment.sku.features, [], assignment.sku.marketStats),
})), })),
productIds: tag.products.map((assignment: ProductTagRow) => assignment.productId), productIds: tag.products.map((assignment: ProductTagRow) => assignment.productId),
})) }))
@ -730,7 +732,9 @@ export async function getProductTagById(tagId: number): Promise<AdminProductTagW
with: { with: {
products: { products: {
with: { with: {
product: true, sku: {
with: { features: true, marketStats: true },
},
}, },
}, },
}, },
@ -742,13 +746,13 @@ export async function getProductTagById(tagId: number): Promise<AdminProductTagW
return { return {
...mapTagInfo(tag), ...mapTagInfo(tag),
products: tag.products.map((assignment: ProductTagRow & { product: ProductRow }) => ({ products: (tag.products || []).map((assignment: any) => ({
productId: assignment.productId, productId: assignment.productId,
tagId: assignment.tagId, tagId: assignment.tagId,
assignedAt: assignment.assignedAt, assignedAt: assignment.assignedAt,
product: mapProduct(assignment.product), product: mapSku(assignment.sku, assignment.sku.features, [], assignment.sku.marketStats),
})), })),
productIds: tag.products.map((assignment: ProductTagRow) => assignment.productId), productIds: (tag.products || []).map((assignment: ProductTagRow) => assignment.productId),
} }
} }
@ -776,7 +780,9 @@ export async function updateProductTag(tagId: number, input: UpdateProductTagInp
with: { with: {
products: { products: {
with: { with: {
product: true, sku: {
with: { features: true, marketStats: true },
},
}, },
}, },
}, },
@ -784,13 +790,13 @@ export async function updateProductTag(tagId: number, input: UpdateProductTagInp
return { return {
...mapTagInfo(tag), ...mapTagInfo(tag),
products: fullTag?.products.map((assignment: ProductTagRow & { product: ProductRow }) => ({ products: (fullTag?.products || []).map((assignment: any) => ({
productId: assignment.productId, productId: assignment.productId,
tagId: assignment.tagId, tagId: assignment.tagId,
assignedAt: assignment.assignedAt, assignedAt: assignment.assignedAt,
product: mapProduct(assignment.product), product: mapSku(assignment.sku, assignment.sku.features, [], assignment.sku.marketStats),
})) || [], })),
productIds: fullTag?.products.map((assignment: ProductTagRow) => assignment.productId) || [], productIds: (fullTag?.products || []).map((assignment: ProductTagRow) => assignment.productId) || [],
} }
} }
@ -906,7 +912,9 @@ export async function getAllProductGroups() {
with: { with: {
memberships: { memberships: {
with: { with: {
product: true, sku: {
with: { features: true, marketStats: true },
},
}, },
}, },
}, },
@ -918,7 +926,9 @@ export async function getAllProductGroups() {
groupName: group.groupName, groupName: group.groupName,
description: group.description ?? null, description: group.description ?? null,
createdAt: group.createdAt, createdAt: group.createdAt,
products: group.memberships.map((membership: any) => mapProduct(membership.product)), products: (group.memberships || []).map((membership: any) =>
mapSku(membership.sku, membership.sku.features, [], membership.sku.marketStats)
),
productCount: group.memberships.length, productCount: group.memberships.length,
memberships: group.memberships memberships: group.memberships
})) }))

View file

@ -1,5 +1,5 @@
import { db } from '../db/db_index' import { db } from '../db/db_index'
import { storeInfo, productInfo } from '../db/schema' import { storeInfo, productInfo, productSkus } from '../db/schema'
import { eq, inArray } from 'drizzle-orm' import { eq, inArray } from 'drizzle-orm'
import { runBatched } from '../lib/run-batched' import { runBatched } from '../lib/run-batched'
@ -57,12 +57,23 @@ export async function createStore(
.returning() .returning()
if (products && products.length > 0) { if (products && products.length > 0) {
await runBatched(tx, products, 10, async (t, chunk) => { // ProductsSelector sends SKU ids — resolve to the owning product ids
await t // (product_info.storeId is a product-level field).
.update(productInfo) const productIds = products.length > 0
.set({ storeId: newStore.id }) ? await tx.query.productSkus.findMany({
.where(inArray(productInfo.id, chunk)) where: inArray(productSkus.id, products),
}) columns: { productId: true },
}).then((rows) => [...new Set(rows.map((r) => r.productId))])
: []
if (productIds.length > 0) {
await runBatched(tx, productIds, 10, async (t, chunk) => {
await t
.update(productInfo)
.set({ storeId: newStore.id })
.where(inArray(productInfo.id, chunk))
})
}
} }
return { return {
@ -109,8 +120,16 @@ export async function updateStore(
.set({ storeId: null }) .set({ storeId: null })
.where(eq(productInfo.storeId, id)) .where(eq(productInfo.storeId, id))
if (products.length > 0) { // ProductsSelector sends SKU ids — resolve to the owning product ids.
await runBatched(tx, products, 10, async (t, chunk) => { const productIds = products.length > 0
? await tx.query.productSkus.findMany({
where: inArray(productSkus.id, products),
columns: { productId: true },
}).then((rows) => [...new Set(rows.map((r) => r.productId))])
: []
if (productIds.length > 0) {
await runBatched(tx, productIds, 10, async (t, chunk) => {
await t await t
.update(productInfo) .update(productInfo)
.set({ storeId: id }) .set({ storeId: id })

View file

@ -244,7 +244,7 @@ export const productGroupInfo = sqliteTable('product_group_info', {
}) })
export const productGroupMembership = sqliteTable('product_group_membership', { export const productGroupMembership = sqliteTable('product_group_membership', {
productId: integer('product_id').notNull().references(() => productInfo.id), productId: integer('product_id').notNull().references(() => productSkus.id),
groupId: integer('group_id').notNull().references(() => productGroupInfo.id), groupId: integer('group_id').notNull().references(() => productGroupInfo.id),
addedAt: timestampText('added_at').notNull().default(sql`CURRENT_TIMESTAMP`), addedAt: timestampText('added_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}, (t) => ({ }, (t) => ({
@ -298,7 +298,7 @@ export const productTagInfo = sqliteTable('product_tag_info', {
export const productTags = sqliteTable('product_tags', { export const productTags = sqliteTable('product_tags', {
id: integer().primaryKey({ autoIncrement: true }), id: integer().primaryKey({ autoIncrement: true }),
productId: integer('product_id').notNull().references(() => productInfo.id), productId: integer('product_id').notNull().references(() => productSkus.id),
tagId: integer('tag_id').notNull().references(() => productTagInfo.id), tagId: integer('tag_id').notNull().references(() => productTagInfo.id),
assignedAt: timestampText('assigned_at').notNull().default(sql`CURRENT_TIMESTAMP`), assignedAt: timestampText('assigned_at').notNull().default(sql`CURRENT_TIMESTAMP`),
}, (t) => ({ }, (t) => ({
@ -630,7 +630,7 @@ export const productTagInfoRelations = relations(productTagInfo, ({ many }) => (
})) }))
export const productTagsRelations = relations(productTags, ({ one }) => ({ export const productTagsRelations = relations(productTags, ({ one }) => ({
product: one(productInfo, { fields: [productTags.productId], references: [productInfo.id] }), sku: one(productSkus, { fields: [productTags.productId], references: [productSkus.id] }),
tag: one(productTagInfo, { fields: [productTags.tagId], references: [productTagInfo.id] }), tag: one(productTagInfo, { fields: [productTags.tagId], references: [productTagInfo.id] }),
})) }))
@ -760,7 +760,7 @@ export const productGroupInfoRelations = relations(productGroupInfo, ({ many })
})) }))
export const productGroupMembershipRelations = relations(productGroupMembership, ({ one }) => ({ export const productGroupMembershipRelations = relations(productGroupMembership, ({ one }) => ({
product: one(productInfo, { fields: [productGroupMembership.productId], references: [productInfo.id] }), sku: one(productSkus, { fields: [productGroupMembership.productId], references: [productSkus.id] }),
group: one(productGroupInfo, { fields: [productGroupMembership.groupId], references: [productGroupInfo.id] }), group: one(productGroupInfo, { fields: [productGroupMembership.groupId], references: [productGroupInfo.id] }),
})) }))

View file

@ -242,7 +242,8 @@ export async function getAllProductsWithUnits(tagId?: number): Promise<ProductSu
if (sku.marketStats?.isSuspended) return false if (sku.marketStats?.isSuspended) return false
if (sku.isDeleted) return false if (sku.isDeleted) return false
if (!tagId) return true if (!tagId) return true
return taggedProductIdSet.has(sku.productId) // product_tags.product_id now holds SKU ids — compare against sku.id
return taggedProductIdSet.has(sku.id)
}) })
.map((sku) => { .map((sku) => {
const features = sku.features || [] const features = sku.features || []
@ -306,6 +307,8 @@ export interface SkuSummary {
productName: string productName: string
label: string label: string
images: unknown images: unknown
storeId: number | null
price: string
} }
export async function getAllSkusSummary(): Promise<SkuSummary[]> { export async function getAllSkusSummary(): Promise<SkuSummary[]> {
@ -314,7 +317,7 @@ export async function getAllSkusSummary(): Promise<SkuSummary[]> {
features: true, features: true,
marketStats: true, marketStats: true,
product: { product: {
columns: { name: true }, columns: { name: true, storeId: true },
}, },
}, },
}) })
@ -330,6 +333,8 @@ export async function getAllSkusSummary(): Promise<SkuSummary[]> {
productName: sku.product?.name ?? 'Unknown', productName: sku.product?.name ?? 'Unknown',
label, label,
images: sku.images, images: sku.images,
storeId: sku.product?.storeId ?? null,
price: sku.marketStats?.ourPrice ? String(sku.marketStats.ourPrice) : '0',
} }
}) })
} }

View file

@ -464,7 +464,7 @@ export interface AdminProductTagAssignment {
productId: number; productId: number;
tagId: number; tagId: number;
assignedAt: Date; assignedAt: Date;
product: AdminProduct; product: AdminSku;
} }
export interface AdminProductTagWithProducts extends AdminProductTagInfo { export interface AdminProductTagWithProducts extends AdminProductTagInfo {
@ -539,7 +539,7 @@ export interface AdminProductGroup {
groupName: string; groupName: string;
description: string | null; description: string | null;
createdAt: Date; createdAt: Date;
products: AdminProduct[]; products: AdminSku[];
productCount: number; productCount: number;
} }