313 lines
No EOL
12 KiB
TypeScript
313 lines
No EOL
12 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { MaterialCommunityIcons, Entypo } from '@expo/vector-icons';
|
|
import { View, TouchableOpacity, FlatList, Alert, ActivityIndicator } from 'react-native';
|
|
import { AppContainer, MyText, tw, MyFlatList , BottomDialog, MyTouchableOpacity, Checkbox } from 'common-ui';
|
|
import { trpc } from '@/src/trpc-client';
|
|
import { useRouter } from 'expo-router';
|
|
import dayjs from 'dayjs';
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
|
|
interface SlotItemProps {
|
|
item: any;
|
|
router: any;
|
|
setDialogProducts: React.Dispatch<React.SetStateAction<any[]>>;
|
|
setDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
refetch: () => void;
|
|
}
|
|
|
|
const SlotItemComponent: React.FC<SlotItemProps> = ({
|
|
item: slot,
|
|
router,
|
|
setDialogProducts,
|
|
setDialogOpen,
|
|
refetch,
|
|
}) => {
|
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
const slotProducts = slot.products?.map((p: any) => p.name).filter(Boolean) || [];
|
|
const displayProducts = slotProducts.slice(0, 2).join(', ');
|
|
|
|
const isActive = slot.isActive;
|
|
const statusColor = isActive ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700';
|
|
const statusText = isActive ? 'Active' : 'Inactive';
|
|
|
|
const updateSlotCapacity = trpc.admin.slots.updateSlotCapacity.useMutation();
|
|
|
|
const handleCapacityToggle = () => {
|
|
updateSlotCapacity.mutate(
|
|
{ slotId: slot.id, isCapacityFull: !slot.isCapacityFull },
|
|
{
|
|
onSuccess: () => {
|
|
setMenuOpen(false);
|
|
refetch();
|
|
Alert.alert(
|
|
'Success',
|
|
slot.isCapacityFull
|
|
? 'Slot capacity reset. It will now be visible to users.'
|
|
: 'Slot marked as full capacity. It will be hidden from users.'
|
|
);
|
|
},
|
|
onError: (error: any) => {
|
|
Alert.alert('Error', error.message || 'Failed to update slot capacity');
|
|
},
|
|
}
|
|
);
|
|
};
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
onPress={() => router.push(`/(drawer)/slots/slot-details?slotId=${slot.id}`)}
|
|
activeOpacity={0.7}
|
|
>
|
|
<View style={tw`bg-white p-5 mb-4 rounded-3xl shadow-sm border border-gray-100`}>
|
|
{/* Header: ID and Status */}
|
|
<View style={tw`flex-row justify-between items-center mb-4`}>
|
|
<View style={tw`flex-row items-center`}>
|
|
<View style={tw`w-10 h-10 bg-pink2 rounded-full items-center justify-center mr-3`}>
|
|
<MaterialCommunityIcons name="calendar-clock" size={20} color="#F83758" />
|
|
</View>
|
|
<View>
|
|
<MyText style={tw`text-lg font-bold text-gray-900`}>Slot #{slot.id}</MyText>
|
|
<MyText style={tw`text-xs text-gray-500`}>ID: {slot.id}</MyText>
|
|
</View>
|
|
</View>
|
|
<View style={tw`flex-row items-center`}>
|
|
<TouchableOpacity
|
|
onPress={() => router.push(`/slots/edit/${slot.id}` as any)}
|
|
style={tw`px-3 py-1 rounded-full bg-pink2 mr-2`}
|
|
>
|
|
<View style={tw`flex-row items-center`}>
|
|
<MaterialCommunityIcons name="pencil" size={12} color="#F83758" style={tw`mr-1`} />
|
|
<MyText style={tw`text-xs font-bold text-pink1`}>Edit</MyText>
|
|
</View>
|
|
</TouchableOpacity>
|
|
<View style={tw`px-3 py-1 rounded-full ${statusColor.split(' ')[0]}`}>
|
|
<MyText style={tw`text-xs font-bold ${statusColor.split(' ')[1]}`}>{statusText}</MyText>
|
|
</View>
|
|
{slot.isCapacityFull && (
|
|
<View style={tw`px-2 py-1 rounded-full bg-red-500 ml-2`}>
|
|
<MyText style={tw`text-xs font-bold text-white`}>FULL</MyText>
|
|
</View>
|
|
)}
|
|
<TouchableOpacity
|
|
onPress={() => setMenuOpen(true)}
|
|
style={tw`ml-2 p-1`}
|
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
>
|
|
<Entypo name="dots-three-vertical" size={20} color="#9CA3AF" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Replicate Menu Dialog */}
|
|
<BottomDialog open={menuOpen} onClose={() => setMenuOpen(false)}>
|
|
<View style={tw`p-4`}>
|
|
<MyText style={tw`text-lg font-bold mb-4`}>Slot #{slot.id} Actions</MyText>
|
|
|
|
{/* Capacity Toggle */}
|
|
<TouchableOpacity
|
|
onPress={handleCapacityToggle}
|
|
disabled={updateSlotCapacity.isPending}
|
|
style={tw`py-4 border-b border-gray-200`}
|
|
>
|
|
<View style={tw`flex-row items-center justify-between`}>
|
|
<View style={tw`flex-row items-center flex-1`}>
|
|
{updateSlotCapacity.isPending ? (
|
|
<ActivityIndicator size="small" color="#EF4444" style={tw`mr-3`} />
|
|
) : (
|
|
<MaterialCommunityIcons
|
|
name={slot.isCapacityFull ? "package-variant-closed" : "package-variant"}
|
|
size={20}
|
|
color={slot.isCapacityFull ? "#EF4444" : "#4B5563"}
|
|
style={tw`mr-3`}
|
|
/>
|
|
)}
|
|
<View>
|
|
<MyText style={tw`text-base text-gray-800`}>Mark as Full Capacity</MyText>
|
|
<MyText style={tw`text-xs text-gray-500 mt-0.5`}>
|
|
{slot.isCapacityFull
|
|
? "Slot is hidden from users"
|
|
: "Hidden from users when full"}
|
|
</MyText>
|
|
</View>
|
|
</View>
|
|
{updateSlotCapacity.isPending ? (
|
|
<ActivityIndicator size="small" color="#EF4444" />
|
|
) : (
|
|
<Checkbox
|
|
checked={slot.isCapacityFull}
|
|
onPress={handleCapacityToggle}
|
|
size={22}
|
|
fillColor="#EF4444"
|
|
checkColor="#FFFFFF"
|
|
/>
|
|
)}
|
|
</View>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
setMenuOpen(false);
|
|
router.push(`/slots/add?baseslot=${slot.id}` as any);
|
|
}}
|
|
style={tw`py-4 border-b border-gray-200`}
|
|
>
|
|
<View style={tw`flex-row items-center`}>
|
|
<MaterialCommunityIcons name="content-copy" size={20} color="#4B5563" style={tw`mr-3`} />
|
|
<MyText style={tw`text-base text-gray-800`}>Replicate Slot</MyText>
|
|
</View>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
onPress={() => setMenuOpen(false)}
|
|
style={tw`py-4 mt-2`}
|
|
>
|
|
<View style={tw`flex-row items-center`}>
|
|
<MaterialCommunityIcons name="close" size={20} color="#EF4444" style={tw`mr-3`} />
|
|
<MyText style={tw`text-base text-red-500`}>Cancel</MyText>
|
|
</View>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</BottomDialog>
|
|
|
|
{/* Divider */}
|
|
<View style={tw`h-[1px] bg-gray-100 mb-4`} />
|
|
|
|
{/* Details Grid */}
|
|
<View style={tw`flex-row flex-wrap`}>
|
|
{/* Delivery Time */}
|
|
<View style={tw`w-1/2 mb-4 pr-2`}>
|
|
<View style={tw`flex-row items-center mb-1`}>
|
|
<MaterialCommunityIcons name="truck-delivery-outline" size={14} color="#6b7280" style={tw`mr-1`} />
|
|
<MyText style={tw`text-xs text-gray-500 uppercase font-semibold tracking-wider`}>Delivery</MyText>
|
|
</View>
|
|
<MyText style={tw`text-sm font-medium text-gray-800`}>
|
|
{dayjs(slot.deliveryTime).format('DD MMM, h:mm A')}
|
|
</MyText>
|
|
</View>
|
|
|
|
{/* Freeze Time */}
|
|
<View style={tw`w-1/2 mb-4 pl-2`}>
|
|
<View style={tw`flex-row items-center mb-1`}>
|
|
<MaterialCommunityIcons name="snowflake" size={14} color="#6b7280" style={tw`mr-1`} />
|
|
<MyText style={tw`text-xs text-gray-500 uppercase font-semibold tracking-wider`}>Freeze</MyText>
|
|
</View>
|
|
<MyText style={tw`text-sm font-medium text-gray-800`}>
|
|
{dayjs(slot.freezeTime).format('DD MMM, h:mm A')}
|
|
</MyText>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Products */}
|
|
{slotProducts.length > 0 ? (
|
|
<View style={tw`bg-gray-50 p-3 rounded-xl mt-1`}>
|
|
<View style={tw`flex-row items-start`}>
|
|
<MaterialCommunityIcons name="basket-outline" size={16} color="#4b5563" style={tw`mr-2 mt-0.5`} />
|
|
<View style={tw`flex-1`}>
|
|
<MyText style={tw`text-xs text-gray-500 mb-0.5`}>Products</MyText>
|
|
<View style={tw`flex-row items-center flex-wrap`}>
|
|
<MyText style={tw`text-sm text-gray-800 leading-5`}>{displayProducts}</MyText>
|
|
{slotProducts.length > 2 && (
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
setDialogProducts(slotProducts);
|
|
setDialogOpen(true);
|
|
}}
|
|
>
|
|
<MyText style={tw`text-sm text-pink1 font-semibold ml-1`}>
|
|
+{slotProducts.length - 2} more
|
|
</MyText>
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
</TouchableOpacity>
|
|
);
|
|
};
|
|
|
|
export default function Slots() {
|
|
const router = useRouter();
|
|
const { data: slotsData, isLoading, refetch } = trpc.admin.slots.getAll.useQuery();
|
|
|
|
const slots = slotsData?.slots || [];
|
|
|
|
// Dialog state
|
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
const [dialogProducts, setDialogProducts] = useState<any[]>([]);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
|
|
const handleRefresh = async () => {
|
|
setRefreshing(true);
|
|
await refetch();
|
|
setRefreshing(false);
|
|
};
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<View style={tw`flex-1 justify-center items-center bg-white`}>
|
|
<MyText>Loading slots...</MyText>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={tw`flex-1 bg-white relative`}>
|
|
<MyFlatList
|
|
data={slots}
|
|
keyExtractor={(item) => item.id.toString()}
|
|
renderItem={({ item }) => (
|
|
<SlotItemComponent
|
|
item={item}
|
|
router={router}
|
|
setDialogProducts={setDialogProducts}
|
|
setDialogOpen={setDialogOpen}
|
|
refetch={refetch}
|
|
/>
|
|
)}
|
|
contentContainerStyle={tw`p-4`}
|
|
onRefresh={handleRefresh}
|
|
refreshing={refreshing}
|
|
/>
|
|
|
|
{/* FAB for Add New Slot */}
|
|
<MyTouchableOpacity
|
|
testID="add-slot-fab"
|
|
accessibilityLabel="add-slot-fab"
|
|
onPress={() => router.push('/slots/add' as any)}
|
|
activeOpacity={0.95}
|
|
style={{ position: 'absolute', bottom: 32, right: 24, zIndex: 100 }}
|
|
>
|
|
<LinearGradient
|
|
colors={['#F83758', '#E91E63']}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 1 }}
|
|
style={tw`w-16 h-16 rounded-[24px] items-center justify-center shadow-lg shadow-pink300`}
|
|
>
|
|
<MaterialCommunityIcons name="plus" size={32} color="white" />
|
|
</LinearGradient>
|
|
</MyTouchableOpacity>
|
|
|
|
{/* Products Dialog */}
|
|
<BottomDialog open={dialogOpen} onClose={() => setDialogOpen(false)}>
|
|
<View style={tw`p-4`}>
|
|
<MyText style={tw`text-lg font-bold mb-4`}>All Products</MyText>
|
|
<FlatList
|
|
data={dialogProducts}
|
|
keyExtractor={(item, index) => index.toString()}
|
|
renderItem={({ item }) => (
|
|
<View style={tw`py-2 border-b border-gray-200`}>
|
|
<MyText style={tw`text-base`}>{item}</MyText>
|
|
</View>
|
|
)}
|
|
showsVerticalScrollIndicator={false}
|
|
style={tw`max-h-80`}
|
|
/>
|
|
</View>
|
|
</BottomDialog>
|
|
</View>
|
|
);
|
|
} |