439 lines
No EOL
14 KiB
TypeScript
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>
|
|
);
|
|
} |