Compare commits
4 commits
d234c8a00f
...
d599c2e004
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d599c2e004 | ||
|
|
55c41fa0af | ||
|
|
d1d7db55a0 | ||
|
|
78e90fd398 |
34 changed files with 599 additions and 82 deletions
File diff suppressed because one or more lines are too long
6
apps/admin-ui/.expo/types/router.d.ts
vendored
6
apps/admin-ui/.expo/types/router.d.ts
vendored
File diff suppressed because one or more lines are too long
|
|
@ -17,6 +17,13 @@ export default function Layout() {
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="all-items-order"
|
||||||
|
options={{
|
||||||
|
title: "All Items Order",
|
||||||
|
headerShown: false,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
391
apps/admin-ui/app/(drawer)/customize-app/all-items-order.tsx
Normal file
391
apps/admin-ui/app/(drawer)/customize-app/all-items-order.tsx
Normal file
|
|
@ -0,0 +1,391 @@
|
||||||
|
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");
|
||||||
|
// Item takes full width minus padding
|
||||||
|
const itemWidth = screenWidth - 48; // 24px padding each side
|
||||||
|
const itemHeight = 80;
|
||||||
|
|
||||||
|
interface Product {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
images: string[];
|
||||||
|
isOutOfStock: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProductItemProps {
|
||||||
|
item: Product;
|
||||||
|
drag: () => void;
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductItem: React.FC<ProductItemProps> = ({
|
||||||
|
item,
|
||||||
|
drag,
|
||||||
|
isActive,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ScaleDecorator>
|
||||||
|
<TouchableOpacity
|
||||||
|
onLongPress={drag}
|
||||||
|
activeOpacity={1}
|
||||||
|
style={[
|
||||||
|
styles.item,
|
||||||
|
isActive && styles.activeItem,
|
||||||
|
item.isOutOfStock && styles.outOfStock,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{/* Drag Handle */}
|
||||||
|
<View style={styles.dragHandle}>
|
||||||
|
<MaterialIcons
|
||||||
|
name="drag-indicator"
|
||||||
|
size={24}
|
||||||
|
color={isActive ? "#3b82f6" : "#9ca3af"}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Product Image */}
|
||||||
|
{item.images?.[0] ? (
|
||||||
|
<Image
|
||||||
|
source={{ uri: item.images[0] }}
|
||||||
|
style={styles.image}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<View style={styles.placeholderImage}>
|
||||||
|
<MaterialIcons name="image" size={24} color="#9ca3af" />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Product Info */}
|
||||||
|
<View style={styles.info}>
|
||||||
|
<MyText style={styles.name} numberOfLines={1}>
|
||||||
|
{item.name.length > 30 ? item.name.substring(0, 30) + '...' : item.name}
|
||||||
|
</MyText>
|
||||||
|
|
||||||
|
{item.isOutOfStock && (
|
||||||
|
<MaterialIcons name="remove-circle" size={16} color="#dc2626" />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</ScaleDecorator>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AllItemsOrder() {
|
||||||
|
const router = useRouter();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
|
||||||
|
// Get current order from constants
|
||||||
|
const { data: constants, isLoading: isLoadingConstants, error: constantsError } = trpc.admin.const.getConstants.useQuery();
|
||||||
|
const { data: allProducts, isLoading: isLoadingProducts, error: productsError } = trpc.common.product.getAllProductsSummary.useQuery({});
|
||||||
|
const updateConstants = trpc.admin.const.updateConstants.useMutation();
|
||||||
|
|
||||||
|
// Initialize products from constants
|
||||||
|
useEffect(() => {
|
||||||
|
if (allProducts?.products) {
|
||||||
|
const allItemsOrderConstant = constants?.find(c => c.key === 'allItemsOrder');
|
||||||
|
|
||||||
|
let orderedIds: number[] = [];
|
||||||
|
|
||||||
|
if (allItemsOrderConstant) {
|
||||||
|
const value = allItemsOrderConstant.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 product map for quick lookup
|
||||||
|
const productMap = new Map(allProducts.products.map(p => [p.id, p]));
|
||||||
|
|
||||||
|
// Sort products based on order, products not in order go to end
|
||||||
|
const sortedProducts: Product[] = [];
|
||||||
|
|
||||||
|
// First add products in the specified order
|
||||||
|
for (const id of orderedIds) {
|
||||||
|
const product = productMap.get(id);
|
||||||
|
if (product) {
|
||||||
|
sortedProducts.push({
|
||||||
|
id: product.id,
|
||||||
|
name: product.name,
|
||||||
|
images: product.images || [],
|
||||||
|
isOutOfStock: product.isOutOfStock || false,
|
||||||
|
});
|
||||||
|
productMap.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then add remaining products (not in order yet)
|
||||||
|
for (const product of productMap.values()) {
|
||||||
|
sortedProducts.push({
|
||||||
|
id: product.id,
|
||||||
|
name: product.name,
|
||||||
|
images: product.images || [],
|
||||||
|
isOutOfStock: product.isOutOfStock || false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setProducts(sortedProducts);
|
||||||
|
}
|
||||||
|
}, [constants, allProducts]);
|
||||||
|
|
||||||
|
const handleDragEnd = useCallback(({ data }: { data: Product[] }) => {
|
||||||
|
setProducts(data);
|
||||||
|
setHasChanges(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderItem = useCallback(({ item, drag, isActive }: { item: Product; drag: () => void; isActive: boolean }) => {
|
||||||
|
return (
|
||||||
|
<ProductItem
|
||||||
|
item={item}
|
||||||
|
drag={drag}
|
||||||
|
isActive={isActive}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const productIds = products.map(p => p.id);
|
||||||
|
|
||||||
|
updateConstants.mutate(
|
||||||
|
{
|
||||||
|
constants: [{
|
||||||
|
key: 'allItemsOrder',
|
||||||
|
value: productIds
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
setHasChanges(false);
|
||||||
|
Alert.alert('Success', 'All items order updated successfully!');
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['const.getConstants'] });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
Alert.alert('Error', 'Failed to update items order. Please try again.');
|
||||||
|
console.error('Update all items order error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show loading state while data is being fetched
|
||||||
|
if (isLoadingConstants || isLoadingProducts) {
|
||||||
|
return (
|
||||||
|
<AppContainer>
|
||||||
|
<View style={tw`flex-1 bg-gray-50`}>
|
||||||
|
<View style={tw`bg-white px-4 py-4 border-b border-gray-200 flex-row items-center justify-between`}>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => router.back()}
|
||||||
|
style={tw`p-2 -ml-4`}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="chevron-left" size={24} color="#374151" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<MyText style={tw`text-xl font-bold text-gray-900`}>All Items Order</MyText>
|
||||||
|
<View style={tw`w-16`} />
|
||||||
|
</View>
|
||||||
|
<View style={tw`flex-1 justify-center items-center p-8`}>
|
||||||
|
<ActivityIndicator size="large" color="#3b82f6" />
|
||||||
|
<MyText style={tw`text-gray-500 mt-4 text-center`}>
|
||||||
|
{isLoadingConstants ? 'Loading order...' : 'Loading products...'}
|
||||||
|
</MyText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</AppContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error state if queries failed
|
||||||
|
if (constantsError || productsError) {
|
||||||
|
return (
|
||||||
|
<AppContainer>
|
||||||
|
<View style={tw`flex-1 bg-gray-50`}>
|
||||||
|
<View style={tw`bg-white px-4 py-4 border-b border-gray-200 flex-row items-center justify-between`}>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => router.back()}
|
||||||
|
style={tw`p-2 -ml-4`}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="chevron-left" size={24} color="#374151" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<MyText style={tw`text-xl font-bold text-gray-900`}>All Items Order</MyText>
|
||||||
|
<View style={tw`w-16`} />
|
||||||
|
</View>
|
||||||
|
<View style={tw`flex-1 justify-center items-center p-8`}>
|
||||||
|
<MaterialIcons name="error-outline" size={64} color="#ef4444" />
|
||||||
|
<MyText style={tw`text-gray-900 text-lg font-bold mt-4`}>Error</MyText>
|
||||||
|
<MyText style={tw`text-gray-500 mt-2 text-center`}>
|
||||||
|
{constantsError ? 'Failed to load order' : 'Failed to load products'}
|
||||||
|
</MyText>
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => router.back()}
|
||||||
|
style={tw`mt-6 bg-blue-600 px-6 py-3 rounded-full`}
|
||||||
|
>
|
||||||
|
<MyText style={tw`text-white font-semibold`}>Go Back</MyText>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</AppContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={tw`flex-1 bg-gray-50`}>
|
||||||
|
{/* Header */}
|
||||||
|
<View style={tw`bg-white px-4 py-4 border-b border-gray-200 flex-row items-center justify-between`}>
|
||||||
|
<MyTouchableOpacity
|
||||||
|
onPress={() => router.back()}
|
||||||
|
style={tw`p-2 -ml-4`}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="chevron-left" size={24} color="#374151" />
|
||||||
|
</MyTouchableOpacity>
|
||||||
|
|
||||||
|
<MyText style={tw`text-xl font-bold text-gray-900`}>All Items Order</MyText>
|
||||||
|
|
||||||
|
<MyTouchableOpacity
|
||||||
|
onPress={handleSave}
|
||||||
|
disabled={!hasChanges || updateConstants.isPending}
|
||||||
|
style={tw`px-4 py-2 rounded-lg ${
|
||||||
|
hasChanges && !updateConstants.isPending
|
||||||
|
? 'bg-blue-600'
|
||||||
|
: 'bg-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<MyText style={tw`${
|
||||||
|
hasChanges && !updateConstants.isPending
|
||||||
|
? 'text-white'
|
||||||
|
: 'text-gray-500'
|
||||||
|
} font-semibold`}>
|
||||||
|
{updateConstants.isPending ? 'Saving...' : 'Save'}
|
||||||
|
</MyText>
|
||||||
|
</MyTouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
{products.length === 0 ? (
|
||||||
|
<View style={tw`flex-1 justify-center items-center p-8`}>
|
||||||
|
<MaterialIcons name="inventory" size={64} color="#e5e7eb" />
|
||||||
|
<MyText style={tw`text-gray-500 mt-4 text-center text-lg`}>
|
||||||
|
No products available
|
||||||
|
</MyText>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={tw`flex-1`}>
|
||||||
|
<View style={tw`bg-blue-50 px-4 py-2 mb-2 mt-2 mx-4 rounded-lg`}>
|
||||||
|
<MyText style={tw`text-blue-700 text-xs text-center`}>
|
||||||
|
Long press and drag to reorder • {products.length} items
|
||||||
|
</MyText>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={tw`flex-1 px-3`}>
|
||||||
|
<DraggableFlatList
|
||||||
|
data={products}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={(item) => item.id.toString()}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
showsVerticalScrollIndicator={true}
|
||||||
|
contentContainerStyle={{ paddingBottom: 20 }}
|
||||||
|
containerStyle={tw`flex-1`}
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
|
// Enable auto-scroll during drag
|
||||||
|
activationDistance={10}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }],
|
||||||
|
},
|
||||||
|
outOfStock: {
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
orderNumber: {
|
||||||
|
fontSize: 11,
|
||||||
|
color: '#9ca3af',
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -31,6 +31,7 @@ const CONST_LABELS: Record<string, string> = {
|
||||||
playStoreUrl: 'Play Store URL',
|
playStoreUrl: 'Play Store URL',
|
||||||
appStoreUrl: 'App Store URL',
|
appStoreUrl: 'App Store URL',
|
||||||
popularItems: 'Popular Items',
|
popularItems: 'Popular Items',
|
||||||
|
allItemsOrder: 'All Items Order',
|
||||||
isFlashDeliveryEnabled: 'Enable Flash Delivery',
|
isFlashDeliveryEnabled: 'Enable Flash Delivery',
|
||||||
supportMobile: 'Support Mobile',
|
supportMobile: 'Support Mobile',
|
||||||
supportEmail: 'Support Email',
|
supportEmail: 'Support Email',
|
||||||
|
|
@ -48,6 +49,7 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constant, setFieldValue,
|
||||||
|
|
||||||
// Special handling for popularItems - show navigation button instead of input
|
// Special handling for popularItems - show navigation button instead of input
|
||||||
if (constant.key === 'popularItems') {
|
if (constant.key === 'popularItems') {
|
||||||
|
console.log('key is allItemsOrder')
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
<MyText style={tw`text-sm font-medium text-gray-700 mb-2`}>
|
<MyText style={tw`text-sm font-medium text-gray-700 mb-2`}>
|
||||||
|
|
@ -67,6 +69,28 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constant, setFieldValue,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special handling for allItemsOrder - show navigation button instead of input
|
||||||
|
if (constant.key === 'allItemsOrder') {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<MyText style={tw`text-sm font-medium text-gray-700 mb-2`}>
|
||||||
|
{CONST_LABELS[constant.key] || constant.key}
|
||||||
|
</MyText>
|
||||||
|
<MyTouchableOpacity
|
||||||
|
onPress={() => router.push('/(drawer)/customize-app/all-items-order')}
|
||||||
|
style={tw`bg-green-50 border-2 border-dashed border-green-200 p-4 rounded-lg flex-row items-center justify-center`}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="reorder" size={20} color="#16a34a" style={tw`mr-2`} />
|
||||||
|
<MyText style={tw`text-green-700 font-medium`}>
|
||||||
|
Manage All Visible Items ({Array.isArray(constant.value) ? constant.value.length : 0} items)
|
||||||
|
</MyText>
|
||||||
|
<MaterialIcons name="chevron-right" size={20} color="#16a34a" style={tw`ml-2`} />
|
||||||
|
</MyTouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Handle boolean values - show checkbox
|
// Handle boolean values - show checkbox
|
||||||
if (typeof constant.value === 'boolean') {
|
if (typeof constant.value === 'boolean') {
|
||||||
return (
|
return (
|
||||||
|
|
@ -134,6 +158,7 @@ export default function CustomizeApp() {
|
||||||
const { data: constants, isLoading: isLoadingConstants, refetch } = trpc.admin.const.getConstants.useQuery();
|
const { data: constants, isLoading: isLoadingConstants, refetch } = trpc.admin.const.getConstants.useQuery();
|
||||||
const { mutate: updateConstants, isPending: isUpdating } = trpc.admin.const.updateConstants.useMutation();
|
const { mutate: updateConstants, isPending: isUpdating } = trpc.admin.const.updateConstants.useMutation();
|
||||||
|
|
||||||
|
|
||||||
const handleSubmit = (values: ConstantFormData) => {
|
const handleSubmit = (values: ConstantFormData) => {
|
||||||
// Filter out constants that haven't changed
|
// Filter out constants that haven't changed
|
||||||
const changedConstants = values.constants.filter((constant, index) => {
|
const changedConstants = values.constants.filter((constant, index) => {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import React, { useState, useEffect, useMemo } from "react";
|
import React, { useState, useEffect, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
TouchableOpacity,
|
|
||||||
Alert,
|
Alert,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
ScrollView,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
|
import { TouchableOpacity } from "react-native-gesture-handler";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import DraggableFlatList, {
|
import DraggableFlatList, {
|
||||||
RenderItemParams,
|
RenderItemParams,
|
||||||
|
|
@ -16,6 +17,7 @@ import {
|
||||||
tw,
|
tw,
|
||||||
BottomDialog,
|
BottomDialog,
|
||||||
BottomDropdown,
|
BottomDropdown,
|
||||||
|
MyTouchableOpacity,
|
||||||
} from "common-ui";
|
} from "common-ui";
|
||||||
import ProductsSelector from "../../../components/ProductsSelector";
|
import ProductsSelector from "../../../components/ProductsSelector";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter } from "expo-router";
|
||||||
|
|
@ -27,8 +29,8 @@ interface PopularProduct {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
shortDescription: string | null;
|
shortDescription: string | null;
|
||||||
price: string;
|
price: number;
|
||||||
marketPrice: string | null;
|
marketPrice: number | null;
|
||||||
unit: string;
|
unit: string;
|
||||||
incrementStep: number;
|
incrementStep: number;
|
||||||
productQuantity: number;
|
productQuantity: number;
|
||||||
|
|
@ -119,7 +121,7 @@ export default function CustomizePopularItems() {
|
||||||
const [popularProducts, setPopularProducts] = useState<PopularProduct[]>([]);
|
const [popularProducts, setPopularProducts] = useState<PopularProduct[]>([]);
|
||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
const [showAddDialog, setShowAddDialog] = useState(false);
|
||||||
const [selectedProductId, setSelectedProductId] = useState<number | null>(null);
|
const [selectedProductIds, setSelectedProductIds] = useState<number[]>([]);
|
||||||
|
|
||||||
// Get current popular items from constants
|
// Get current popular items from constants
|
||||||
const { data: constants, isLoading: isLoadingConstants, error: constantsError } = trpc.admin.const.getConstants.useQuery();
|
const { data: constants, isLoading: isLoadingConstants, error: constantsError } = trpc.admin.const.getConstants.useQuery();
|
||||||
|
|
@ -182,14 +184,20 @@ export default function CustomizePopularItems() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddProduct = () => {
|
const handleAddProduct = () => {
|
||||||
if (selectedProductId) {
|
if (selectedProductIds.length > 0) {
|
||||||
const product = allProducts?.products.find(p => p.id === selectedProductId);
|
const newProducts = selectedProductIds
|
||||||
if (product && !popularProducts.find(p => p.id === product.id)) {
|
.map(id => allProducts?.products.find(p => p.id === id))
|
||||||
setPopularProducts(prev => [...prev, product as PopularProduct]);
|
.filter((product): product is NonNullable<typeof product> =>
|
||||||
|
product !== undefined && !popularProducts.find(p => p.id === product.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newProducts.length > 0) {
|
||||||
|
setPopularProducts(prev => [...prev, ...newProducts as PopularProduct[]]);
|
||||||
setHasChanges(true);
|
setHasChanges(true);
|
||||||
setSelectedProductId(null);
|
|
||||||
setShowAddDialog(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSelectedProductIds([]);
|
||||||
|
setShowAddDialog(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -293,20 +301,19 @@ export default function CustomizePopularItems() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContainer>
|
<View style={[tw`flex-1 bg-gray-50 relative`]}>
|
||||||
<View style={tw`flex-1 bg-gray-50`}>
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={tw`bg-white px-4 py-4 border-b border-gray-200 flex-row items-center justify-between`}>
|
<View style={tw`bg-white px-4 py-4 border-b border-gray-200 flex-row items-center justify-between`}>
|
||||||
<TouchableOpacity
|
<MyTouchableOpacity
|
||||||
onPress={() => router.back()}
|
onPress={() => router.back()}
|
||||||
style={tw`p-2 -ml-4`}
|
style={tw`p-2 -ml-4`}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="chevron-left" size={24} color="#374151" />
|
<MaterialIcons name="chevron-left" size={24} color="#374151" />
|
||||||
</TouchableOpacity>
|
</MyTouchableOpacity>
|
||||||
|
|
||||||
<MyText style={tw`text-xl font-bold text-gray-900`}>Popular Items</MyText>
|
<MyText style={tw`text-xl font-bold text-gray-900`}>Popular Items</MyText>
|
||||||
|
|
||||||
<TouchableOpacity
|
<MyTouchableOpacity
|
||||||
onPress={handleSave}
|
onPress={handleSave}
|
||||||
disabled={!hasChanges || updateConstants.isPending}
|
disabled={!hasChanges || updateConstants.isPending}
|
||||||
style={tw`px-4 py-2 rounded-lg ${
|
style={tw`px-4 py-2 rounded-lg ${
|
||||||
|
|
@ -322,7 +329,7 @@ export default function CustomizePopularItems() {
|
||||||
} font-semibold`}>
|
} font-semibold`}>
|
||||||
{updateConstants.isPending ? 'Saving...' : 'Save'}
|
{updateConstants.isPending ? 'Saving...' : 'Save'}
|
||||||
</MyText>
|
</MyText>
|
||||||
</TouchableOpacity>
|
</MyTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
|
|
@ -356,35 +363,41 @@ export default function CustomizePopularItems() {
|
||||||
)}
|
)}
|
||||||
keyExtractor={(item) => item.id.toString()}
|
keyExtractor={(item) => item.id.toString()}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={true}
|
||||||
contentContainerStyle={tw`pb-8`}
|
scrollEnabled={true}
|
||||||
|
contentContainerStyle={{ paddingBottom: 80 }}
|
||||||
|
containerStyle={tw`flex-1`}
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* FAB for Add Product */}
|
{/* FAB for Add Product - Fixed position */}
|
||||||
<View style={tw`absolute bottom-4 right-4`}>
|
<View style={tw`absolute bottom-12 right-6 z-50`}>
|
||||||
<TouchableOpacity
|
<MyTouchableOpacity
|
||||||
onPress={() => setShowAddDialog(true)}
|
onPress={() => setShowAddDialog(true)}
|
||||||
style={tw`bg-blue-600 p-4 rounded-full shadow-lg`}
|
style={tw`bg-blue-600 p-4 rounded-full shadow-lg elevation-5`}
|
||||||
>
|
>
|
||||||
<MaterialIcons name="add" size={24} color="white" />
|
<MaterialIcons name="add" size={24} color="white" />
|
||||||
</TouchableOpacity>
|
</MyTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Add Product Dialog */}
|
{/* Add Product Dialog */}
|
||||||
<BottomDialog
|
<BottomDialog
|
||||||
open={showAddDialog}
|
open={showAddDialog}
|
||||||
onClose={() => setShowAddDialog(false)}
|
onClose={() => {
|
||||||
|
setShowAddDialog(false);
|
||||||
|
setSelectedProductIds([]);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<View style={tw`pb-8 pt-2 px-4`}>
|
<View style={tw`pb-8 pt-2 px-4`}>
|
||||||
<View style={tw`items-center mb-6`}>
|
<View style={tw`items-center mb-6`}>
|
||||||
<View style={tw`w-12 h-1.5 bg-gray-200 rounded-full mb-4`} />
|
<View style={tw`w-12 h-1.5 bg-gray-200 rounded-full mb-4`} />
|
||||||
<MyText style={tw`text-lg font-bold text-gray-900`}>
|
<MyText style={tw`text-lg font-bold text-gray-900`}>
|
||||||
Add Popular Item
|
Add Popular Items
|
||||||
</MyText>
|
</MyText>
|
||||||
<MyText style={tw`text-sm text-gray-500`}>
|
<MyText style={tw`text-sm text-gray-500`}>
|
||||||
Select a product to add to popular items
|
Select products to add to popular items
|
||||||
</MyText>
|
</MyText>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
@ -398,41 +411,43 @@ export default function CustomizePopularItems() {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<ProductsSelector
|
<ProductsSelector
|
||||||
value={selectedProductId || 0}
|
value={selectedProductIds}
|
||||||
onChange={(val) => setSelectedProductId(val as number)}
|
onChange={(val) => setSelectedProductIds(val as number[])}
|
||||||
multiple={false}
|
multiple={true}
|
||||||
label="Select Product"
|
label="Select Products"
|
||||||
placeholder="Choose a product..."
|
placeholder="Choose products..."
|
||||||
labelFormat={(product) => `${product.name} - ₹${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`}>
|
||||||
<TouchableOpacity
|
<MyTouchableOpacity
|
||||||
onPress={() => setShowAddDialog(false)}
|
onPress={() => setShowAddDialog(false)}
|
||||||
style={tw`flex-1 bg-gray-100 p-3 rounded-lg`}
|
style={tw`flex-1 bg-gray-100 p-3 rounded-lg`}
|
||||||
>
|
>
|
||||||
<MyText style={tw`text-gray-700 text-center font-semibold`}>
|
<MyText style={tw`text-gray-700 text-center font-semibold`}>
|
||||||
Cancel
|
Cancel
|
||||||
</MyText>
|
</MyText>
|
||||||
</TouchableOpacity>
|
</MyTouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
<MyTouchableOpacity
|
||||||
onPress={handleAddProduct}
|
onPress={handleAddProduct}
|
||||||
disabled={!selectedProductId}
|
disabled={selectedProductIds.length === 0}
|
||||||
style={tw`flex-1 ${
|
style={tw`flex-1 ${
|
||||||
selectedProductId ? 'bg-blue-600' : 'bg-gray-300'
|
selectedProductIds.length > 0 ? 'bg-blue-600' : 'bg-gray-300'
|
||||||
} p-3 rounded-lg`}
|
} p-3 rounded-lg`}
|
||||||
>
|
>
|
||||||
<MyText style={tw`text-white text-center font-semibold`}>
|
<MyText style={tw`text-white text-center font-semibold`}>
|
||||||
Add Product
|
{selectedProductIds.length > 0
|
||||||
|
? `Add ${selectedProductIds.length} Product${selectedProductIds.length > 1 ? 's' : ''}`
|
||||||
|
: 'Add Products'}
|
||||||
</MyText>
|
</MyText>
|
||||||
</TouchableOpacity>
|
</MyTouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</BottomDialog>
|
</BottomDialog>
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</AppContainer>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -44,7 +44,6 @@ export default function EditProduct() {
|
||||||
tagIds: values.tagIds,
|
tagIds: values.tagIds,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log({payload})
|
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
Object.entries(payload).forEach(([key, value]) => {
|
Object.entries(payload).forEach(([key, value]) => {
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,6 @@ const VendorSnippetForm: React.FC<VendorSnippetFormProps> = ({
|
||||||
},
|
},
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
try {
|
try {
|
||||||
console.log({values})
|
|
||||||
|
|
||||||
const submitData = {
|
const submitData = {
|
||||||
snippetCode: values.snippetCode,
|
snippetCode: values.snippetCode,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
ENV_MODE=PROD
|
ENV_MODE=PROD
|
||||||
DATABASE_URL=postgresql://postgres:meatfarmer_master_password@57.128.212.174:7447/meatfarmer #technocracy
|
# DATABASE_URL=postgresql://postgres:meatfarmer_master_password@57.128.212.174:7447/meatfarmer #technocracy
|
||||||
# DATABASE_URL=postgres://postgres:meatfarmer_master_password@5.223.55.14:7447/meatfarmer #hetzner
|
DATABASE_URL=postgres://postgres:meatfarmer_master_password@5.223.55.14:7447/meatfarmer #hetzner
|
||||||
PHONE_PE_BASE_URL=https://api-preprod.phonepe.com/
|
PHONE_PE_BASE_URL=https://api-preprod.phonepe.com/
|
||||||
PHONE_PE_CLIENT_ID=TEST-M23F2IGP34ZAR_25090
|
PHONE_PE_CLIENT_ID=TEST-M23F2IGP34ZAR_25090
|
||||||
PHONE_PE_CLIENT_VERSION=1
|
PHONE_PE_CLIENT_VERSION=1
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -19,7 +19,7 @@ import { seed } from 'src/db/seed';
|
||||||
import './src/jobs/jobs-index';
|
import './src/jobs/jobs-index';
|
||||||
import { startAutomatedJobs } from './src/lib/automatedJobs';
|
import { startAutomatedJobs } from './src/lib/automatedJobs';
|
||||||
|
|
||||||
// seed()
|
seed()
|
||||||
initFunc()
|
initFunc()
|
||||||
startAutomatedJobs()
|
startAutomatedJobs()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,6 @@ export const updateProduct = async (req: Request, res: Response) => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { name, shortDescription, longDescription, unitId, storeId, price, marketPrice, incrementStep, productQuantity, isSuspended, isFlashAvailable, flashPrice, deals:dealsRaw, imagesToDelete:imagesToDeleteRaw, tagIds } = req.body;
|
const { name, shortDescription, longDescription, unitId, storeId, price, marketPrice, incrementStep, productQuantity, isSuspended, isFlashAvailable, flashPrice, deals:dealsRaw, imagesToDelete:imagesToDeleteRaw, tagIds } = req.body;
|
||||||
|
|
||||||
console.log({productQuantity})
|
|
||||||
|
|
||||||
const deals = dealsRaw ? JSON.parse(dealsRaw) : null;
|
const deals = dealsRaw ? JSON.parse(dealsRaw) : null;
|
||||||
const imagesToDelete = imagesToDeleteRaw ? JSON.parse(imagesToDeleteRaw) : [];
|
const imagesToDelete = imagesToDeleteRaw ? JSON.parse(imagesToDeleteRaw) : [];
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,7 @@ export async function seed() {
|
||||||
{ key: CONST_KEYS.flashFreeDeliveryThreshold, value: 500 },
|
{ key: CONST_KEYS.flashFreeDeliveryThreshold, value: 500 },
|
||||||
{ key: CONST_KEYS.flashDeliveryCharge, value: 69 },
|
{ key: CONST_KEYS.flashDeliveryCharge, value: 69 },
|
||||||
{ key: CONST_KEYS.popularItems, value: [] },
|
{ key: CONST_KEYS.popularItems, value: [] },
|
||||||
|
{ key: CONST_KEYS.allItemsOrder, value: [] },
|
||||||
{ key: CONST_KEYS.versionNum, value: '1.1.0' },
|
{ key: CONST_KEYS.versionNum, value: '1.1.0' },
|
||||||
{ key: CONST_KEYS.playStoreUrl, value: 'https://play.google.com/store/apps/details?id=in.freshyo.app' },
|
{ key: CONST_KEYS.playStoreUrl, value: 'https://play.google.com/store/apps/details?id=in.freshyo.app' },
|
||||||
{ key: CONST_KEYS.appStoreUrl, value: 'https://play.google.com/store/apps/details?id=in.freshyo.app' },
|
{ key: CONST_KEYS.appStoreUrl, value: 'https://play.google.com/store/apps/details?id=in.freshyo.app' },
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ export const CONST_KEYS = {
|
||||||
flashDeliveryCharge: 'flashDeliveryCharge',
|
flashDeliveryCharge: 'flashDeliveryCharge',
|
||||||
platformFeePercent: 'platformFeePercent',
|
platformFeePercent: 'platformFeePercent',
|
||||||
taxRate: 'taxRate',
|
taxRate: 'taxRate',
|
||||||
|
tester: 'tester',
|
||||||
minOrderAmountForCoupon: 'minOrderAmountForCoupon',
|
minOrderAmountForCoupon: 'minOrderAmountForCoupon',
|
||||||
maxCouponDiscount: 'maxCouponDiscount',
|
maxCouponDiscount: 'maxCouponDiscount',
|
||||||
flashDeliverySlotId: 'flashDeliverySlotId',
|
flashDeliverySlotId: 'flashDeliverySlotId',
|
||||||
|
|
@ -14,6 +15,7 @@ export const CONST_KEYS = {
|
||||||
playStoreUrl: 'playStoreUrl',
|
playStoreUrl: 'playStoreUrl',
|
||||||
appStoreUrl: 'appStoreUrl',
|
appStoreUrl: 'appStoreUrl',
|
||||||
popularItems: 'popularItems',
|
popularItems: 'popularItems',
|
||||||
|
allItemsOrder: 'allItemsOrder',
|
||||||
isFlashDeliveryEnabled: 'isFlashDeliveryEnabled',
|
isFlashDeliveryEnabled: 'isFlashDeliveryEnabled',
|
||||||
supportMobile: 'supportMobile',
|
supportMobile: 'supportMobile',
|
||||||
supportEmail: 'supportEmail',
|
supportEmail: 'supportEmail',
|
||||||
|
|
@ -27,6 +29,7 @@ export const CONST_LABELS: Record<ConstKey, string> = {
|
||||||
flashDeliveryCharge: 'Flash Delivery Charge',
|
flashDeliveryCharge: 'Flash Delivery Charge',
|
||||||
platformFeePercent: 'Platform Fee Percent',
|
platformFeePercent: 'Platform Fee Percent',
|
||||||
taxRate: 'Tax Rate',
|
taxRate: 'Tax Rate',
|
||||||
|
tester: 'Tester',
|
||||||
minOrderAmountForCoupon: 'Minimum Order Amount for Coupon',
|
minOrderAmountForCoupon: 'Minimum Order Amount for Coupon',
|
||||||
maxCouponDiscount: 'Maximum Coupon Discount',
|
maxCouponDiscount: 'Maximum Coupon Discount',
|
||||||
flashDeliverySlotId: 'Flash Delivery Slot ID',
|
flashDeliverySlotId: 'Flash Delivery Slot ID',
|
||||||
|
|
@ -35,6 +38,7 @@ export const CONST_LABELS: Record<ConstKey, string> = {
|
||||||
playStoreUrl: 'Play Store URL',
|
playStoreUrl: 'Play Store URL',
|
||||||
appStoreUrl: 'App Store URL',
|
appStoreUrl: 'App Store URL',
|
||||||
popularItems: 'Popular Items',
|
popularItems: 'Popular Items',
|
||||||
|
allItemsOrder: 'All Items Order',
|
||||||
isFlashDeliveryEnabled: 'Enable Flash Delivery',
|
isFlashDeliveryEnabled: 'Enable Flash Delivery',
|
||||||
supportMobile: 'Support Mobile',
|
supportMobile: 'Support Mobile',
|
||||||
supportEmail: 'Support Email',
|
supportEmail: 'Support Email',
|
||||||
|
|
|
||||||
|
|
@ -431,7 +431,6 @@ export const orderRouter = router({
|
||||||
.input(updateOrderItemPackagingSchema)
|
.input(updateOrderItemPackagingSchema)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { orderItemId, isPackaged, isPackageVerified } = input;
|
const { orderItemId, isPackaged, isPackageVerified } = input;
|
||||||
console.log({ orderItemId, isPackaged, isPackageVerified });
|
|
||||||
|
|
||||||
// Validate that orderItem exists
|
// Validate that orderItem exists
|
||||||
const orderItem = await db.query.orderItems.findFirst({
|
const orderItem = await db.query.orderItems.findFirst({
|
||||||
|
|
|
||||||
|
|
@ -543,9 +543,6 @@ export const slotsRouter = router({
|
||||||
|
|
||||||
const { id, deliverySequence } = input;
|
const { id, deliverySequence } = input;
|
||||||
|
|
||||||
|
|
||||||
console.log({deliverySequence})
|
|
||||||
|
|
||||||
const [updatedSlot] = await db
|
const [updatedSlot] = await db
|
||||||
.update(deliverySlotInfo)
|
.update(deliverySlotInfo)
|
||||||
.set({ deliverySequence })
|
.set({ deliverySequence })
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,6 @@ export const vendorSnippetsRouter = router({
|
||||||
.input(updateSnippetSchema)
|
.input(updateSnippetSchema)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { id, updates } = input;
|
const { id, updates } = input;
|
||||||
console.log({updates})
|
|
||||||
|
|
||||||
// Check if snippet exists
|
// Check if snippet exists
|
||||||
const existingSnippet = await db.query.vendorSnippets.findFirst({
|
const existingSnippet = await db.query.vendorSnippets.findFirst({
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,6 @@ export const bannerRouter = router({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log({bannersWithSignedUrls})
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -713,8 +713,6 @@ export const orderRouter = router({
|
||||||
const userId = ctx.user.userId;
|
const userId = ctx.user.userId;
|
||||||
const { id, reason } = input;
|
const { id, reason } = input;
|
||||||
|
|
||||||
console.log({id, reason})
|
|
||||||
|
|
||||||
// Check if order exists and belongs to user
|
// Check if order exists and belongs to user
|
||||||
const order = await db.query.orders.findFirst({
|
const order = await db.query.orders.findFirst({
|
||||||
where: eq(orders.id, Number(id)),
|
where: eq(orders.id, Number(id)),
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ export const userRouter = router({
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const userId = ctx.user.userId;
|
const userId = ctx.user.userId;
|
||||||
const { token } = input;
|
const { token } = input;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new ApiError('User not authenticated', 401);
|
throw new ApiError('User not authenticated', 401);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,8 +137,6 @@ function ErrorPage() {
|
||||||
export function UserHomeRoute() {
|
export function UserHomeRoute() {
|
||||||
const { user } = useUserStore()
|
const { user } = useUserStore()
|
||||||
|
|
||||||
console.log({user})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-6xl mx-auto space-y-6">
|
<div className="max-w-6xl mx-auto space-y-6">
|
||||||
{(user?.role?.name === 'admin' || user?.role?.name === 'super_admin') && <AdminDashboard />}
|
{(user?.role?.name === 'admin' || user?.role?.name === 'super_admin') && <AdminDashboard />}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "Freshyo",
|
"name": "Freshyo",
|
||||||
"slug": "freshyo",
|
"slug": "freshyo",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/images/freshyo-logo.png",
|
"icon": "./assets/images/freshyo-logo.png",
|
||||||
"scheme": "freshyo",
|
"scheme": "freshyo",
|
||||||
|
|
@ -67,7 +67,8 @@
|
||||||
"backgroundColor": "#fff0f6"
|
"backgroundColor": "#fff0f6"
|
||||||
},
|
},
|
||||||
"edgeToEdgeEnabled": true,
|
"edgeToEdgeEnabled": true,
|
||||||
"package": "in.freshyo.app"
|
"package": "in.freshyo.app",
|
||||||
|
"googleServicesFile": "./google-services.json"
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"bundler": "metro",
|
"bundler": "metro",
|
||||||
|
|
@ -85,7 +86,8 @@
|
||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"expo-secure-store"
|
"expo-secure-store",
|
||||||
|
"expo-notifications"
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true
|
"typedRoutes": true
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
||||||
import Ionicons from '@expo/vector-icons/Ionicons';
|
import Ionicons from '@expo/vector-icons/Ionicons';
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import ComplaintForm from "@/components/ComplaintForm";
|
import ComplaintForm from "@/components/ComplaintForm";
|
||||||
|
import { orderStatusManipulator } from "@/src/lib/string-manipulators";
|
||||||
|
|
||||||
export default function OrderDetails() {
|
export default function OrderDetails() {
|
||||||
const { id } = useLocalSearchParams<{ id: string }>();
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
|
|
@ -145,7 +146,7 @@ export default function OrderDetails() {
|
||||||
<View style={tw`flex-row items-center gap-2`}>
|
<View style={tw`flex-row items-center gap-2`}>
|
||||||
<View style={[tw`px-3 py-1 rounded-full`, { backgroundColor: statusConfig.color + '10' }]}>
|
<View style={[tw`px-3 py-1 rounded-full`, { backgroundColor: statusConfig.color + '10' }]}>
|
||||||
<MyText style={[tw`text-[10px] font-bold uppercase`, { color: statusConfig.color }]}>
|
<MyText style={[tw`text-[10px] font-bold uppercase`, { color: statusConfig.color }]}>
|
||||||
{statusConfig.label}
|
{orderStatusManipulator(statusConfig.label)}
|
||||||
</MyText>
|
</MyText>
|
||||||
</View>
|
</View>
|
||||||
{order.isFlashDelivery && (
|
{order.isFlashDelivery && (
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { trpc } from '@/src/trpc-client';
|
||||||
// import RazorpayCheckout from 'react-native-razorpay';
|
// import RazorpayCheckout from 'react-native-razorpay';
|
||||||
import OrderMenu from '@/components/OrderMenu';
|
import OrderMenu from '@/components/OrderMenu';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { orderStatusManipulator } from '@/src/lib/string-manipulators';
|
||||||
|
|
||||||
// Type definitions
|
// Type definitions
|
||||||
interface OrderItem {
|
interface OrderItem {
|
||||||
|
|
@ -136,9 +137,9 @@ const OrderItem: React.FC<OrderItemProps> = ({
|
||||||
{item.orderStatus.toLowerCase() === 'cancelled' && (
|
{item.orderStatus.toLowerCase() === 'cancelled' && (
|
||||||
<View style={[tw`flex-row items-center px-3 py-1.5 rounded-full border ${mainStatus.bg} ${mainStatus.border}`]}>
|
<View style={[tw`flex-row items-center px-3 py-1.5 rounded-full border ${mainStatus.bg} ${mainStatus.border}`]}>
|
||||||
<MaterialIcons name={mainStatus.icon as any} size={14} color={mainStatus.color} />
|
<MaterialIcons name={mainStatus.icon as any} size={14} color={mainStatus.color} />
|
||||||
<MyText style={[tw`text-[11px] font-bold ml-1.5 uppercase tracking-wide`, { color: mainStatus.color }]}>
|
<MyText style={[tw`text-[11px] font-bold ml-1.5 uppercase tracking-wide`, { color: mainStatus.color }]}>
|
||||||
{mainStatus.label}
|
{orderStatusManipulator(mainStatus.label)}
|
||||||
</MyText>
|
</MyText>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{item.isFlashDelivery && (
|
{item.isFlashDelivery && (
|
||||||
|
|
@ -265,7 +266,7 @@ const OrderItem: React.FC<OrderItemProps> = ({
|
||||||
{item.isFlashDelivery ? "1 Hr Delivery:" : "Shipping Status:"}
|
{item.isFlashDelivery ? "1 Hr Delivery:" : "Shipping Status:"}
|
||||||
</MyText>
|
</MyText>
|
||||||
<MyText style={[tw`text-xs font-bold`, item.isFlashDelivery ? tw`text-amber-700` : { color: deliveryStatus.color }]} numberOfLines={1}>
|
<MyText style={[tw`text-xs font-bold`, item.isFlashDelivery ? tw`text-amber-700` : { color: deliveryStatus.color }]} numberOfLines={1}>
|
||||||
{item.deliveryStatus}
|
{orderStatusManipulator(item.deliveryStatus)}
|
||||||
</MyText>
|
</MyText>
|
||||||
</View>
|
</View>
|
||||||
{item.isFlashDelivery && (
|
{item.isFlashDelivery && (
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ export default function RootLayout() {
|
||||||
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
|
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('from layout')
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
Appearance.setColorScheme('light')
|
Appearance.setColorScheme('light')
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { MaterialIcons, Ionicons } from '@expo/vector-icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { trpc } from '@/src/trpc-client';
|
import { trpc } from '@/src/trpc-client';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
|
import { orderStatusManipulator } from '@/src/lib/string-manipulators';
|
||||||
|
|
||||||
interface OrderItem {
|
interface OrderItem {
|
||||||
productName: string;
|
productName: string;
|
||||||
|
|
@ -126,7 +127,7 @@ export default function NextOrderGlimpse() {
|
||||||
</MyText>
|
</MyText>
|
||||||
</View>
|
</View>
|
||||||
<View style={tw`bg-white px-2 py-1 rounded-lg border border-amber-200`}>
|
<View style={tw`bg-white px-2 py-1 rounded-lg border border-amber-200`}>
|
||||||
<MyText style={tw`text-[10px] font-bold text-amber-700 uppercase`}>{nextOrder.deliveryStatus}</MyText>
|
<MyText style={tw`text-[10px] font-bold text-amber-700 uppercase`}>{orderStatusManipulator(nextOrder.deliveryStatus)}</MyText>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,10 @@ const ProductCard: React.FC<ProductCardProps> = ({
|
||||||
const slotId = getQuickestSlot(item.id);
|
const slotId = getQuickestSlot(item.id);
|
||||||
const displayIsOutOfStock = item.isOutOfStock || !slotId;
|
const displayIsOutOfStock = item.isOutOfStock || !slotId;
|
||||||
|
|
||||||
|
// if(item.name.startsWith('Mutton Curry Cut')) {
|
||||||
|
// console.log({slotId, displayIsOutOfStock})
|
||||||
|
// }
|
||||||
|
|
||||||
// Return null if nullIfNotAvailable is true and the product is out of stock
|
// Return null if nullIfNotAvailable is true and the product is out of stock
|
||||||
if (nullIfNotAvailable && displayIsOutOfStock) {
|
if (nullIfNotAvailable && displayIsOutOfStock) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -397,7 +397,14 @@ export function SlotProducts({ slotId:slotIdParent, storeId:storeIdParent, baseU
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredProducts: any[] = storeIdNum ? productsQuery?.data?.filter(p => p.store?.id === storeIdNum) || [] : slotQuery.data.products;
|
// Create a Set of product IDs from slot data for O(1) lookup
|
||||||
|
const slotProductIds = new Set(slotQuery.data.products?.map((p: any) => p.id) || []);
|
||||||
|
|
||||||
|
const filteredProducts: any[] = storeIdNum
|
||||||
|
? productsQuery?.data?.filter(p =>
|
||||||
|
p.store?.id === storeIdNum && slotProductIds.has(p.id)
|
||||||
|
) || []
|
||||||
|
: slotQuery.data.products;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={tw`flex-1`}>
|
<View style={tw`flex-1`}>
|
||||||
|
|
@ -481,11 +488,18 @@ export function FlashDeliveryProducts({ storeId:storeIdParent, baseUrl, onProduc
|
||||||
// Filter products to only include those eligible for flash delivery
|
// Filter products to only include those eligible for flash delivery
|
||||||
let flashProducts: any[] = [];
|
let flashProducts: any[] = [];
|
||||||
if (storeIdNum) {
|
if (storeIdNum) {
|
||||||
// Filter by store and flash availability
|
// Filter by store, flash availability, and stock status
|
||||||
flashProducts = productsQuery?.data?.filter(p => p.store?.id === storeIdNum && p.isFlashAvailable) || [];
|
flashProducts = productsQuery?.data?.filter(p =>
|
||||||
|
p.store?.id === storeIdNum &&
|
||||||
|
p.isFlashAvailable &&
|
||||||
|
!p.isOutOfStock
|
||||||
|
) || [];
|
||||||
} else {
|
} else {
|
||||||
// Show all flash-available products (no slot filtering)
|
// Show all flash-available products that are in stock
|
||||||
flashProducts = productsQuery?.data?.filter(p => p.isFlashAvailable) || [];
|
flashProducts = productsQuery?.data?.filter(p =>
|
||||||
|
p.isFlashAvailable &&
|
||||||
|
!p.isOutOfStock
|
||||||
|
) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
29
apps/user-ui/google-services.json
Normal file
29
apps/user-ui/google-services.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "535753078248",
|
||||||
|
"project_id": "freshyo-cefb2",
|
||||||
|
"storage_bucket": "freshyo-cefb2.firebasestorage.app"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:535753078248:android:d00803550e29eb3238605d",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "in.freshyo.app"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyDW4w6vGDrZnkf8vgwdQObBLnT_iucs2H8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,8 @@ import dayjs from 'dayjs';
|
||||||
|
|
||||||
export function useProductSlotIdentifier() {
|
export function useProductSlotIdentifier() {
|
||||||
// Fetch all slots with products
|
// Fetch all slots with products
|
||||||
const { data: slotsData } = trpc.user.slots.getSlotsWithProducts.useQuery();
|
const { data: slotsData, isLoading: isProductsLoading } = trpc.user.slots.getSlotsWithProducts.useQuery();
|
||||||
|
|
||||||
|
|
||||||
const productSlotsMap = new Map<number, number[]>();
|
const productSlotsMap = new Map<number, number[]>();
|
||||||
|
|
||||||
|
|
@ -24,16 +25,18 @@ export function useProductSlotIdentifier() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const getQuickestSlot = (productId: number): number | null => {
|
const getQuickestSlot = (productId: number): number | null => {
|
||||||
|
|
||||||
if (!slotsData?.slots) return null;
|
if (!slotsData?.slots) return null;
|
||||||
|
|
||||||
const now = dayjs();
|
const now = dayjs();
|
||||||
|
|
||||||
// Find slots that contain this product and have future delivery time
|
// Find slots that contain this product and have future delivery time
|
||||||
const availableSlots = slotsData.slots.filter(slot =>
|
const availableSlots = slotsData.slots.filter(slot =>
|
||||||
slot.products.some(product => product.id === productId) &&
|
slot.products.some(product => product.id === productId) &&
|
||||||
dayjs(slot.deliveryTime).isAfter(now)
|
dayjs(slot.deliveryTime).isAfter(now)
|
||||||
);
|
);
|
||||||
|
// if(productId === 98)
|
||||||
|
// console.log(JSON.stringify(slotsData))
|
||||||
if (availableSlots.length === 0) return null;
|
if (availableSlots.length === 0) return null;
|
||||||
|
|
||||||
// Return earliest slot ID (sorted by delivery time)
|
// Return earliest slot ID (sorted by delivery time)
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ function NotifChecker(props: Props) {
|
||||||
const { notifPermission, expoPushToken } = useNotification();
|
const { notifPermission, expoPushToken } = useNotification();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
console.log({isAuthenticated, expoPushToken, notifPermission});
|
||||||
if (isAuthenticated && expoPushToken && notifPermission === 'granted') {
|
if (isAuthenticated && expoPushToken && notifPermission === 'granted') {
|
||||||
savePushTokenMutation.mutate(
|
savePushTokenMutation.mutate(
|
||||||
{ token: expoPushToken },
|
{ token: expoPushToken },
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,13 @@ export const NotificationProvider: React.FC<NotificationProviderProps> = ({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
registerForPushNotificationsAsync()
|
registerForPushNotificationsAsync()
|
||||||
.then((token) => {
|
.then((token) => {
|
||||||
|
|
||||||
|
|
||||||
setExpoPushToken(token);
|
setExpoPushToken(token);
|
||||||
setNotifPermission("granted");
|
setNotifPermission("granted");
|
||||||
})
|
})
|
||||||
.catch((errorRaw) => {
|
.catch((errorRaw) => {
|
||||||
|
|
||||||
|
|
||||||
const err = String(errorRaw).slice(7); //remove the "Error: " string component in beginning
|
const err = String(errorRaw).slice(7); //remove the "Error: " string component in beginning
|
||||||
|
|
||||||
|
|
|
||||||
27
apps/user-ui/src/lib/string-manipulators.ts
Normal file
27
apps/user-ui/src/lib/string-manipulators.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* String manipulation utilities for the user UI
|
||||||
|
*
|
||||||
|
* This file contains helper functions for transforming and formatting
|
||||||
|
* strings throughout the application. These utilities ensure consistent
|
||||||
|
* display formatting across all components.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms delivery status for display.
|
||||||
|
*
|
||||||
|
* Why: The term "Pending" can be confusing for users as it implies
|
||||||
|
* uncertainty. "Confirmed" provides clearer communication that the
|
||||||
|
* order has been received and acknowledged by the system.
|
||||||
|
*
|
||||||
|
* @param status - The raw delivery status from API
|
||||||
|
* @returns User-friendly status label
|
||||||
|
* @example
|
||||||
|
* orderStatusManipulator('pending') // returns 'Confirmed'
|
||||||
|
* orderStatusManipulator('packaged') // returns 'packaged' (unchanged)
|
||||||
|
*/
|
||||||
|
export const orderStatusManipulator = (status: string): string => {
|
||||||
|
if (status.toLowerCase() === 'pending') {
|
||||||
|
return 'Confirmed';
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
};
|
||||||
|
|
@ -63,8 +63,8 @@ const isDevMode = Constants.executionEnvironment !== "standalone";
|
||||||
// const BASE_API_URL = API_URL;
|
// const BASE_API_URL = API_URL;
|
||||||
// const BASE_API_URL = 'http://10.0.2.2:4000';
|
// const BASE_API_URL = 'http://10.0.2.2:4000';
|
||||||
// const BASE_API_URL = 'http://192.168.100.101:4000';
|
// const BASE_API_URL = 'http://192.168.100.101:4000';
|
||||||
// const BASE_API_URL = 'http://192.168.1.3:4000';
|
const BASE_API_URL = 'http://192.168.1.3:4000';
|
||||||
let BASE_API_URL = "https://mf.freshyo.in";
|
// let BASE_API_URL = "https://mf.freshyo.in";
|
||||||
// let BASE_API_URL = 'http://192.168.100.104:4000';
|
// let BASE_API_URL = 'http://192.168.100.104:4000';
|
||||||
// let BASE_API_URL = 'http://192.168.29.176:4000';
|
// let BASE_API_URL = 'http://192.168.29.176:4000';
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue