424 lines
No EOL
15 KiB
TypeScript
424 lines
No EOL
15 KiB
TypeScript
import React, { useState, useEffect, useMemo } from "react";
|
|
import {
|
|
View,
|
|
TouchableOpacity,
|
|
FlatList,
|
|
Image,
|
|
Alert,
|
|
ActivityIndicator,
|
|
TextInput,
|
|
} from "react-native";
|
|
import { useRouter } from "expo-router";
|
|
import {
|
|
AppContainer,
|
|
MyText,
|
|
tw,
|
|
BottomDialog,
|
|
BottomDropdown,
|
|
Checkbox,
|
|
} from "common-ui";
|
|
import { trpc } from "@/src/trpc-client";
|
|
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
|
import { Entypo } from "@expo/vector-icons";
|
|
|
|
interface ProductItemProps {
|
|
item: any;
|
|
hasChanges: (productId: number) => boolean;
|
|
pendingChanges: Record<string, any>;
|
|
setPendingChanges: React.Dispatch<React.SetStateAction<Record<string, any>>>;
|
|
openEditDialog: (product: any) => void;
|
|
}
|
|
|
|
const ProductItemComponent: React.FC<ProductItemProps> = ({
|
|
item: product,
|
|
hasChanges,
|
|
pendingChanges,
|
|
setPendingChanges,
|
|
openEditDialog,
|
|
}) => {
|
|
const changed = hasChanges(product.id);
|
|
const change = pendingChanges[product.id] || {};
|
|
const displayPrice = change.price !== undefined ? change.price : product.price;
|
|
const displayMarketPrice = change.marketPrice !== undefined ? change.marketPrice : product.marketPrice;
|
|
const displayFlashPrice = change.flashPrice !== undefined ? change.flashPrice : product.flashPrice;
|
|
|
|
return (
|
|
<View style={tw`bg-white p-4 mb-3 rounded-xl border border-gray-200 shadow-sm`}>
|
|
{/* Change indicator */}
|
|
<View style={tw`absolute top-2 right-2`}>
|
|
<View
|
|
style={[
|
|
tw`w-4 h-4 rounded-full items-center justify-center`,
|
|
changed ? tw`bg-green-500` : tw`bg-gray-300`,
|
|
]}
|
|
>
|
|
{changed && <MaterialIcons name="check" size={10} color="white" />}
|
|
</View>
|
|
</View>
|
|
|
|
{/* First row: Image and Name */}
|
|
<View style={tw`flex-row items-center mb-2`}>
|
|
{/* Product image */}
|
|
<Image
|
|
source={{
|
|
uri: product.images?.[0] || "https://via.placeholder.com/32x32?text=No+Image"
|
|
}}
|
|
style={tw`w-10 h-10 rounded-lg mr-3`}
|
|
resizeMode="cover"
|
|
/>
|
|
|
|
{/* Product name and Flash Checkbox */}
|
|
<View style={tw`flex-1 flex-row items-center`}>
|
|
<MyText style={tw`text-base font-medium text-gray-800`} numberOfLines={1}>
|
|
{product.name.length > 25 ? product.name.substring(0, 25) + '...' : product.name}
|
|
</MyText>
|
|
<View style={tw`flex-row items-center ml-2`}>
|
|
<Checkbox
|
|
checked={change.isFlashAvailable ?? product.isFlashAvailable ?? false}
|
|
onPress={() => {
|
|
const currentValue = change.isFlashAvailable ?? product.isFlashAvailable ?? false;
|
|
setPendingChanges(prev => ({
|
|
...prev,
|
|
[product.id]: {
|
|
...change,
|
|
isFlashAvailable: !currentValue,
|
|
},
|
|
}));
|
|
}}
|
|
style={tw`mr-1`}
|
|
/>
|
|
<MyText style={tw`text-sm text-gray-600`}>Flash</MyText>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Second row: Prices */}
|
|
<View style={tw`flex-row items-center justify-between`}>
|
|
{/* Our Price */}
|
|
<View style={tw`items-center flex-1`}>
|
|
<MyText style={tw`text-xs text-gray-500 mb-1`}>Our Price</MyText>
|
|
<View style={tw`flex-row items-center justify-center`}>
|
|
<MyText style={tw`text-sm font-bold text-green-600`}>₹{displayPrice}</MyText>
|
|
<TouchableOpacity onPress={() => openEditDialog(product)} style={tw`ml-1`}>
|
|
<MaterialIcons name="edit" size={14} color="#6b7280" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Market Price */}
|
|
<View style={tw`items-center flex-1`}>
|
|
<MyText style={tw`text-xs text-gray-500 mb-1`}>Market Price</MyText>
|
|
<View style={tw`flex-row items-center justify-center`}>
|
|
<MyText style={tw`text-sm text-gray-600`}>{displayMarketPrice ? `₹${displayMarketPrice}` : "N/A"}</MyText>
|
|
<TouchableOpacity onPress={() => openEditDialog(product)} style={tw`ml-1`}>
|
|
<MaterialIcons name="edit" size={14} color="#6b7280" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Flash Price */}
|
|
<View style={tw`items-center flex-1`}>
|
|
<MyText style={tw`text-xs text-gray-500 mb-1`}>Flash Price</MyText>
|
|
<View style={tw`flex-row items-center justify-center`}>
|
|
<MyText style={tw`text-sm text-orange-600`}>{displayFlashPrice ? `₹${displayFlashPrice}` : "N/A"}</MyText>
|
|
<TouchableOpacity onPress={() => openEditDialog(product)} style={tw`ml-1`}>
|
|
<MaterialIcons name="edit" size={14} color="#6b7280" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
interface PendingChange {
|
|
price?: number;
|
|
marketPrice?: number | null;
|
|
flashPrice?: number | null;
|
|
isFlashAvailable?: boolean;
|
|
}
|
|
|
|
interface EditDialogState {
|
|
open: boolean;
|
|
product: any;
|
|
tempPrice: string;
|
|
tempMarketPrice: string;
|
|
tempFlashPrice: string;
|
|
}
|
|
|
|
export default function PricesOverview() {
|
|
const router = useRouter();
|
|
const [selectedStores, setSelectedStores] = useState<string[]>([]);
|
|
const [pendingChanges, setPendingChanges] = useState<Record<number, PendingChange>>({});
|
|
const [editDialog, setEditDialog] = useState<EditDialogState>({
|
|
open: false,
|
|
product: null,
|
|
tempPrice: "",
|
|
tempMarketPrice: "",
|
|
tempFlashPrice: "",
|
|
});
|
|
const [showMenu, setShowMenu] = useState(false);
|
|
|
|
const { data: productsData, isLoading: productsLoading, refetch: refetchProducts } =
|
|
trpc.admin.product.getProducts.useQuery();
|
|
const { data: storesData, isLoading: storesLoading } =
|
|
trpc.admin.store.getStores.useQuery();
|
|
|
|
const updatePricesMutation = trpc.admin.product.updateProductPrices.useMutation();
|
|
|
|
const stores = storesData?.stores || [];
|
|
const allProducts = productsData?.products || [];
|
|
|
|
// Sort stores alphabetically
|
|
const sortedStores = useMemo(() =>
|
|
[...stores].sort((a, b) => a.name.localeCompare(b.name)),
|
|
[stores]
|
|
);
|
|
|
|
// Store options for dropdown
|
|
const storeOptions = useMemo(() =>
|
|
sortedStores.map(store => ({
|
|
label: store.name,
|
|
value: store.id.toString(),
|
|
})),
|
|
[sortedStores]
|
|
);
|
|
|
|
// Initialize selectedStores to all if not set
|
|
useEffect(() => {
|
|
if (stores.length > 0 && selectedStores.length === 0) {
|
|
setSelectedStores(stores.map(s => s.id.toString()));
|
|
}
|
|
}, [stores, selectedStores]);
|
|
|
|
// Filter products by selected stores
|
|
const filteredProducts = useMemo(() => {
|
|
if (selectedStores.length === 0) return allProducts;
|
|
return allProducts.filter(product =>
|
|
product.storeId && selectedStores.includes(product.storeId.toString())
|
|
);
|
|
}, [allProducts, selectedStores]);
|
|
|
|
// Check if a product has changes
|
|
const hasChanges = (productId: number) => !!pendingChanges[productId];
|
|
|
|
// Open edit dialog
|
|
const openEditDialog = (product: any) => {
|
|
const change = pendingChanges[product.id] || {};
|
|
setEditDialog({
|
|
open: true,
|
|
product,
|
|
tempPrice: (change.price ?? product.price)?.toString() || "",
|
|
tempMarketPrice: (change.marketPrice ?? product.marketPrice)?.toString() || "",
|
|
tempFlashPrice: (change.flashPrice ?? product.flashPrice)?.toString() || "",
|
|
});
|
|
};
|
|
|
|
// Save edit dialog
|
|
const saveEditDialog = () => {
|
|
const price = parseFloat(editDialog.tempPrice);
|
|
const marketPrice = editDialog.tempMarketPrice ? parseFloat(editDialog.tempMarketPrice) : null;
|
|
const flashPrice = editDialog.tempFlashPrice ? parseFloat(editDialog.tempFlashPrice) : null;
|
|
|
|
if (isNaN(price) || price <= 0) {
|
|
Alert.alert("Error", "Please enter a valid price");
|
|
return;
|
|
}
|
|
|
|
if (editDialog.tempMarketPrice && (isNaN(marketPrice!) || marketPrice! <= 0)) {
|
|
Alert.alert("Error", "Please enter a valid market price");
|
|
return;
|
|
}
|
|
|
|
if (editDialog.tempFlashPrice && (isNaN(flashPrice!) || flashPrice! <= 0)) {
|
|
Alert.alert("Error", "Please enter a valid flash price");
|
|
return;
|
|
}
|
|
|
|
setPendingChanges(prev => ({
|
|
...prev,
|
|
[editDialog.product.id]: {
|
|
price: price !== editDialog.product.price ? price : undefined,
|
|
marketPrice: marketPrice !== editDialog.product.marketPrice ? marketPrice : undefined,
|
|
flashPrice: flashPrice !== editDialog.product.flashPrice ? flashPrice : undefined,
|
|
},
|
|
}));
|
|
|
|
setEditDialog({ open: false, product: null, tempPrice: "", tempMarketPrice: "", tempFlashPrice: "" });
|
|
};
|
|
|
|
// Handle save all changes
|
|
const handleSave = () => {
|
|
const updates = Object.entries(pendingChanges).map(([productId, change]) => {
|
|
const update: any = { productId: parseInt(productId) };
|
|
if (change.price !== undefined) update.price = change.price;
|
|
if (change.marketPrice !== undefined) update.marketPrice = change.marketPrice;
|
|
if (change.flashPrice !== undefined) update.flashPrice = change.flashPrice;
|
|
if (change.isFlashAvailable !== undefined) update.isFlashAvailable = change.isFlashAvailable;
|
|
return update;
|
|
});
|
|
|
|
updatePricesMutation.mutate(
|
|
{ updates },
|
|
{
|
|
onSuccess: () => {
|
|
setPendingChanges({});
|
|
refetchProducts();
|
|
Alert.alert("Success", "Prices updated successfully");
|
|
},
|
|
onError: (error: any) => {
|
|
Alert.alert("Error", `Failed to update prices: ${error.message || "Unknown error"}`);
|
|
},
|
|
}
|
|
);
|
|
};
|
|
|
|
|
|
|
|
const changeCount = Object.keys(pendingChanges).length;
|
|
|
|
return (
|
|
<View style={tw`flex-1 bg-gray-50`}>
|
|
{/* Stores filter, save button, and menu */}
|
|
<View style={tw`bg-white px-4 py-4 border-b border-gray-200 flex-row items-center`}>
|
|
<View style={tw`flex-1 mr-4`}>
|
|
<BottomDropdown
|
|
label="Filter by Stores"
|
|
options={storeOptions}
|
|
value={selectedStores}
|
|
onValueChange={(value) => setSelectedStores(value as string[])}
|
|
multiple={true}
|
|
placeholder="Select stores"
|
|
/>
|
|
</View>
|
|
<TouchableOpacity
|
|
style={[
|
|
tw`px-4 py-2 rounded-lg flex-row items-center mr-3`,
|
|
changeCount > 0 && !updatePricesMutation.isPending ? tw`bg-blue-600` : tw`bg-gray-300`,
|
|
]}
|
|
onPress={handleSave}
|
|
disabled={changeCount === 0 || updatePricesMutation.isPending}
|
|
>
|
|
{updatePricesMutation.isPending ? (
|
|
<ActivityIndicator size="small" color={changeCount > 0 ? "white" : "#6b7280"} style={tw`mr-2`} />
|
|
) : (
|
|
<MaterialIcons
|
|
name="save"
|
|
size={20}
|
|
color={changeCount > 0 ? "white" : "#6b7280"}
|
|
style={tw`mr-2`}
|
|
/>
|
|
)}
|
|
<MyText
|
|
style={[
|
|
tw`font-medium`,
|
|
changeCount > 0 ? tw`text-white` : tw`text-gray-500`,
|
|
]}
|
|
>
|
|
Save ({changeCount})
|
|
</MyText>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
onPress={() => setShowMenu(true)}
|
|
style={tw`p-2 -mr-2`}
|
|
>
|
|
<Entypo name="dots-three-vertical" size={16} color="#9CA3AF" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Content */}
|
|
{productsLoading || storesLoading ? (
|
|
<View style={tw`flex-1 justify-center items-center`}>
|
|
<ActivityIndicator size="large" color="#3b82f6" />
|
|
<MyText style={tw`text-gray-500 mt-4`}>Loading...</MyText>
|
|
</View>
|
|
) : (
|
|
<FlatList
|
|
data={filteredProducts}
|
|
renderItem={({ item }) => (
|
|
<ProductItemComponent
|
|
item={item}
|
|
hasChanges={hasChanges}
|
|
pendingChanges={pendingChanges}
|
|
setPendingChanges={setPendingChanges}
|
|
openEditDialog={openEditDialog}
|
|
/>
|
|
)}
|
|
keyExtractor={(item) => item.id.toString()}
|
|
contentContainerStyle={tw`p-4`}
|
|
showsVerticalScrollIndicator={false}
|
|
/>
|
|
)}
|
|
|
|
{/* Edit Dialog */}
|
|
<BottomDialog open={editDialog.open} onClose={() => setEditDialog({ ...editDialog, open: false, tempFlashPrice: "" })}>
|
|
<View style={tw`p-4`}>
|
|
<MyText style={tw`text-lg font-bold mb-4`}>{editDialog.product?.name}</MyText>
|
|
|
|
<View style={tw`mb-4`}>
|
|
<MyText style={tw`text-sm font-medium mb-1`}>Our Price</MyText>
|
|
<TextInput
|
|
style={tw`border border-gray-300 rounded-md px-3 py-2`}
|
|
value={editDialog.tempPrice}
|
|
onChangeText={(text) => setEditDialog({ ...editDialog, tempPrice: text })}
|
|
keyboardType="numeric"
|
|
placeholder="Enter price"
|
|
/>
|
|
</View>
|
|
|
|
<View style={tw`mb-4`}>
|
|
<MyText style={tw`text-sm font-medium mb-1`}>Market Price (Optional)</MyText>
|
|
<TextInput
|
|
style={tw`border border-gray-300 rounded-md px-3 py-2`}
|
|
value={editDialog.tempMarketPrice}
|
|
onChangeText={(text) => setEditDialog({ ...editDialog, tempMarketPrice: text })}
|
|
keyboardType="numeric"
|
|
placeholder="Enter market price"
|
|
/>
|
|
</View>
|
|
|
|
<View style={tw`mb-4`}>
|
|
<MyText style={tw`text-sm font-medium mb-1`}>Flash Price (Optional)</MyText>
|
|
<TextInput
|
|
style={tw`border border-gray-300 rounded-md px-3 py-2`}
|
|
value={editDialog.tempFlashPrice}
|
|
onChangeText={(text) => setEditDialog({ ...editDialog, tempFlashPrice: text })}
|
|
keyboardType="numeric"
|
|
placeholder="Enter flash price"
|
|
/>
|
|
</View>
|
|
|
|
<TouchableOpacity
|
|
style={tw`bg-blue-600 py-3 rounded-md items-center`}
|
|
onPress={saveEditDialog}
|
|
>
|
|
<MyText style={tw`text-white font-medium`}>Update Price</MyText>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</BottomDialog>
|
|
|
|
{/* Menu Dialog */}
|
|
<BottomDialog open={showMenu} onClose={() => setShowMenu(false)}>
|
|
<View style={tw`p-6`}>
|
|
<MyText style={tw`text-lg font-bold text-gray-800 mb-6`}>
|
|
Options
|
|
</MyText>
|
|
<TouchableOpacity
|
|
style={tw`flex-row items-center p-4 bg-gray-50 rounded-lg`}
|
|
onPress={() => {
|
|
router.push('/rebalance-orders' as any);
|
|
setShowMenu(false);
|
|
}}
|
|
>
|
|
<Entypo name="shuffle" size={20} color="#6B7280" />
|
|
<MyText style={tw`text-gray-800 font-medium ml-3`}>
|
|
Re-Balance Orders
|
|
</MyText>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</BottomDialog>
|
|
|
|
|
|
</View>
|
|
);
|
|
} |