freshyo/apps/admin-ui/app/(drawer)/prices-overview/index.tsx
2026-01-24 00:13:15 +05:30

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>
);
}