freshyo/apps/admin-ui/app/(drawer)/customize-app/popular-items.tsx
2026-01-24 00:13:15 +05:30

439 lines
No EOL
14 KiB
TypeScript

import React, { useState, useEffect, useMemo } from "react";
import {
View,
TouchableOpacity,
Alert,
ActivityIndicator,
} from "react-native";
import { Image } from "expo-image";
import DraggableFlatList, {
RenderItemParams,
ScaleDecorator,
} from "react-native-draggable-flatlist";
import {
AppContainer,
MyText,
tw,
BottomDialog,
BottomDropdown,
} 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";
interface PopularProduct {
id: number;
name: string;
shortDescription: string | null;
price: string;
marketPrice: string | null;
unit: string;
incrementStep: number;
productQuantity: number;
storeId: number | null;
isOutOfStock: boolean;
nextDeliveryDate: string | null;
images: string[];
}
interface ProductItemProps {
item: PopularProduct;
drag: () => void;
isActive: boolean;
onDelete: (id: number) => void;
}
const ProductItem: React.FC<ProductItemProps> = ({
item,
drag,
isActive,
onDelete,
}) => {
return (
<ScaleDecorator>
<TouchableOpacity
onLongPress={drag}
activeOpacity={1}
style={tw`mx-4 my-2`}
>
<View
style={[
tw`bg-white p-4 rounded-xl border`,
isActive
? tw`shadow-xl border-blue-500 z-50`
: tw`shadow-sm border-gray-100`,
]}
>
<View style={tw`flex-row items-center`}>
{/* Drag Handle */}
<View style={tw`mr-4`}>
<MaterialIcons
name="drag-indicator"
size={24}
color={isActive ? "#3b82f6" : "#9ca3af"}
/>
</View>
{/* Product Image */}
{item.images?.[0] ? (
<Image
source={{ uri: item.images[0] }}
style={tw`w-12 h-12 rounded-lg mr-4`}
resizeMode="cover"
/>
) : (
<View style={tw`w-12 h-12 rounded-lg bg-gray-100 mr-4 items-center justify-center`}>
<MaterialIcons name="image" size={20} color="#9ca3af" />
</View>
)}
{/* Product Info */}
<View style={tw`flex-1`}>
<MyText style={tw`font-semibold text-gray-900 text-base`} numberOfLines={2}>
{item.name}
</MyText>
<MyText style={tw`text-green-600 font-bold text-sm mt-1`}>
{item.price}
</MyText>
</View>
{/* Delete Button */}
<TouchableOpacity
onPress={() => onDelete(item.id)}
style={tw`p-2 ml-2`}
>
<MaterialIcons name="delete" size={20} color="#ef4444" />
</TouchableOpacity>
</View>
</View>
</TouchableOpacity>
</ScaleDecorator>
);
};
export default function CustomizePopularItems() {
const router = useRouter();
const queryClient = useQueryClient();
const [popularProducts, setPopularProducts] = useState<PopularProduct[]>([]);
const [hasChanges, setHasChanges] = useState(false);
const [showAddDialog, setShowAddDialog] = useState(false);
const [selectedProductId, setSelectedProductId] = useState<number | null>(null);
// Get current popular items 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 popular products from constants
useEffect(() => {
if (constants && allProducts?.products) {
const popularItemsConstant = constants.find(c => c.key === 'popularItems');
let popularIds: number[] = [];
if (popularItemsConstant) {
const value = popularItemsConstant.value;
if (Array.isArray(value)) {
// Already an array of IDs
popularIds = value.map((id: any) => parseInt(id));
} else if (typeof value === 'string') {
// Comma-separated string
popularIds = value.split(',').map((id: string) => parseInt(id.trim())).filter(id => !isNaN(id));
}
const popularProds: PopularProduct[] = [];
for (const id of popularIds) {
const product = allProducts.products.find(p => p.id === id);
if (product) {
popularProds.push(product);
}
}
setPopularProducts(popularProds);
}
}
}, [constants, allProducts]);
const handleDragEnd = ({ data }: { data: PopularProduct[] }) => {
setPopularProducts(data);
setHasChanges(true);
};
const handleDelete = (productId: number) => {
Alert.alert(
"Remove Product",
"Are you sure you want to remove this product from popular items?",
[
{ text: "Cancel", style: "cancel" },
{
text: "Remove",
style: "destructive",
onPress: () => {
setPopularProducts(prev => prev.filter(p => p.id !== productId));
setHasChanges(true);
}
}
]
);
};
const handleAddProduct = () => {
if (selectedProductId) {
const product = allProducts?.products.find(p => p.id === selectedProductId);
if (product && !popularProducts.find(p => p.id === product.id)) {
setPopularProducts(prev => [...prev, product as PopularProduct]);
setHasChanges(true);
setSelectedProductId(null);
setShowAddDialog(false);
}
}
};
const handleSave = () => {
const popularIds = popularProducts.map(p => p.id);
updateConstants.mutate(
{
constants: [{
key: 'popularItems',
value: popularIds
}]
},
{
onSuccess: () => {
setHasChanges(false);
Alert.alert('Success', 'Popular items updated successfully!');
queryClient.invalidateQueries({ queryKey: ['const.getConstants'] });
},
onError: (error) => {
Alert.alert('Error', 'Failed to update popular items. Please try again.');
console.error('Update popular items error:', error);
}
}
);
};
const availableProducts = (allProducts?.products || []).filter(
product => !popularProducts.find(p => p.id === product.id)
);
const productOptions = availableProducts.map(product => ({
label: `${product.name} - ₹${product.price}`,
value: product.id,
}));
// Show loading state while data is being fetched
if (isLoadingConstants || isLoadingProducts) {
return (
<AppContainer>
<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`}>
<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`}>Popular Items</MyText>
<View style={tw`w-16`} /> {/* Spacer for centering */}
</View>
{/* Loading Content */}
<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 constants...' : '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`}>
{/* Header */}
<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`}>Popular Items</MyText>
<View style={tw`w-16`} /> {/* Spacer for centering */}
</View>
{/* Error Content */}
<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 constants' : '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 (
<AppContainer>
<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`}>
<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`}>Popular Items</MyText>
<TouchableOpacity
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>
</TouchableOpacity>
</View>
{/* Content */}
{popularProducts.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 popular items configured
</MyText>
<MyText style={tw`text-gray-400 text-center mt-2`}>
Add products to display as popular items
</MyText>
</View>
) : (
<View style={tw`flex-1`}>
<View style={tw`bg-blue-50 px-4 py-2 mb-2`}>
<MyText style={tw`text-blue-700 text-xs text-center`}>
Long press an item to drag and reorder
</MyText>
</View>
<DraggableFlatList
data={popularProducts}
renderItem={({ item, drag, isActive }) => (
<ProductItem
item={item}
drag={drag}
isActive={isActive}
onDelete={handleDelete}
/>
)}
keyExtractor={(item) => item.id.toString()}
onDragEnd={handleDragEnd}
showsVerticalScrollIndicator={false}
contentContainerStyle={tw`pb-8`}
/>
</View>
)}
{/* FAB for Add Product */}
<View style={tw`absolute bottom-4 right-4`}>
<TouchableOpacity
onPress={() => setShowAddDialog(true)}
style={tw`bg-blue-600 p-4 rounded-full shadow-lg`}
>
<MaterialIcons name="add" size={24} color="white" />
</TouchableOpacity>
</View>
{/* Add Product Dialog */}
<BottomDialog
open={showAddDialog}
onClose={() => setShowAddDialog(false)}
>
<View style={tw`pb-8 pt-2 px-4`}>
<View style={tw`items-center mb-6`}>
<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`}>
Add Popular Item
</MyText>
<MyText style={tw`text-sm text-gray-500`}>
Select a product to add to popular items
</MyText>
</View>
{availableProducts.length === 0 ? (
<View style={tw`items-center py-8`}>
<MaterialIcons name="inventory" size={48} color="#e5e7eb" />
<MyText style={tw`text-gray-500 mt-4 text-center`}>
All products are already in popular items
</MyText>
</View>
) : (
<>
<BottomDropdown
label="Select Product"
options={productOptions}
value={selectedProductId || ""}
onValueChange={(val) => setSelectedProductId(val as number)}
placeholder="Choose a product..."
/>
<View style={tw`flex-row gap-3 mt-6`}>
<TouchableOpacity
onPress={() => setShowAddDialog(false)}
style={tw`flex-1 bg-gray-100 p-3 rounded-lg`}
>
<MyText style={tw`text-gray-700 text-center font-semibold`}>
Cancel
</MyText>
</TouchableOpacity>
<TouchableOpacity
onPress={handleAddProduct}
disabled={!selectedProductId}
style={tw`flex-1 ${
selectedProductId ? 'bg-blue-600' : 'bg-gray-300'
} p-3 rounded-lg`}
>
<MyText style={tw`text-white text-center font-semibold`}>
Add Product
</MyText>
</TouchableOpacity>
</View>
</>
)}
</View>
</BottomDialog>
</View>
</AppContainer>
);
}