From 6a0bc18a8d4671d22f9a75f8e4fd4e1606832f52 Mon Sep 17 00:00:00 2001
From: shafi54 <108669266+shafi-aviz@users.noreply.github.com>
Date: Sat, 8 Aug 2026 19:59:45 +0530
Subject: [PATCH] enh
---
.commandcode/settings.json | 5 +-
.../app/(drawer)/product-tags/_layout.tsx | 1 +
.../app/(drawer)/product-tags/add.tsx | 3 +
.../app/(drawer)/product-tags/edit/index.tsx | 7 +
.../app/(drawer)/product-tags/index.tsx | 17 +-
.../app/(drawer)/product-tags/order.tsx | 369 ++++++++++++++++
apps/admin-ui/src/components/TagForm.tsx | 214 +++++++++-
apps/backend/src/lib/const-keys.ts | 4 +
apps/backend/src/sqliteImporter.ts | 1 +
.../src/trpc/apis/admin-apis/apis/product.ts | 24 +-
.../src/trpc/apis/common-apis/common.ts | 39 +-
apps/backend/wrangler.dev.toml | 2 +-
.../app/(drawer)/(tabs)/home/index.tsx | 402 ++++++++++--------
.../drizzle/0002_sku_split.sql | 3 +
packages/db_helper_sqlite/index.ts | 1 +
.../src/admin-apis/product.ts | 24 ++
packages/db_helper_sqlite/src/db/schema.ts | 1 +
.../src/stores/store-helpers.ts | 2 +
packages/shared/types/admin.ts | 2 +
19 files changed, 909 insertions(+), 212 deletions(-)
create mode 100644 apps/admin-ui/app/(drawer)/product-tags/order.tsx
diff --git a/.commandcode/settings.json b/.commandcode/settings.json
index b8291e3..187814d 100644
--- a/.commandcode/settings.json
+++ b/.commandcode/settings.json
@@ -1,7 +1,10 @@
{
"permissions": {
"allow": [
- "Shell(npx tsc --noEmit --skipLibCheck src/trpc/apis/common-apis/common.ts 2 >& 1)"
+ "Shell(npx tsc --noEmit --skipLibCheck src/trpc/apis/common-apis/common.ts 2 >& 1)",
+ "Shell(npx tsc --noEmit 2 >& 1)",
+ "Shell(grep:*)",
+ "Shell(npx tsc --noEmit -p packages/shared/tsconfig.json 2 >& 1)"
],
"deny": [],
"defaultMode": "default"
diff --git a/apps/admin-ui/app/(drawer)/product-tags/_layout.tsx b/apps/admin-ui/app/(drawer)/product-tags/_layout.tsx
index 4181014..9b37411 100644
--- a/apps/admin-ui/app/(drawer)/product-tags/_layout.tsx
+++ b/apps/admin-ui/app/(drawer)/product-tags/_layout.tsx
@@ -6,6 +6,7 @@ export default function Layout() {
+
);
}
\ No newline at end of file
diff --git a/apps/admin-ui/app/(drawer)/product-tags/add.tsx b/apps/admin-ui/app/(drawer)/product-tags/add.tsx
index 8de7bd7..32b8c34 100644
--- a/apps/admin-ui/app/(drawer)/product-tags/add.tsx
+++ b/apps/admin-ui/app/(drawer)/product-tags/add.tsx
@@ -11,6 +11,7 @@ interface TagFormData {
tagDescription: string;
isDashboardTag: boolean;
relatedStores: number[];
+ productIds: number[];
}
export default function AddTag() {
@@ -45,6 +46,7 @@ export default function AddTag() {
imageUrl,
isDashboardTag: values.isDashboardTag,
relatedStores: values.relatedStores,
+ productIds: values.productIds,
uploadUrls,
})
@@ -66,6 +68,7 @@ export default function AddTag() {
tagDescription: '',
isDashboardTag: false,
relatedStores: [],
+ productIds: [],
};
return (
diff --git a/apps/admin-ui/app/(drawer)/product-tags/edit/index.tsx b/apps/admin-ui/app/(drawer)/product-tags/edit/index.tsx
index f9e0e62..28d7d63 100644
--- a/apps/admin-ui/app/(drawer)/product-tags/edit/index.tsx
+++ b/apps/admin-ui/app/(drawer)/product-tags/edit/index.tsx
@@ -11,6 +11,7 @@ interface TagFormData {
tagDescription: string;
isDashboardTag: boolean;
relatedStores: number[];
+ productIds: number[];
existingImageUrl?: string;
}
@@ -58,6 +59,7 @@ export default function EditTag() {
imageUrl,
isDashboardTag: values.isDashboardTag,
relatedStores: values.relatedStores,
+ productIds: values.productIds,
uploadUrls,
})
@@ -95,11 +97,16 @@ export default function EditTag() {
}
const tag = tagData.tag;
+ const tagProductIds = tag.productIds || (tag.products || []).map((p: any) => p.productId);
+ // Order by the saved sortOrder (fall back to the join order if empty).
+ const orderedProductIds = (tag.sortOrder || []).filter((id: number) => tagProductIds.includes(id));
+ const remainingProductIds = tagProductIds.filter((id: number) => !orderedProductIds.includes(id));
const initialValues: TagFormData = {
tagName: tag.tagName,
tagDescription: tag.tagDescription || '',
isDashboardTag: tag.isDashboardTag,
relatedStores: Array.isArray(tag.relatedStores) ? tag.relatedStores : [],
+ productIds: [...orderedProductIds, ...remainingProductIds],
existingImageUrl: tag.imageUrl || undefined,
};
diff --git a/apps/admin-ui/app/(drawer)/product-tags/index.tsx b/apps/admin-ui/app/(drawer)/product-tags/index.tsx
index d2b6d8f..d520c2b 100644
--- a/apps/admin-ui/app/(drawer)/product-tags/index.tsx
+++ b/apps/admin-ui/app/(drawer)/product-tags/index.tsx
@@ -53,11 +53,18 @@ const TagItem: React.FC = ({ item, onDeleteSuccess }) => (
interface TagHeaderProps {
onAddNewTag: () => void;
+ onOrderTags: () => void;
}
-const TagHeader: React.FC = ({ onAddNewTag }) => (
+const TagHeader: React.FC = ({ onAddNewTag, onOrderTags }) => (
- Product Tags
+
+
+ Tag Orders
+
{
+ router.push('/product-tags/order');
+ };
+
if (isLoading) {
@@ -127,7 +138,7 @@ export default function ProductTags() {
refreshControl={
}
- ListHeaderComponent={}
+ ListHeaderComponent={}
contentContainerStyle={tw`pb-4`}
ListEmptyComponent={
diff --git a/apps/admin-ui/app/(drawer)/product-tags/order.tsx b/apps/admin-ui/app/(drawer)/product-tags/order.tsx
new file mode 100644
index 0000000..7b542f4
--- /dev/null
+++ b/apps/admin-ui/app/(drawer)/product-tags/order.tsx
@@ -0,0 +1,369 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import {
+ View,
+ Alert,
+ ActivityIndicator,
+ Dimensions,
+ StyleSheet,
+} from 'react-native';
+import { TouchableOpacity } from 'react-native-gesture-handler';
+import { Image } from 'expo-image';
+import DraggableFlatList, {
+ ScaleDecorator,
+} from 'react-native-draggable-flatlist';
+import {
+ AppContainer,
+ MyText,
+ tw,
+ MyTouchableOpacity,
+} from 'common-ui';
+import { useRouter } from 'expo-router';
+import { trpc } from '../../../src/trpc-client';
+import MaterialIcons from '@expo/vector-icons/MaterialIcons';
+import { useQueryClient } from '@tanstack/react-query';
+
+const { width: screenWidth } = Dimensions.get('window');
+const itemWidth = screenWidth - 48;
+const itemHeight = 80;
+
+interface Tag {
+ id: number;
+ tagName: string;
+ imageUrl: string | null;
+}
+
+interface TagItemProps {
+ item: Tag;
+ drag: () => void;
+ isActive: boolean;
+}
+
+const TagItem: React.FC = ({ item, drag, isActive }) => {
+ return (
+
+
+ {/* Drag Handle */}
+
+
+
+
+ {/* Tag Image */}
+ {item.imageUrl ? (
+
+ ) : (
+
+
+
+ )}
+
+ {/* Tag Info */}
+
+
+ {item.tagName}
+
+
+
+
+ );
+};
+
+export default function TagOrders() {
+ const router = useRouter();
+ const queryClient = useQueryClient();
+ const [tags, setTags] = useState([]);
+ const [hasChanges, setHasChanges] = useState(false);
+
+ // Get current order from constants
+ const { data: constants, isLoading: isLoadingConstants, error: constantsError } = trpc.admin.const.getConstants.useQuery();
+ const { data: tagsData, isLoading: isLoadingTags, error: tagsError } = trpc.admin.product.getProductTags.useQuery();
+ const updateConstants = trpc.admin.const.updateConstants.useMutation();
+
+ // Initialize tags from the tagsOrder constant
+ useEffect(() => {
+ if (tagsData?.tags) {
+ const tagsOrderConstant = constants?.find(c => c.key === 'tagsOrder');
+
+ let orderedIds: number[] = [];
+
+ if (tagsOrderConstant) {
+ const value = tagsOrderConstant.value;
+
+ if (Array.isArray(value)) {
+ orderedIds = value.map((id: any) => parseInt(id));
+ } else if (typeof value === 'string') {
+ orderedIds = value.split(',').map((id: string) => parseInt(id.trim())).filter(id => !isNaN(id));
+ }
+ }
+
+ // Create tag map for quick lookup
+ const tagMap = new Map(tagsData.tags.map(t => [t.id, t]));
+
+ // Sort tags based on order, tags not in order go to end
+ const sortedTags: Tag[] = [];
+
+ // First add tags in the specified order
+ for (const id of orderedIds) {
+ const tag = tagMap.get(id);
+ if (tag) {
+ sortedTags.push({
+ id: tag.id,
+ tagName: tag.tagName,
+ imageUrl: tag.imageUrl || null,
+ });
+ tagMap.delete(id);
+ }
+ }
+
+ // Then add remaining tags (not in order yet)
+ for (const tag of tagMap.values()) {
+ sortedTags.push({
+ id: tag.id,
+ tagName: tag.tagName,
+ imageUrl: tag.imageUrl || null,
+ });
+ }
+
+ setTags(sortedTags);
+ }
+ }, [constants, tagsData]);
+
+ const handleDragEnd = useCallback(({ data }: { data: Tag[] }) => {
+ setTags(data);
+ setHasChanges(true);
+ }, []);
+
+ const renderItem = useCallback(({ item, drag, isActive }: { item: Tag; drag: () => void; isActive: boolean }) => {
+ return (
+
+ );
+ }, []);
+
+ const handleSave = () => {
+ const tagIds = tags.map(t => t.id);
+
+ updateConstants.mutate(
+ {
+ constants: [{
+ key: 'tagsOrder',
+ value: tagIds
+ }]
+ },
+ {
+ onSuccess: () => {
+ setHasChanges(false);
+ Alert.alert('Success', 'Tag order updated successfully!');
+ queryClient.invalidateQueries({ queryKey: ['const.getConstants'] });
+ },
+ onError: (error) => {
+ Alert.alert('Error', 'Failed to update tag order. Please try again.');
+ console.error('Update tag order error:', error);
+ }
+ }
+ );
+ };
+
+ // Show loading state while data is being fetched
+ if (isLoadingConstants || isLoadingTags) {
+ return (
+
+
+
+ router.back()}
+ style={tw`p-2 -ml-4`}
+ >
+
+
+ Tag Orders
+
+
+
+
+
+ {isLoadingConstants ? 'Loading order...' : 'Loading tags...'}
+
+
+
+
+ );
+ }
+
+ // Show error state if queries failed
+ if (constantsError || tagsError) {
+ return (
+
+
+
+ router.back()}
+ style={tw`p-2 -ml-4`}
+ >
+
+
+ Tag Orders
+
+
+
+
+ Error
+
+ {constantsError ? 'Failed to load order' : 'Failed to load tags'}
+
+ router.back()}
+ style={tw`mt-6 bg-blue-600 px-6 py-3 rounded-full`}
+ >
+ Go Back
+
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+ router.back()}
+ style={tw`p-2 -ml-4`}
+ >
+
+
+
+ Tag Orders
+
+
+
+ {updateConstants.isPending ? 'Saving...' : 'Save'}
+
+
+
+
+ {/* Content */}
+ {tags.length === 0 ? (
+
+
+
+ No tags available
+
+
+ ) : (
+
+
+
+ Long press and drag to reorder • {tags.length} items
+
+
+
+
+ item.id.toString()}
+ onDragEnd={handleDragEnd}
+ showsVerticalScrollIndicator={true}
+ contentContainerStyle={{ paddingBottom: 20 }}
+ containerStyle={tw`flex-1`}
+ keyboardShouldPersistTaps="handled"
+ activationDistance={10}
+ />
+
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ item: {
+ width: itemWidth,
+ height: 60,
+ backgroundColor: 'white',
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: '#e5e7eb',
+ padding: 10,
+ flexDirection: 'row',
+ alignItems: 'center',
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.1,
+ shadowRadius: 2,
+ elevation: 2,
+ marginVertical: 4,
+ },
+ activeItem: {
+ shadowColor: '#3b82f6',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.3,
+ shadowRadius: 8,
+ elevation: 8,
+ borderColor: '#3b82f6',
+ transform: [{ scale: 1.02 }],
+ },
+ dragHandle: {
+ marginRight: 8,
+ padding: 2,
+ },
+ image: {
+ width: 30,
+ height: 30,
+ borderRadius: 6,
+ marginRight: 10,
+ },
+ placeholderImage: {
+ width: 30,
+ height: 30,
+ borderRadius: 6,
+ backgroundColor: '#f3f4f6',
+ marginRight: 10,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ info: {
+ flex: 1,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ },
+ name: {
+ fontSize: 13,
+ color: '#111827',
+ fontWeight: '500',
+ flex: 1,
+ marginRight: 4,
+ },
+});
diff --git a/apps/admin-ui/src/components/TagForm.tsx b/apps/admin-ui/src/components/TagForm.tsx
index 481a3f0..6e4e769 100644
--- a/apps/admin-ui/src/components/TagForm.tsx
+++ b/apps/admin-ui/src/components/TagForm.tsx
@@ -1,9 +1,14 @@
import React, { useState, useEffect, forwardRef, useCallback } from 'react';
-import { View, TouchableOpacity } from 'react-native';
+import { View, TouchableOpacity, StyleSheet } from 'react-native';
import { Formik } from 'formik';
import * as Yup from 'yup';
import { MyTextInput, MyText, Checkbox, ImageUploaderNeo, tw, useFocusCallback, BottomDropdown, type ImageUploaderNeoItem, type ImageUploaderNeoPayload } from 'common-ui';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
+import { TouchableOpacity as GHTouchableOpacity } from 'react-native-gesture-handler';
+import DraggableFlatList, { ScaleDecorator } from 'react-native-draggable-flatlist';
+import { Image } from 'expo-image';
+import ProductsSelector from '@/components/ProductsSelector';
+import { trpc } from '@/src/trpc-client';
interface StoreOption {
id: number;
@@ -15,6 +20,7 @@ interface TagFormData {
tagDescription: string;
isDashboardTag: boolean;
relatedStores: number[];
+ productIds: number[];
}
interface TagFormProps {
@@ -26,6 +32,13 @@ interface TagFormProps {
stores?: StoreOption[];
}
+interface SelectedProduct {
+ skuId: number;
+ productId: number;
+ label: string;
+ imageUrl?: string | null;
+}
+
const TagForm = forwardRef(({
mode,
initialValues,
@@ -37,10 +50,35 @@ const TagForm = forwardRef(({
const [images, setImages] = useState([])
const [removedExisting, setRemovedExisting] = useState(false)
const [isDashboardTagChecked, setIsDashboardTagChecked] = useState(Boolean(initialValues.isDashboardTag));
+ const [selectedProducts, setSelectedProducts] = useState([]);
const existingImageUrl = existingImageUrlRaw || ''
const stores = storesRaw || []
+ const { data: skusData } = trpc.common.product.getAllSkusSummary.useQuery();
+ const allSkus: SelectedProduct[] = (skusData?.skus || []).map((sku: any) => ({
+ skuId: sku.id,
+ productId: sku.productId,
+ label: sku.label,
+ imageUrl: sku.images?.[0] || null,
+ }));
+
+ // Build the ordered list from initialValues.productIds (product ids, in sortOrder).
+ useEffect(() => {
+ const ordered: SelectedProduct[] = [];
+ const skuMap = new Map();
+ for (const sku of allSkus) {
+ skuMap.set(sku.productId, sku);
+ }
+ for (const productId of initialValues.productIds || []) {
+ const sku = skuMap.get(productId);
+ if (sku) ordered.push(sku);
+ else ordered.push({ skuId: 0, productId, label: `Product #${productId}`, imageUrl: null });
+ }
+ setSelectedProducts(ordered);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [initialValues.productIds, skusData]);
+
// Update checkbox when initial values change
useEffect(() => {
setIsDashboardTagChecked(Boolean(initialValues.isDashboardTag));
@@ -51,7 +89,6 @@ const TagForm = forwardRef(({
}
setRemovedExisting(false)
}, [existingImageUrlRaw, initialValues.isDashboardTag]);
-
const validationSchema = Yup.object().shape({
tagName: Yup.string()
@@ -62,21 +99,74 @@ const TagForm = forwardRef(({
.max(500, 'Description must be less than 500 characters'),
});
+ // When a product is picked in the selector, add it to the ordered list (if not already present).
+ const handleProductSelect = (value: number | number[]) => {
+ const skuIds = Array.isArray(value) ? value : [value];
+ setSelectedProducts((prev) => {
+ const next = [...prev];
+ for (const skuId of skuIds) {
+ if (next.some((p) => p.skuId === skuId)) continue;
+ const sku = allSkus.find((s) => s.skuId === skuId);
+ if (sku) next.push(sku);
+ }
+ return next;
+ });
+ };
+
+ const handleDragEnd = useCallback(({ data }: { data: SelectedProduct[] }) => {
+ setSelectedProducts(data);
+ }, []);
+
+ const handleRemoveProduct = (productId: number) => {
+ setSelectedProducts((prev) => prev.filter((p) => p.productId !== productId));
+ };
+
+ const renderSelectedItem = useCallback(({ item, drag, isActive }: { item: SelectedProduct; drag: () => void; isActive: boolean }) => (
+
+
+
+
+
+ {item.imageUrl ? (
+
+ ) : (
+
+
+
+ )}
+
+ {item.label}
+
+ handleRemoveProduct(item.productId)} style={styles.removeButton}>
+
+
+
+
+ ), []);
+
return (
onSubmit(values, images, removedExisting)}
+ onSubmit={(values) => onSubmit({ ...values, productIds: selectedProducts.map((p) => p.productId) }, images, removedExisting)}
enableReinitialize
>
- {({ handleChange, handleSubmit, values, setFieldValue, errors, touched, setFieldValue: formikSetFieldValue, resetForm }) => {
- // Clear form when screen comes into focus
- const clearForm = useCallback(() => {
- setImages([])
- setRemovedExisting(false)
- setIsDashboardTagChecked(false);
- resetForm();
- }, [resetForm]);
+ {({ handleChange, handleSubmit, values, setFieldValue, errors, touched, resetForm }) => {
+ // Clear form when screen comes into focus (create mode only — edit keeps its loaded products)
+ const clearForm = useCallback(() => {
+ setImages([])
+ setRemovedExisting(false)
+ setIsDashboardTagChecked(false);
+ if (mode === 'create') {
+ setSelectedProducts([]);
+ }
+ resetForm();
+ }, [resetForm, mode]);
useFocusCallback(clearForm);
@@ -106,7 +196,6 @@ const TagForm = forwardRef(({
Tag Image {mode === 'edit' ? '(Upload new to replace)' : '(Optional)'}
-
{
@@ -132,7 +221,7 @@ const TagForm = forwardRef(({
onPress={() => {
const newValue = !isDashboardTagChecked;
setIsDashboardTagChecked(newValue);
- formikSetFieldValue('isDashboardTag', newValue);
+ setFieldValue('isDashboardTag', newValue);
}}
/>
Mark as Dashboard Tag
@@ -153,12 +242,53 @@ const TagForm = forwardRef(({
}))}
onValueChange={(selectedValues) => {
const numericValues = (selectedValues as string[]).map(v => parseInt(v));
- formikSetFieldValue('relatedStores', numericValues);
+ setFieldValue('relatedStores', numericValues);
}}
multiple={true}
/>
+ {/* Products Section: selector + reorderable list */}
+
+
+ Products
+
+
+
+ {selectedProducts.length > 0 && (
+
+
+ Long press and drag to reorder • {selectedProducts.length} items
+
+
+ )}
+ {selectedProducts.length > 0 ? (
+ item.productId.toString()}
+ onDragEnd={handleDragEnd}
+ showsVerticalScrollIndicator={false}
+ contentContainerStyle={{ paddingBottom: 20 }}
+ keyboardShouldPersistTaps="handled"
+ activationDistance={10}
+ />
+ ) : (
+
+
+ No products selected yet
+
+ )}
+
+
+
handleSubmit()}
disabled={isLoading}
@@ -175,6 +305,62 @@ const TagForm = forwardRef(({
);
});
+const styles = StyleSheet.create({
+ item: {
+ backgroundColor: 'white',
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: '#e5e7eb',
+ padding: 10,
+ flexDirection: 'row',
+ alignItems: 'center',
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.1,
+ shadowRadius: 2,
+ elevation: 2,
+ marginVertical: 4,
+ },
+ activeItem: {
+ shadowColor: '#3b82f6',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.3,
+ shadowRadius: 8,
+ elevation: 8,
+ borderColor: '#3b82f6',
+ transform: [{ scale: 1.02 }],
+ },
+ dragHandle: {
+ marginRight: 8,
+ padding: 2,
+ },
+ image: {
+ width: 30,
+ height: 30,
+ borderRadius: 6,
+ marginRight: 10,
+ },
+ placeholderImage: {
+ width: 30,
+ height: 30,
+ borderRadius: 6,
+ backgroundColor: '#f3f4f6',
+ marginRight: 10,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ name: {
+ flex: 1,
+ fontSize: 13,
+ color: '#111827',
+ fontWeight: '500',
+ marginRight: 4,
+ },
+ removeButton: {
+ padding: 2,
+ },
+});
+
TagForm.displayName = 'TagForm';
export default TagForm;
diff --git a/apps/backend/src/lib/const-keys.ts b/apps/backend/src/lib/const-keys.ts
index 7ea1dbf..0f710db 100644
--- a/apps/backend/src/lib/const-keys.ts
+++ b/apps/backend/src/lib/const-keys.ts
@@ -17,6 +17,7 @@ export const CONST_KEYS = {
appStoreUrl: 'appStoreUrl',
popularItems: 'popularItems',
allItemsOrder: 'allItemsOrder',
+ tagsOrder: 'tagsOrder',
isFlashDeliveryEnabled: 'isFlashDeliveryEnabled',
supportMobile: 'supportMobile',
supportEmail: 'supportEmail',
@@ -41,6 +42,7 @@ export const CONST_LABELS: Record = {
appStoreUrl: 'App Store URL',
popularItems: 'Popular Items',
allItemsOrder: 'All Items Order',
+ tagsOrder: 'Tags Order',
isFlashDeliveryEnabled: 'Enable Flash Delivery',
supportMobile: 'Support Mobile',
supportEmail: 'Support Email',
@@ -71,6 +73,7 @@ export const CONST_TYPES: Record = {
appStoreUrl: 'string',
popularItems: 'string',
allItemsOrder: 'string',
+ tagsOrder: 'string',
isFlashDeliveryEnabled: 'boolean',
supportMobile: 'string',
supportEmail: 'string',
@@ -95,6 +98,7 @@ export const CONST_VISIBILITY: Record = {
appStoreUrl: true,
popularItems: true,
allItemsOrder: true,
+ tagsOrder: false,
isFlashDeliveryEnabled: true,
supportMobile: true,
supportEmail: true,
diff --git a/apps/backend/src/sqliteImporter.ts b/apps/backend/src/sqliteImporter.ts
index 322fc09..eb74ffa 100644
--- a/apps/backend/src/sqliteImporter.ts
+++ b/apps/backend/src/sqliteImporter.ts
@@ -72,6 +72,7 @@ export {
createSpecialDealsForSku,
updateSkuDeals,
replaceProductTags,
+ replaceTagProducts,
mergeSkus,
updateSlotProducts,
getSlotsProductIds,
diff --git a/apps/backend/src/trpc/apis/admin-apis/apis/product.ts b/apps/backend/src/trpc/apis/admin-apis/apis/product.ts
index eac06d3..3657d87 100644
--- a/apps/backend/src/trpc/apis/admin-apis/apis/product.ts
+++ b/apps/backend/src/trpc/apis/admin-apis/apis/product.ts
@@ -21,6 +21,7 @@ import {
checkUnitExists,
createProduct as createProductInDb,
replaceProductTags,
+ replaceTagProducts,
getProductImagesById,
updateProduct as updateProductInDb,
checkProductTagExistsByName,
@@ -29,6 +30,7 @@ import {
deleteProductTag as deleteProductTagInDb,
getAllProductTagInfos as getAllProductTagInfosInDb,
getProductTagInfoById as getProductTagInfoByIdInDb,
+ getProductTagById as getProductTagByIdInDb,
} from '@/src/dbService'
import type {
AdminProduct,
@@ -880,8 +882,8 @@ export const productRouter = router({
getProductTagById: protectedProcedure
.input(z.object({ id: z.number() }))
- .query(async ({ input }): Promise<{ tag: { id: number; tagName: string; tagDescription: string | null; imageUrl: string | null; isDashboardTag: boolean; relatedStores: unknown; createdAt: Date }; message: string }> => {
- const tag = await getProductTagInfoByIdInDb(input.id)
+ .query(async ({ input }): Promise<{ tag: { id: number; tagName: string; tagDescription: string | null; imageUrl: string | null; isDashboardTag: boolean; relatedStores: unknown; sortOrder: number[]; createdAt: Date; products: Array<{ productId: number; tagId: number; assignedAt: Date; product: any }>; productIds: number[] }; message: string }> => {
+ const tag = await getProductTagByIdInDb(input.id)
if (!tag) {
throw new ApiError('Tag not found', 404)
@@ -905,10 +907,11 @@ export const productRouter = router({
imageUrl: z.string().optional().nullable(),
isDashboardTag: z.boolean().optional().default(false),
relatedStores: z.array(z.number()).optional().default([]),
+ productIds: z.array(z.number()).optional().default([]),
uploadUrls: z.array(z.string()).optional().default([]),
}))
- .mutation(async ({ input }): Promise<{ tag: { id: number; tagName: string; tagDescription: string | null; imageUrl: string | null; isDashboardTag: boolean; relatedStores: unknown; createdAt: Date }; message: string }> => {
- const { tagName, tagDescription, imageUrl, isDashboardTag, relatedStores, uploadUrls } = input
+ .mutation(async ({ input }): Promise<{ tag: { id: number; tagName: string; tagDescription: string | null; imageUrl: string | null; isDashboardTag: boolean; relatedStores: unknown; sortOrder: number[]; createdAt: Date }; message: string }> => {
+ const { tagName, tagDescription, imageUrl, isDashboardTag, relatedStores, productIds, uploadUrls } = input
const existingTag = await checkProductTagExistsByName(tagName.trim())
if (existingTag) {
@@ -921,8 +924,11 @@ export const productRouter = router({
imageUrl: imageUrl ?? null,
isDashboardTag,
relatedStores,
+ sortOrder: productIds,
})
+ await replaceTagProducts(createdTag.id, productIds)
+
if (uploadUrls.length > 0) {
await Promise.all(uploadUrls.map((url) => claimUploadUrl(url)))
}
@@ -948,10 +954,11 @@ export const productRouter = router({
imageUrl: z.string().optional().nullable(),
isDashboardTag: z.boolean().optional(),
relatedStores: z.array(z.number()).optional(),
+ productIds: z.array(z.number()).optional(),
uploadUrls: z.array(z.string()).optional().default([]),
}))
- .mutation(async ({ input }): Promise<{ tag: { id: number; tagName: string; tagDescription: string | null; imageUrl: string | null; isDashboardTag: boolean; relatedStores: unknown; createdAt: Date }; message: string }> => {
- const { id, tagName, tagDescription, imageUrl, isDashboardTag, relatedStores, uploadUrls } = input
+ .mutation(async ({ input }): Promise<{ tag: { id: number; tagName: string; tagDescription: string | null; imageUrl: string | null; isDashboardTag: boolean; relatedStores: unknown; sortOrder: number[]; createdAt: Date }; message: string }> => {
+ const { id, tagName, tagDescription, imageUrl, isDashboardTag, relatedStores, productIds, uploadUrls } = input
const currentTag = await getProductTagInfoByIdInDb(id)
@@ -971,8 +978,13 @@ export const productRouter = router({
imageUrl: imageUrl ?? undefined,
isDashboardTag,
relatedStores,
+ sortOrder: productIds,
})
+ if (productIds !== undefined) {
+ await replaceTagProducts(id, productIds)
+ }
+
if (uploadUrls.length > 0) {
await Promise.all(uploadUrls.map((url) => claimUploadUrl(url)))
}
diff --git a/apps/backend/src/trpc/apis/common-apis/common.ts b/apps/backend/src/trpc/apis/common-apis/common.ts
index 2f2cece..fb39a72 100644
--- a/apps/backend/src/trpc/apis/common-apis/common.ts
+++ b/apps/backend/src/trpc/apis/common-apis/common.ts
@@ -11,6 +11,8 @@ import {
import { generateSignedUrlsFromS3Urls, generateSignedUrlFromS3Url, scaffoldAssetUrl } from '@/src/lib/s3-client'
import { getAllProducts as getAllProductsFromCache } from '@/src/stores/product-store'
import { getDashboardTags as getDashboardTagsFromCache } from '@/src/stores/product-tag-store'
+import { getConstant } from '@/src/lib/const-store'
+import { CONST_KEYS } from '@/src/lib/const-keys'
// Re-export with original name for backwards compatibility
export const getNextDeliveryDate = getNextDeliveryDateWithCapacity
@@ -65,6 +67,28 @@ export async function scaffoldProducts() {
getAllTagProductMappings(),
])
+ // Order tags by the admin-defined tagsOrder (unknown tags go to the end).
+ const tagsOrderRaw = await getConstant(CONST_KEYS.tagsOrder)
+ let tagsOrderIds: number[] = []
+ if (Array.isArray(tagsOrderRaw)) {
+ tagsOrderIds = tagsOrderRaw.map((id: any) => parseInt(id)).filter((id: number) => !isNaN(id))
+ } else if (typeof tagsOrderRaw === 'string') {
+ tagsOrderIds = tagsOrderRaw.split(',').map((id: string) => parseInt(id.trim())).filter((id: number) => !isNaN(id))
+ }
+
+ const tagsById = new Map(allTags.map((tag: any) => [tag.id, tag]))
+ const orderedTags: any[] = []
+ for (const id of tagsOrderIds) {
+ const tag = tagsById.get(id)
+ if (tag) {
+ orderedTags.push(tag)
+ tagsById.delete(id)
+ }
+ }
+ for (const tag of tagsById.values()) {
+ orderedTags.push(tag)
+ }
+
const productIdsByTag = new Map()
for (const mapping of tagMappings) {
if (!productIdsByTag.has(mapping.tagId)) {
@@ -73,14 +97,25 @@ export async function scaffoldProducts() {
productIdsByTag.get(mapping.tagId)!.push(mapping.productId)
}
- const tags = allTags.map((tag: { id: number; tagName: string; tagDescription: string | null; imageUrl: string | null; isDashboardTag: boolean; relatedStores: unknown }) => ({
+ // Reorder each tag's product ids by the admin-defined sortOrder (unknown products go to the end).
+ const reorderProductIds = (tagId: number, tagSortOrder: number[] | undefined) => {
+ const current = productIdsByTag.get(tagId) || []
+ if (!Array.isArray(tagSortOrder) || tagSortOrder.length === 0) {
+ return current
+ }
+ const ordered = tagSortOrder.filter((id: number) => current.includes(id))
+ const rest = current.filter((id: number) => !ordered.includes(id))
+ return [...ordered, ...rest]
+ }
+
+ const tags = orderedTags.map((tag: { id: number; tagName: string; tagDescription: string | null; imageUrl: string | null; isDashboardTag: boolean; relatedStores: unknown; sortOrder: number[] | null }) => ({
id: tag.id,
tagName: tag.tagName,
tagDescription: tag.tagDescription,
imageUrl: tag.imageUrl ? scaffoldAssetUrl(tag.imageUrl) : null,
isDashboardTag: tag.isDashboardTag,
relatedStores: (tag.relatedStores as number[]) || [],
- productIds: productIdsByTag.get(tag.id) || [],
+ productIds: reorderProductIds(tag.id, tag.sortOrder ?? undefined),
}))
return {
diff --git a/apps/backend/wrangler.dev.toml b/apps/backend/wrangler.dev.toml
index 1048178..b0f643c 100644
--- a/apps/backend/wrangler.dev.toml
+++ b/apps/backend/wrangler.dev.toml
@@ -9,7 +9,7 @@ routes = [
[[d1_databases]]
binding = "DB"
database_name = "freshyo-backend-dev"
-database_id = "a976d1fb-c6a7-4948-b303-81c6433ed265"
+database_id = "b05f2d65-5496-45bc-9780-ad3cd6f83afa"
#database_name = "freshyo-dev"
#database_id = "3cad440f-dc14-4baa-ab05-04eb08e206de"
migrations_dir="../../packages/db_helper_sqlite/drizzle"
diff --git a/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx b/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx
index e277b1a..39fe454 100755
--- a/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx
+++ b/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx
@@ -1,5 +1,6 @@
-import React, { useState, useCallback, useMemo, memo } from "react";
+import React, { useState, useCallback, useMemo, memo, useRef } from "react";
import { View, Dimensions, Image, RefreshControl, ScrollView } from "react-native";
+import { TabView, TabBar } from "react-native-tab-view";
import { LinearGradient } from "expo-linear-gradient";
import { useRouter } from "expo-router";
import {
@@ -25,7 +26,6 @@ import { useProductSlotIdentifier } from "@/hooks/useProductSlotIdentifier";
import { useCentralSlotStore } from "@/src/store/centralSlotStore";
import { useCentralProductStore } from "@/src/store/centralProductStore";
import FloatingCartBar from "@/components/floating-cart-bar";
-import BannerCarousel from "@/components/BannerCarousel";
import { useUserDetails } from "@/src/contexts/AuthContext";
import TabLayoutWrapper from "@/components/TabLayoutWrapper";
import { useNavigationStore } from "@/src/store/navigationStore";
@@ -37,6 +37,14 @@ const itemWidth = screenWidth * 0.45;
const heroItemWidth = (screenWidth - 72) / 3;
const gridItemWidth = (screenWidth - 48) / 2;
+// Hero product card geometry (used to size the tab scene heights)
+const heroCardImageHeight = heroItemWidth * 0.82;
+const heroCardTextBlock = 86;
+const heroCardHeight = heroCardImageHeight + heroCardTextBlock;
+const heroRowHeight = heroCardHeight + 12; // + mb-3
+const TAG_GRID_COLUMNS = 3;
+const TAB_BAR_HEIGHT = 48;
+
const formatTimeRange = (deliveryTime: string) => {
const time = dayjs(deliveryTime);
const endTime = time.add(1, 'hour');
@@ -53,7 +61,6 @@ const formatTimeRange = (deliveryTime: string) => {
const staticStyles = {
flatListContent: { gap: 16 },
columnWrapper: { gap: 16, paddingHorizontal: 16 },
- popularListContent: { paddingBottom: 16 },
slotsListContent: { paddingBottom: 24 },
};
@@ -91,15 +98,15 @@ const RenderStore = memo(({ item }: RenderStoreProps) => {
activeOpacity={0.7}
>
{item.signedImageUrl ? (
) : (
-
+
)}
-
+
{item.name.replace(/^The\s+/i, "")}
@@ -125,7 +132,7 @@ const SlotCard = memo(({ slot }: SlotCardProps) => {
{
))}
- View all {slot.products.length} items
-
+
+ View all {slot.products.length} items
+
+
);
});
-interface PopularProductItemProps {
- item: any;
- onPress: (id: number) => void;
-}
-
-const PopularProductItem = memo(({ item, onPress }: PopularProductItemProps) => {
- const handlePress = useCallback(() => onPress(item.id), [item.id, onPress]);
-
- return (
-
-
-
- );
-});
-
interface ExploreTabProps {
tag: any;
isSelected: boolean;
@@ -226,7 +213,7 @@ const ExploreTab = memo(({ tag, isSelected, onPress }: ExploreTabProps) => {
style={[
tw`text-base tracking-tight`,
{
- color: isSelected ? '#111827' : '#64748B',
+ color: isSelected ? theme.colors.brand600 : '#64748B',
fontWeight: isSelected ? '700' : '500',
},
]}
@@ -239,7 +226,7 @@ const ExploreTab = memo(({ tag, isSelected, onPress }: ExploreTabProps) => {
height: 4,
borderRadius: 999,
marginTop: 8,
- backgroundColor: isSelected ? '#111827' : 'transparent',
+ backgroundColor: isSelected ? theme.colors.brand500 : 'transparent',
}}
/>
@@ -247,28 +234,141 @@ const ExploreTab = memo(({ tag, isSelected, onPress }: ExploreTabProps) => {
);
});
-interface ExploreTabsRowProps {
+interface TagTabViewProps {
+ dashboardTags: any[];
+ activeTagId: number | null;
+ productsByTagId: Record;
+ onSelectTag: (id: number) => void;
+ onProductPress: (id: number) => void;
+}
+
+const TagTabView = memo(({
+ dashboardTags,
+ activeTagId,
+ productsByTagId,
+ onSelectTag,
+ onProductPress,
+}: TagTabViewProps) => {
+ const [expandedByTagId, setExpandedByTagId] = useState>({});
+
+ const routes = useMemo(
+ () => dashboardTags.map((tag) => ({ key: String(tag.id), title: tag.tagName })),
+ [dashboardTags]
+ );
+
+ const index = useMemo(() => {
+ const idx = dashboardTags.findIndex((tag) => tag.id === activeTagId);
+ return idx < 0 ? 0 : idx;
+ }, [dashboardTags, activeTagId]);
+
+ // Height: fit the max visible product rows across all tags (default 6 per tag).
+ const sceneHeight = useMemo(() => {
+ let maxRows = 1;
+ for (const tag of dashboardTags) {
+ const count = (productsByTagId[tag.id] || []).length;
+ const visible = expandedByTagId[tag.id] ? count : Math.min(count, 6);
+ const rows = Math.max(1, Math.ceil(visible / TAG_GRID_COLUMNS));
+ maxRows = Math.max(maxRows, rows);
+ }
+ return maxRows * heroRowHeight + 16;
+ }, [dashboardTags, productsByTagId, expandedByTagId]);
+
+ const renderTabBar = useCallback((props: any) => (
+
+ ), []);
+
+ const renderScene = useCallback(({ route }: any) => {
+ const tagId = Number(route.key);
+ const products = productsByTagId[tagId] || [];
+ const expanded = !!expandedByTagId[tagId];
+ const visible = expanded ? products : products.slice(0, 6);
+ const hasMore = products.length > 6 && !expanded;
+
+ return (
+
+ {products.length > 0 ? (
+
+ {visible.map((product: any) => (
+
+ ))}
+
+ ) : (
+
+
+ No products in this category yet
+
+
+ )}
+ {hasMore && (
+ setExpandedByTagId((prev) => ({ ...prev, [tagId]: true }))}
+ >
+ Show More
+
+ )}
+
+ );
+ }, [productsByTagId, expandedByTagId, onProductPress]);
+
+ return (
+ onSelectTag(Number(routes[i].key))}
+ renderTabBar={renderTabBar}
+ renderScene={renderScene}
+ swipeEnabled
+ lazy
+ style={{ height: TAB_BAR_HEIGHT + sceneHeight }}
+ />
+ );
+});
+
+interface StickyTabsRowProps {
dashboardTags: any[];
activeTagId: number | null;
onSelectTag: (id: number) => void;
}
-const ExploreTabsRow = memo(({ dashboardTags, activeTagId, onSelectTag }: ExploreTabsRowProps) => (
-
- {dashboardTags.map((tag) => (
- onSelectTag(tag.id)}
- />
- ))}
-
-));
+const StickyTabsRow = memo(({ dashboardTags, activeTagId, onSelectTag }: StickyTabsRowProps) => {
+ const scrollRef = useRef(null);
+
+ const handleSelect = (id: number) => {
+ onSelectTag(id);
+ const idx = dashboardTags.findIndex((tag) => tag.id === id);
+ if (idx >= 0) {
+ scrollRef.current?.scrollTo({ x: idx * 96, animated: true });
+ }
+ };
+
+ return (
+
+ {dashboardTags.map((tag) => (
+ handleSelect(tag.id)} />
+ ))}
+
+ );
+});
interface ExploreProductItemProps {
item: any;
@@ -323,12 +423,11 @@ interface ListHeaderProps {
gradientHeight: number;
onGradientLayout: (height: number) => void;
storesData: any;
- popularProducts: any[];
sortedSlots: any[];
onProductPress: (id: number) => void;
dashboardTags: any[];
activeTagId: number | null;
- activeTagProducts: any[];
+ productsByTagId: Record;
onSelectTag: (id: number) => void;
onTabsSectionLayout: (layout: { y: number; height: number }) => void;
}
@@ -337,29 +436,19 @@ const ListHeader = memo(({
gradientHeight,
onGradientLayout,
storesData,
- popularProducts,
sortedSlots,
onProductPress,
dashboardTags,
activeTagId,
- activeTagProducts,
+ productsByTagId,
onSelectTag,
onTabsSectionLayout,
}: ListHeaderProps) => {
- const [showAllActiveProducts, setShowAllActiveProducts] = useState(false);
const handleLayout = useCallback((event: any) => {
const { y, height } = event.nativeEvent.layout;
onGradientLayout(y + height);
}, [onGradientLayout]);
- React.useEffect(() => {
- setShowAllActiveProducts(false);
- }, [activeTagId]);
-
- const renderPopularItem = useCallback(({ item }: { item: any }) => (
-
- ), [onProductPress]);
-
const renderSlotItem = useCallback(({ item }: { item: any }) => (
), []);
@@ -370,16 +459,14 @@ const ListHeader = memo(({
], [gradientHeight]);
const activeColor = activeTagId == null ? null : getTagColor(activeTagId);
const pageTint = activeColor?.pageBg ?? '#FFFFFF';
- const visibleActiveTagProducts = showAllActiveProducts ? activeTagProducts : activeTagProducts.slice(0, 6);
- const hasMoreActiveTagProducts = activeTagProducts.length > visibleActiveTagProducts.length;
return (
<>
-
+
@@ -390,59 +477,29 @@ const ListHeader = memo(({
onTabsSectionLayout({ y, height });
}}
style={[
- tw`mx-4 mb-2 rounded-[28px] px-3 pt-3 pb-2`,
- { backgroundColor: pageTint },
+ tw`mx-4 mb-2 rounded-[28px] px-3 pt-3 pb-[30px]`,
+ { backgroundColor: theme.colors.brand25 },
]}
>
-
- {activeTagProducts.length > 0 ? (
-
-
- {visibleActiveTagProducts.map((product: any) => (
-
- ))}
-
- {hasMoreActiveTagProducts && (
- setShowAllActiveProducts(true)}
- >
- Show More
-
- )}
-
- ) : (
-
-
- No products in this category yet
-
-
- )}
)}
-
-
-
-
{storesData?.stores && storesData.stores.length > 0 && (
-
+
@@ -467,36 +524,15 @@ const ListHeader = memo(({
-
- Popular Items
- Trending fresh picks just for you
-
-
-
- item.id.toString()}
- horizontal
- showsHorizontalScrollIndicator={false}
- contentContainerStyle={staticStyles.popularListContent}
- renderItem={renderPopularItem}
- removeClippedSubviews={true}
- />
-
-
-
{sortedSlots.length > 0 && (
- Upcoming Delivery Slots
- Plan your fresh deliveries ahead
+
+
+ Upcoming Delivery Slots
+
+ Plan your fresh deliveries ahead
- All Available Products
- Browse our complete selection
+
+
+ All Available Products
+
+ Browse our complete selection
@@ -587,21 +626,6 @@ export default function Dashboard() {
setHasMore(products.length > 10)
}, [productsData, productSlotsMap]);
- const popularItemIds = useMemo(() => {
- const popularItems = essentialConsts?.popularItems;
- if (!popularItems) return [];
-
- if (Array.isArray(popularItems)) {
- return popularItems.map((id: any) => parseInt(id)).filter((id: number) => !isNaN(id));
- } else if (typeof popularItems === 'string') {
- return popularItems
- .split(',')
- .map((id: string) => parseInt(id.trim()))
- .filter((id: number) => !isNaN(id));
- }
- return [];
- }, [essentialConsts?.popularItems]);
-
const sortedSlots = useMemo(() => {
if (!slotsData?.slots) return [];
const now = dayjs();
@@ -614,31 +638,43 @@ export default function Dashboard() {
});
}, [slotsData]);
- const popularProducts = useMemo(() => {
- return popularItemIds
- .map(id => products.find(product => product.id === id))
- .filter((product): product is NonNullable => product != null);
- }, [popularItemIds, products]);
+ const productsByTagId = useMemo(() => {
+ const map: Record = {};
+ for (const tag of dashboardTags) {
+ const productById = new Map();
+ for (const product of products) {
+ productById.set(product.id, product);
+ }
- const activeTagProducts = useMemo(() => {
- if (activeTagId == null) return [];
- const activeTag = dashboardTags.find((tag: any) => tag.id === activeTagId);
- if (!activeTag) return [];
+ // tag.productIds is already in the admin-curated order (backend sorts by sortOrder).
+ const orderedIds = (tag.productIds || []).filter((id: number) => productById.has(id));
+ const ordered: any[] = [];
+ const rest: any[] = [];
+ for (const id of orderedIds) {
+ const product = productById.get(id);
+ const isOutOfStock = Boolean(productSlotsMap[id]?.isOutOfStock) || !getQuickestSlot(id);
+ if (isOutOfStock) rest.push(product);
+ else ordered.push(product);
+ }
- return products
- .filter((product: any) => activeTag.productIds?.includes(product.id) ?? false)
- .sort((a: any, b: any) => {
- const slotA = getQuickestSlot(a.id)
- const slotB = getQuickestSlot(b.id)
+ // Products in the tag's productIds but not in the curated order (added later) — availability sort.
+ const seen = new Set(orderedIds);
+ const extra = products
+ .filter((product: any) => (tag.productIds?.includes(product.id) ?? false) && !seen.has(product.id))
+ .sort((a: any, b: any) => {
+ const slotA = getQuickestSlot(a.id)
+ const slotB = getQuickestSlot(b.id)
+ const aOutOfStock = Boolean(productSlotsMap[a.id]?.isOutOfStock) || !slotA
+ const bOutOfStock = Boolean(productSlotsMap[b.id]?.isOutOfStock) || !slotB
+ if (aOutOfStock && !bOutOfStock) return 1
+ if (!aOutOfStock && bOutOfStock) return -1
+ return 0
+ });
- const aOutOfStock = Boolean(productSlotsMap[a.id]?.isOutOfStock) || !slotA
- const bOutOfStock = Boolean(productSlotsMap[b.id]?.isOutOfStock) || !slotB
-
- if (aOutOfStock && !bOutOfStock) return 1
- if (!aOutOfStock && bOutOfStock) return -1
- return 0
- });
- }, [activeTagId, dashboardTags, products, getQuickestSlot, productSlotsMap]);
+ map[tag.id] = [...ordered, ...rest, ...extra];
+ }
+ return map;
+ }, [dashboardTags, products, getQuickestSlot, productSlotsMap]);
const handleRefresh = useCallback(async () => {
setIsRefreshing(true);
@@ -703,26 +739,25 @@ export default function Dashboard() {
gradientHeight={gradientHeight}
onGradientLayout={handleGradientLayout}
storesData={storesData}
- popularProducts={popularProducts}
sortedSlots={sortedSlots}
onProductPress={handleProductPress}
dashboardTags={dashboardTags}
activeTagId={activeTagId}
- activeTagProducts={activeTagProducts}
+ productsByTagId={productsByTagId}
onSelectTag={setSelectedTagId}
onTabsSectionLayout={handleTabsSectionLayout}
/>
- ), [gradientHeight, handleGradientLayout, storesData, popularProducts, sortedSlots, handleProductPress, dashboardTags, activeTagId, activeTagProducts, handleTabsSectionLayout]);
+ ), [gradientHeight, handleGradientLayout, storesData, sortedSlots, handleProductPress, dashboardTags, activeTagId, productsByTagId, handleTabsSectionLayout]);
const pageTint = activeTagId == null ? '#FFFFFF' : getTagColor(activeTagId).pageBg;
const pageTintStyle = useMemo(() => [
tw`flex-1`,
- { backgroundColor: pageTint }
+ { backgroundColor: '#FFFFFF' }
], [pageTint]);
const searchBarContainerStyle = useMemo(() => [
tw`w-full px-4 pt-4 pb-0`,
- { backgroundColor: pageTint }
+ { backgroundColor: '#FFFFFF' }
], [pageTint]);
const listContentContainerStyle = useMemo(() => [
@@ -751,11 +786,8 @@ export default function Dashboard() {
);
}
- let str = ''
- displayedProducts.forEach(product => str += `${product.id}-`)
- // console.log(str)
return (
-
+
item.id.toString()}
numColumns={2}
- style={{ backgroundColor: pageTint }}
+ style={{ backgroundColor: '#FFFFFF' }}
onScroll={handleListScroll}
scrollEventThrottle={16}
contentContainerStyle={listContentContainerStyle}
@@ -791,8 +823,8 @@ export default function Dashboard() {
}
ListEmptyComponent={
@@ -814,7 +846,7 @@ export default function Dashboard() {
tw`absolute left-0 right-0 z-20 px-4 pb-2`,
{
top: searchBarHeight,
- backgroundColor: pageTint,
+ backgroundColor: '#FFFFFF',
elevation: 8,
},
]}
@@ -822,10 +854,10 @@ export default function Dashboard() {
- ({
imageUrl: tag.imageUrl ?? null,
isDashboardTag: tag.isDashboardTag,
relatedStores: tag.relatedStores,
+ sortOrder: tag.sortOrder ?? [],
createdAt: tag.createdAt,
})
@@ -587,6 +588,7 @@ export async function getAllProductTags(): Promise assignment.productId),
}))
}
@@ -616,6 +618,7 @@ export interface CreateProductTagInput {
imageUrl?: string | null
isDashboardTag?: boolean
relatedStores?: number[]
+ sortOrder?: number[]
}
export async function createProductTag(input: CreateProductTagInput): Promise {
@@ -625,11 +628,13 @@ export async function createProductTag(input: CreateProductTagInput): Promise assignment.productId),
}
}
@@ -666,6 +672,7 @@ export interface UpdateProductTagInput {
imageUrl?: string | null
isDashboardTag?: boolean
relatedStores?: number[]
+ sortOrder?: number[]
}
export async function updateProductTag(tagId: number, input: UpdateProductTagInput): Promise {
@@ -675,6 +682,7 @@ export async function updateProductTag(tagId: number, input: UpdateProductTagInp
...(input.imageUrl !== undefined && { imageUrl: input.imageUrl }),
...(input.isDashboardTag !== undefined && { isDashboardTag: input.isDashboardTag }),
...(input.relatedStores !== undefined && { relatedStores: input.relatedStores }),
+ ...(input.sortOrder !== undefined && { sortOrder: input.sortOrder }),
}).where(eq(productTagInfo.id, tagId)).returning()
const fullTag = await db.query.productTagInfo.findFirst({
@@ -696,6 +704,7 @@ export async function updateProductTag(tagId: number, input: UpdateProductTagInp
assignedAt: assignment.assignedAt,
product: mapProduct(assignment.product),
})) || [],
+ productIds: fullTag?.products.map((assignment: ProductTagRow) => assignment.productId) || [],
}
}
@@ -1147,6 +1156,21 @@ export async function replaceProductTags(productId: number, tagIds: number[]): P
await db.insert(productTags).values(tagAssociations)
}
+export async function replaceTagProducts(tagId: number, productIds: number[]): Promise {
+ await db.delete(productTags).where(eq(productTags.tagId, tagId))
+
+ if (productIds.length === 0) {
+ return
+ }
+
+ const productAssociations = productIds.map((productId) => ({
+ productId,
+ tagId,
+ }))
+
+ await db.insert(productTags).values(productAssociations)
+}
+
export async function mergeSkus(fromSkuId: number, toSkuId: number) {
if (fromSkuId === toSkuId) {
return { fromSkuId, toSkuId, message: 'SKUs are the same, no merge needed', counts: {} }
diff --git a/packages/db_helper_sqlite/src/db/schema.ts b/packages/db_helper_sqlite/src/db/schema.ts
index a767840..e3ad403 100644
--- a/packages/db_helper_sqlite/src/db/schema.ts
+++ b/packages/db_helper_sqlite/src/db/schema.ts
@@ -291,6 +291,7 @@ export const productTagInfo = sqliteTable('product_tag_info', {
imageUrl: text('image_url'),
isDashboardTag: integer('is_dashboard_tag', { mode: 'boolean' }).notNull().default(false),
relatedStores: jsonText('related_stores').$defaultFn(() => []),
+ sortOrder: jsonText('sort_order').$defaultFn(() => []),
createdAt: timestampText('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
})
diff --git a/packages/db_helper_sqlite/src/stores/store-helpers.ts b/packages/db_helper_sqlite/src/stores/store-helpers.ts
index 03758b6..5bfa5d0 100644
--- a/packages/db_helper_sqlite/src/stores/store-helpers.ts
+++ b/packages/db_helper_sqlite/src/stores/store-helpers.ts
@@ -267,6 +267,7 @@ export interface TagBasicData {
imageUrl: string | null
isDashboardTag: boolean
relatedStores: unknown
+ sortOrder: number[] | null
}
export interface TagProductMapping {
@@ -283,6 +284,7 @@ export async function getAllTagsForCache(): Promise {
imageUrl: productTagInfo.imageUrl,
isDashboardTag: productTagInfo.isDashboardTag,
relatedStores: productTagInfo.relatedStores,
+ sortOrder: productTagInfo.sortOrder,
})
.from(productTagInfo)
}
diff --git a/packages/shared/types/admin.ts b/packages/shared/types/admin.ts
index f1b4932..f653d6d 100644
--- a/packages/shared/types/admin.ts
+++ b/packages/shared/types/admin.ts
@@ -454,6 +454,7 @@ export interface AdminProductTagInfo {
imageUrl: string | null;
isDashboardTag: boolean;
relatedStores: unknown;
+ sortOrder: number[];
createdAt: Date;
}
@@ -466,6 +467,7 @@ export interface AdminProductTagAssignment {
export interface AdminProductTagWithProducts extends AdminProductTagInfo {
products: AdminProductTagAssignment[];
+ productIds: number[];
}
export interface AdminSpecialDeal {