Compare commits
No commits in common. "21002332fb6720c947583cbc06bbf933803c8d27" and "d1ffdfbc2154b5dbed72e33d42c6540e1a7c9166" have entirely different histories.
21002332fb
...
d1ffdfbc21
15 changed files with 86 additions and 181 deletions
|
|
@ -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.productName} - ₹${product.price}`}
|
labelFormat={(product) => `${product.name} - ₹${product.price}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View style={tw`flex-row gap-3 mt-6`}>
|
<View style={tw`flex-row gap-3 mt-6`}>
|
||||||
|
|
|
||||||
|
|
@ -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.productName} (₹${product.price})`}
|
labelFormat={(product) => `${product.name} (${product.price})`}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.label}
|
labelFormat={(product) => `${product.name}${product.shortDescription ? ` - ${product.shortDescription}` : ''}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@ interface SkuSummary {
|
||||||
productId: number;
|
productId: number;
|
||||||
productName: string;
|
productName: string;
|
||||||
label: string;
|
label: string;
|
||||||
storeId: number | null;
|
|
||||||
price: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Group {
|
interface Group {
|
||||||
|
|
|
||||||
|
|
@ -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 SKUs belonging to this store's products
|
// For edit mode, pre-select products belonging to this store
|
||||||
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)
|
||||||
.flatMap(p => (p.skus || []).map(sku => sku.id));
|
.map(p => p.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={(sku) => sku.storeId !== null && sku.storeId !== storeId}
|
isDisabled={(product) => product.storeId !== null && product.storeId !== storeId}
|
||||||
labelFormat={(sku) => `${sku.productName} - ₹${sku.price}`}
|
labelFormat={(product) => `${product.name} - ₹${product.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>
|
||||||
|
|
|
||||||
|
|
@ -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.label}
|
labelFormat={(product) => `${product.name} (${product.unit})`}
|
||||||
/>
|
/>
|
||||||
{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>
|
||||||
|
|
|
||||||
|
|
@ -123,37 +123,21 @@ const productValidationSchema = Yup.object().shape({
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
.test('quantity-feature', 'Each SKU must have exactly one quantity feature', function (variants) {
|
.test('quantity-feature', 'Every SKU must have a 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 quantityCount = attrs.filter(
|
const hasQuantity = attrs.some(
|
||||||
(a) => ((a as Attribute)?.featureName ?? '').trim().toLowerCase() === 'quantity'
|
(a) => ((a as Attribute)?.featureName ?? '').trim().toLowerCase() === 'quantity'
|
||||||
).length
|
)
|
||||||
if (quantityCount === 0) {
|
if (!hasQuantity) {
|
||||||
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,
|
||||||
|
|
@ -204,20 +188,6 @@ 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 }))
|
||||||
)
|
)
|
||||||
|
|
@ -233,7 +203,7 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onSubmit(normalizedValues, images, deletedKeys)
|
onSubmit(values, images, deletedKeys)
|
||||||
}}
|
}}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
>
|
>
|
||||||
|
|
@ -353,24 +323,13 @@ 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={(text) =>
|
onChangeText={handleChange(`variants.${vIndex}.attributes.${aIndex}.featureName`)}
|
||||||
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}
|
||||||
/>
|
/>
|
||||||
|
|
@ -382,14 +341,13 @@ const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
|
||||||
onChangeText={handleChange(`variants.${vIndex}.attributes.${aIndex}.featureValue`)}
|
onChangeText={handleChange(`variants.${vIndex}.attributes.${aIndex}.featureValue`)}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
{canDelete && (
|
{variant.attributes.length > 1 && !isQuantityFeature(attr) && (
|
||||||
<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())}
|
||||||
|
|
@ -576,10 +534,18 @@ 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 variantsMessages = collectErrorMessages(validationErrors.variants)
|
const variantsError = validationErrors.variants
|
||||||
const allMessages = collectErrorMessages(validationErrors)
|
const firstVariantErrors = Array.isArray(variantsError)
|
||||||
const message = variantsMessages[0] || allMessages[0] || 'Please fix the highlighted fields'
|
? (variantsError[0] as Record<string, unknown> | undefined) || {}
|
||||||
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()
|
||||||
|
|
@ -588,12 +554,7 @@ 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>
|
||||||
|
|
|
||||||
|
|
@ -606,12 +606,31 @@ 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: groups.map(group => ({
|
||||||
|
...group,
|
||||||
|
products: group.memberships.map((m: any) => ({
|
||||||
|
...(m.product as AdminProduct),
|
||||||
|
images: (m.product.images as string[]) || null,
|
||||||
|
})),
|
||||||
|
productCount: group.memberships.length,
|
||||||
|
})),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 = "b2528bdb-0ec0-478e-8765-b118918153c0"
|
database_id = "b0c7a47d-3807-4e13-8b3e-fb43a45b02b2"
|
||||||
#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"
|
||||||
|
|
|
||||||
|
|
@ -187,45 +187,6 @@ 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(
|
||||||
|
|
|
||||||
|
|
@ -660,21 +660,19 @@ export async function getAllProductTags(): Promise<AdminProductTagWithProducts[]
|
||||||
with: {
|
with: {
|
||||||
products: {
|
products: {
|
||||||
with: {
|
with: {
|
||||||
sku: {
|
product: true,
|
||||||
with: { features: true, marketStats: true },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}) as Array<ProductTagInfoRow & { products: Array<ProductTagRow & { sku: SkuRow & { features: SkuFeatureRow[]; marketStats: MarketStatsRow | null } }> }>
|
}) as Array<ProductTagInfoRow & { products: Array<ProductTagRow & { product: ProductRow }> }>
|
||||||
|
|
||||||
return tags.map((tag: any) => ({
|
return tags.map((tag: ProductTagInfoRow & { products: Array<ProductTagRow & { product: ProductRow }> }) => ({
|
||||||
...mapTagInfo(tag),
|
...mapTagInfo(tag),
|
||||||
products: tag.products.map((assignment: any) => ({
|
products: tag.products.map((assignment: ProductTagRow & { product: ProductRow }) => ({
|
||||||
productId: assignment.productId,
|
productId: assignment.productId,
|
||||||
tagId: assignment.tagId,
|
tagId: assignment.tagId,
|
||||||
assignedAt: assignment.assignedAt,
|
assignedAt: assignment.assignedAt,
|
||||||
product: mapSku(assignment.sku, assignment.sku.features, [], assignment.sku.marketStats),
|
product: mapProduct(assignment.product),
|
||||||
})),
|
})),
|
||||||
productIds: tag.products.map((assignment: ProductTagRow) => assignment.productId),
|
productIds: tag.products.map((assignment: ProductTagRow) => assignment.productId),
|
||||||
}))
|
}))
|
||||||
|
|
@ -732,9 +730,7 @@ export async function getProductTagById(tagId: number): Promise<AdminProductTagW
|
||||||
with: {
|
with: {
|
||||||
products: {
|
products: {
|
||||||
with: {
|
with: {
|
||||||
sku: {
|
product: true,
|
||||||
with: { features: true, marketStats: true },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -746,13 +742,13 @@ export async function getProductTagById(tagId: number): Promise<AdminProductTagW
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...mapTagInfo(tag),
|
...mapTagInfo(tag),
|
||||||
products: (tag.products || []).map((assignment: any) => ({
|
products: tag.products.map((assignment: ProductTagRow & { product: ProductRow }) => ({
|
||||||
productId: assignment.productId,
|
productId: assignment.productId,
|
||||||
tagId: assignment.tagId,
|
tagId: assignment.tagId,
|
||||||
assignedAt: assignment.assignedAt,
|
assignedAt: assignment.assignedAt,
|
||||||
product: mapSku(assignment.sku, assignment.sku.features, [], assignment.sku.marketStats),
|
product: mapProduct(assignment.product),
|
||||||
})),
|
})),
|
||||||
productIds: (tag.products || []).map((assignment: ProductTagRow) => assignment.productId),
|
productIds: tag.products.map((assignment: ProductTagRow) => assignment.productId),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -780,9 +776,7 @@ export async function updateProductTag(tagId: number, input: UpdateProductTagInp
|
||||||
with: {
|
with: {
|
||||||
products: {
|
products: {
|
||||||
with: {
|
with: {
|
||||||
sku: {
|
product: true,
|
||||||
with: { features: true, marketStats: true },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -790,13 +784,13 @@ export async function updateProductTag(tagId: number, input: UpdateProductTagInp
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...mapTagInfo(tag),
|
...mapTagInfo(tag),
|
||||||
products: (fullTag?.products || []).map((assignment: any) => ({
|
products: fullTag?.products.map((assignment: ProductTagRow & { product: ProductRow }) => ({
|
||||||
productId: assignment.productId,
|
productId: assignment.productId,
|
||||||
tagId: assignment.tagId,
|
tagId: assignment.tagId,
|
||||||
assignedAt: assignment.assignedAt,
|
assignedAt: assignment.assignedAt,
|
||||||
product: mapSku(assignment.sku, assignment.sku.features, [], assignment.sku.marketStats),
|
product: mapProduct(assignment.product),
|
||||||
})),
|
})) || [],
|
||||||
productIds: (fullTag?.products || []).map((assignment: ProductTagRow) => assignment.productId) || [],
|
productIds: fullTag?.products.map((assignment: ProductTagRow) => assignment.productId) || [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -912,9 +906,7 @@ export async function getAllProductGroups() {
|
||||||
with: {
|
with: {
|
||||||
memberships: {
|
memberships: {
|
||||||
with: {
|
with: {
|
||||||
sku: {
|
product: true,
|
||||||
with: { features: true, marketStats: true },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -926,9 +918,7 @@ 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) =>
|
products: group.memberships.map((membership: any) => mapProduct(membership.product)),
|
||||||
mapSku(membership.sku, membership.sku.features, [], membership.sku.marketStats)
|
|
||||||
),
|
|
||||||
productCount: group.memberships.length,
|
productCount: group.memberships.length,
|
||||||
memberships: group.memberships
|
memberships: group.memberships
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { db } from '../db/db_index'
|
import { db } from '../db/db_index'
|
||||||
import { storeInfo, productInfo, productSkus } from '../db/schema'
|
import { storeInfo, productInfo } from '../db/schema'
|
||||||
import { eq, inArray } from 'drizzle-orm'
|
import { eq, inArray } from 'drizzle-orm'
|
||||||
import { runBatched } from '../lib/run-batched'
|
import { runBatched } from '../lib/run-batched'
|
||||||
|
|
||||||
|
|
@ -57,23 +57,12 @@ export async function createStore(
|
||||||
.returning()
|
.returning()
|
||||||
|
|
||||||
if (products && products.length > 0) {
|
if (products && products.length > 0) {
|
||||||
// ProductsSelector sends SKU ids — resolve to the owning product ids
|
await runBatched(tx, products, 10, async (t, chunk) => {
|
||||||
// (product_info.storeId is a product-level field).
|
await t
|
||||||
const productIds = products.length > 0
|
.update(productInfo)
|
||||||
? await tx.query.productSkus.findMany({
|
.set({ storeId: newStore.id })
|
||||||
where: inArray(productSkus.id, products),
|
.where(inArray(productInfo.id, chunk))
|
||||||
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 {
|
||||||
|
|
@ -120,16 +109,8 @@ export async function updateStore(
|
||||||
.set({ storeId: null })
|
.set({ storeId: null })
|
||||||
.where(eq(productInfo.storeId, id))
|
.where(eq(productInfo.storeId, id))
|
||||||
|
|
||||||
// ProductsSelector sends SKU ids — resolve to the owning product ids.
|
if (products.length > 0) {
|
||||||
const productIds = products.length > 0
|
await runBatched(tx, products, 10, async (t, chunk) => {
|
||||||
? 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 })
|
||||||
|
|
|
||||||
|
|
@ -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(() => productSkus.id),
|
productId: integer('product_id').notNull().references(() => productInfo.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(() => productSkus.id),
|
productId: integer('product_id').notNull().references(() => productInfo.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 }) => ({
|
||||||
sku: one(productSkus, { fields: [productTags.productId], references: [productSkus.id] }),
|
product: one(productInfo, { fields: [productTags.productId], references: [productInfo.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 }) => ({
|
||||||
sku: one(productSkus, { fields: [productGroupMembership.productId], references: [productSkus.id] }),
|
product: one(productInfo, { fields: [productGroupMembership.productId], references: [productInfo.id] }),
|
||||||
group: one(productGroupInfo, { fields: [productGroupMembership.groupId], references: [productGroupInfo.id] }),
|
group: one(productGroupInfo, { fields: [productGroupMembership.groupId], references: [productGroupInfo.id] }),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -242,8 +242,7 @@ 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
|
||||||
// product_tags.product_id now holds SKU ids — compare against sku.id
|
return taggedProductIdSet.has(sku.productId)
|
||||||
return taggedProductIdSet.has(sku.id)
|
|
||||||
})
|
})
|
||||||
.map((sku) => {
|
.map((sku) => {
|
||||||
const features = sku.features || []
|
const features = sku.features || []
|
||||||
|
|
@ -307,8 +306,6 @@ 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[]> {
|
||||||
|
|
@ -317,7 +314,7 @@ export async function getAllSkusSummary(): Promise<SkuSummary[]> {
|
||||||
features: true,
|
features: true,
|
||||||
marketStats: true,
|
marketStats: true,
|
||||||
product: {
|
product: {
|
||||||
columns: { name: true, storeId: true },
|
columns: { name: true },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -333,8 +330,6 @@ 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',
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -464,7 +464,7 @@ export interface AdminProductTagAssignment {
|
||||||
productId: number;
|
productId: number;
|
||||||
tagId: number;
|
tagId: number;
|
||||||
assignedAt: Date;
|
assignedAt: Date;
|
||||||
product: AdminSku;
|
product: AdminProduct;
|
||||||
}
|
}
|
||||||
|
|
||||||
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: AdminSku[];
|
products: AdminProduct[];
|
||||||
productCount: number;
|
productCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue