enh
This commit is contained in:
parent
55c41fa0af
commit
d599c2e004
11 changed files with 437 additions and 11 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,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="all-items-order"
|
||||
options={{
|
||||
title: "All Items Order",
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
</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',
|
||||
appStoreUrl: 'App Store URL',
|
||||
popularItems: 'Popular Items',
|
||||
allItemsOrder: 'All Items Order',
|
||||
isFlashDeliveryEnabled: 'Enable Flash Delivery',
|
||||
supportMobile: 'Support Mobile',
|
||||
supportEmail: 'Support Email',
|
||||
|
|
@ -48,6 +49,7 @@ const ConstantInput: React.FC<ConstantInputProps> = ({ constant, setFieldValue,
|
|||
|
||||
// Special handling for popularItems - show navigation button instead of input
|
||||
if (constant.key === 'popularItems') {
|
||||
console.log('key is allItemsOrder')
|
||||
return (
|
||||
<View>
|
||||
<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
|
||||
if (typeof constant.value === 'boolean') {
|
||||
return (
|
||||
|
|
@ -134,6 +158,7 @@ export default function CustomizeApp() {
|
|||
const { data: constants, isLoading: isLoadingConstants, refetch } = trpc.admin.const.getConstants.useQuery();
|
||||
const { mutate: updateConstants, isPending: isUpdating } = trpc.admin.const.updateConstants.useMutation();
|
||||
|
||||
|
||||
const handleSubmit = (values: ConstantFormData) => {
|
||||
// Filter out constants that haven't changed
|
||||
const changedConstants = values.constants.filter((constant, index) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
ENV_MODE=PROD
|
||||
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=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
|
||||
PHONE_PE_BASE_URL=https://api-preprod.phonepe.com/
|
||||
PHONE_PE_CLIENT_ID=TEST-M23F2IGP34ZAR_25090
|
||||
PHONE_PE_CLIENT_VERSION=1
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { seed } from 'src/db/seed';
|
|||
import './src/jobs/jobs-index';
|
||||
import { startAutomatedJobs } from './src/lib/automatedJobs';
|
||||
|
||||
// seed()
|
||||
seed()
|
||||
initFunc()
|
||||
startAutomatedJobs()
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ export async function seed() {
|
|||
{ key: CONST_KEYS.flashFreeDeliveryThreshold, value: 500 },
|
||||
{ key: CONST_KEYS.flashDeliveryCharge, value: 69 },
|
||||
{ key: CONST_KEYS.popularItems, value: [] },
|
||||
{ key: CONST_KEYS.allItemsOrder, value: [] },
|
||||
{ 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.appStoreUrl, value: 'https://play.google.com/store/apps/details?id=in.freshyo.app' },
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export const CONST_KEYS = {
|
|||
flashDeliveryCharge: 'flashDeliveryCharge',
|
||||
platformFeePercent: 'platformFeePercent',
|
||||
taxRate: 'taxRate',
|
||||
tester: 'tester',
|
||||
minOrderAmountForCoupon: 'minOrderAmountForCoupon',
|
||||
maxCouponDiscount: 'maxCouponDiscount',
|
||||
flashDeliverySlotId: 'flashDeliverySlotId',
|
||||
|
|
@ -14,6 +15,7 @@ export const CONST_KEYS = {
|
|||
playStoreUrl: 'playStoreUrl',
|
||||
appStoreUrl: 'appStoreUrl',
|
||||
popularItems: 'popularItems',
|
||||
allItemsOrder: 'allItemsOrder',
|
||||
isFlashDeliveryEnabled: 'isFlashDeliveryEnabled',
|
||||
supportMobile: 'supportMobile',
|
||||
supportEmail: 'supportEmail',
|
||||
|
|
@ -27,6 +29,7 @@ export const CONST_LABELS: Record<ConstKey, string> = {
|
|||
flashDeliveryCharge: 'Flash Delivery Charge',
|
||||
platformFeePercent: 'Platform Fee Percent',
|
||||
taxRate: 'Tax Rate',
|
||||
tester: 'Tester',
|
||||
minOrderAmountForCoupon: 'Minimum Order Amount for Coupon',
|
||||
maxCouponDiscount: 'Maximum Coupon Discount',
|
||||
flashDeliverySlotId: 'Flash Delivery Slot ID',
|
||||
|
|
@ -35,6 +38,7 @@ export const CONST_LABELS: Record<ConstKey, string> = {
|
|||
playStoreUrl: 'Play Store URL',
|
||||
appStoreUrl: 'App Store URL',
|
||||
popularItems: 'Popular Items',
|
||||
allItemsOrder: 'All Items Order',
|
||||
isFlashDeliveryEnabled: 'Enable Flash Delivery',
|
||||
supportMobile: 'Support Mobile',
|
||||
supportEmail: 'Support Email',
|
||||
|
|
|
|||
|
|
@ -115,8 +115,6 @@ export const userRouter = router({
|
|||
const userId = ctx.user.userId;
|
||||
const { token } = input;
|
||||
|
||||
console.log({token})
|
||||
|
||||
if (!userId) {
|
||||
throw new ApiError('User not authenticated', 401);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,13 +53,13 @@ export const NotificationProvider: React.FC<NotificationProviderProps> = ({
|
|||
useEffect(() => {
|
||||
registerForPushNotificationsAsync()
|
||||
.then((token) => {
|
||||
console.log({token})
|
||||
|
||||
|
||||
setExpoPushToken(token);
|
||||
setNotifPermission("granted");
|
||||
})
|
||||
.catch((errorRaw) => {
|
||||
console.log({errorRaw})
|
||||
|
||||
|
||||
const err = String(errorRaw).slice(7); //remove the "Error: " string component in beginning
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue