import React, { useState, useEffect, useMemo } from "react"; import { View, TouchableOpacity, Alert, ActivityIndicator, Linking, } from "react-native"; import DraggableFlatList, { RenderItemParams, ScaleDecorator, } from "react-native-draggable-flatlist"; import { AppContainer, MyText, tw, useManualRefresh, useMarkDataFetchers, BottomDialog, Checkbox, BottomDropdown, } from "common-ui"; import { useQueryClient } from "@tanstack/react-query"; import dayjs from "dayjs"; import { useLocalSearchParams, useRouter } from "expo-router"; import { trpc } from "@/src/trpc-client"; import MaterialIcons from "@expo/vector-icons/MaterialIcons"; import Entypo from "@expo/vector-icons/Entypo"; import * as Location from "expo-location"; // Define types outside interface OrderWithSequence { sequenceId: number; readableId: number; isPackaged: boolean; totalAmount: number; address: string; latitude: number | null; longitude: number | null; id: number; isDelivered: boolean; addressId: number; adminNotes?: string | null; customerName?: string | null; assignedUserName?: string | null; } interface SlotProgressProps { slotId: number; orders: any[]; } const SlotProgress: React.FC = ({ slotId, orders }) => { const delivered = orders.filter((o) => o.isDelivered).length; const undelivered = orders.filter((o) => !o.isDelivered).length; const packaged = orders.filter((o) => o.isPackaged).length; const notPackaged = orders.filter((o) => !o.isPackaged).length; const stats = [ { label: "Delivered", value: delivered }, { label: "Undelivered", value: undelivered }, { label: "Packaged", value: packaged }, { label: "Not Packaged", value: notPackaged }, ]; return ( Slot {slotId} Statistics {stats.map((stat, index) => ( {stat.value} {stat.label} ))} ); }; interface OrderItemProps { item: OrderWithSequence; drag: () => void; isActive: boolean; isPending: boolean; setSelectedOrder: (order: OrderWithSequence | null) => void; setShowOrderMenu: (show: boolean) => void; selectedOrderIds: number[]; onToggleOrder: (id: number) => void; assignedUserName?: string | null; staffData?: { staff?: Array<{ id: number; name: string }> }; setSelectedUserId?: (id: number) => void; } const OrderItem: React.FC = ({ item, drag, isActive, isPending, setSelectedOrder, setShowOrderMenu, selectedOrderIds, onToggleOrder, assignedUserName, staffData, setSelectedUserId, }) => { const orderItem = item; return ( {/* Checkbox and Drag Handle */} onToggleOrder(orderItem.id)} size={20} fillColor="#3b82f6" checkColor="#FFFFFF" /> {/* Content */} #{orderItem.readableId} Pkg {/* Optional: Add time if available, or just keep ID */} ₹{orderItem.totalAmount} { setSelectedOrder(orderItem); setShowOrderMenu(true); }} style={tw`p-2`} > {orderItem.address} {orderItem.latitude && orderItem.longitude && ( { Linking.openURL( `https://www.google.com/maps?q=${orderItem.latitude},${orderItem.longitude}` ); }} style={tw`ml-2`} > )} {assignedUserName && ( Assigned to{" "} { const assignedUserId = staffData?.staff?.find( (s: { id: number; name: string }) => s.name === assignedUserName )?.id; if (assignedUserId && setSelectedUserId) { setSelectedUserId(assignedUserId); } }} > {assignedUserName} )} {/* Admin Notes */} {orderItem.adminNotes && ( {orderItem.adminNotes} )} ); }; export default function DeliverySequences() { const { slotId } = useLocalSearchParams(); const selectedSlotId = slotId ? Number(slotId) : null; const [localOrderedOrders, setLocalOrderedOrders] = useState< OrderWithSequence[] >([]); const [showOrderMenu, setShowOrderMenu] = useState(false); const [selectedOrder, setSelectedOrder] = useState( null ); const [selectedUserId, setSelectedUserId] = useState(-1); const [selectedOrderIds, setSelectedOrderIds] = useState([]); const [hasSequenceChanged, setHasSequenceChanged] = useState(false); const router = useRouter(); const { data: slotsData, refetch: refetchSlots } = trpc.admin.slots.getAll.useQuery(); const { data: ordersData, isLoading: ordersLoading, refetch: refetchOrders, } = trpc.admin.order.getSlotOrders.useQuery( { slotId: String(selectedSlotId) }, { enabled: !!selectedSlotId, } ); const { data: sequenceData, refetch: refetchSequence } = trpc.admin.slots.getDeliverySequence.useQuery( { id: String(selectedSlotId) }, { enabled: !!selectedSlotId, } ); const { data: staffData } = trpc.admin.staffUser.getStaff.useQuery(); // Auto-select first slot if no slotId provided useEffect(() => { if (!slotId && slotsData?.slots && slotsData.slots.length > 0) { router.replace(`/delivery-sequences?slotId=${slotsData.slots[0].id}`); } }, [slotId, slotsData, router]); const updateSequenceMutation = trpc.admin.slots.updateDeliverySequence.useMutation(); const updatePackagedMutation = trpc.admin.order.updatePackaged.useMutation(); const updateDeliveredMutation = trpc.admin.order.updateDelivered.useMutation(); const updateAddressCoordsMutation = trpc.admin.order.updateAddressCoords.useMutation(); // Manual refresh functionality useManualRefresh(() => { refetchSlots(); refetchOrders(); refetchSequence(); }); useMarkDataFetchers(() => { refetchSlots(); refetchOrders(); refetchSequence(); }); const slots = slotsData?.slots || []; const orders = ordersData?.data || []; const deliverySequence = sequenceData?.deliverySequence || []; const slotOptions = slots?.map((slot) => ({ label: dayjs(slot.deliveryTime).format("ddd DD MMM, h:mm a"), value: slot.id, })) || []; const userOptions = [ { label: "Unassigned", value: -1 }, { label: "All Users", value: -2 }, ...(staffData?.staff?.map((staff) => ({ label: staff.name, value: staff.id, })) || []), ]; // Create ordered orders based on delivery sequence const computedOrderedOrders = useMemo(() => { if (orders.length > 0) { const userSequence = selectedUserId !== -1 ? (deliverySequence as any)?.[String(selectedUserId)] || [] : null; if (selectedUserId !== -1 && userSequence && userSequence.length > 0) { // Sort orders according to user's sequence const sequenceMap = new Map( userSequence.map((id: number, index: number) => [id, index]) ); let ordered = orders .filter((order) => userSequence.includes(order.id)) .map((order) => ({ ...order, sequenceId: order.id })) .sort((a, b) => { const aIndex = sequenceMap.get(a.sequenceId) ?? Infinity; const bIndex = sequenceMap.get(b.sequenceId) ?? Infinity; return aIndex < bIndex ? -1 : aIndex > bIndex ? 1 : 0; }); return ordered; } else if (selectedUserId === -1) { // Show unassigned orders (not in any user's sequence) const assignedIds = new Set( Object.values(deliverySequence as any).flat() ); let ordered = orders .filter((order) => !assignedIds.has(order.id)) .map((order) => ({ ...order, sequenceId: order.id })) .sort((a, b) => a.sequenceId < b.sequenceId ? -1 : a.sequenceId > b.sequenceId ? 1 : 0 ); return ordered; } else if (selectedUserId === -2) { // Show all orders with assignment info const orderToUserMap = new Map(); Object.entries(deliverySequence as any).forEach(([userId, orderIds]) => { (orderIds as number[]).forEach((orderId) => { orderToUserMap.set(orderId, parseInt(userId)); }); }); let ordered = orders .map((order) => { const assignedUserId = orderToUserMap.get(order.id); const assignedUser = staffData?.staff?.find( (s) => s.id === assignedUserId ); return { ...order, sequenceId: order.id, assignedUserName: assignedUser?.name || null, }; }) .sort((a, b) => a.sequenceId < b.sequenceId ? -1 : a.sequenceId > b.sequenceId ? 1 : 0 ); return ordered; } else { // No sequence for user, show empty return []; } } else { return []; } }, [ordersData, sequenceData, selectedUserId]); // Sync local state with computed orders useEffect(() => { setLocalOrderedOrders(computedOrderedOrders); }, [computedOrderedOrders]); const handleDragEnd = ({ data }: { data: OrderWithSequence[] }) => { if (selectedUserId !== -1 && selectedUserId !== -2) { setLocalOrderedOrders(data); setHasSequenceChanged(true); } }; const handleSaveSequence = () => { if (!selectedSlotId || selectedUserId === -1) return; const newSequence = { ...(deliverySequence as any) }; newSequence[String(selectedUserId)] = localOrderedOrders.map( (order) => order.sequenceId ); updateSequenceMutation.mutate( { id: selectedSlotId, deliverySequence: newSequence, }, { onSuccess: () => { setHasSequenceChanged(false); Alert.alert("Success", "Delivery sequence updated successfully"); }, onError: (error: any) => { Alert.alert( "Error", `Failed to update delivery sequence: ${ error.message || "Unknown error" }` ); }, } ); }; if (!slotsData) { return ( Loading slots... ); } if (slotsData.slots.length === 0) { return ( No slots available ); } return ( {selectedSlotId ? ( <> {/* Header Section */} router.back()} style={tw`p-2 -ml-4`} accessibilityLabel="Go back" > { if (val) { router.replace(`/delivery-sequences?slotId=${val}`); } }} placeholder="Select slot" /> setSelectedUserId(val as number)} placeholder="Select user" /> {/* Content Section */} {ordersLoading ? ( Loading orders... ) : localOrderedOrders.length === 0 ? ( No orders found for this slot ) : ( {/* Long press an item to drag and reorder */} ( { setSelectedOrderIds((prev) => prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id] ); }} assignedUserName={selectedUserId === -2 ? item.assignedUserName : undefined} staffData={staffData} setSelectedUserId={setSelectedUserId} /> )} keyExtractor={(item) => item.sequenceId.toString()} onDragEnd={handleDragEnd} showsVerticalScrollIndicator={false} contentContainerStyle={tw`pb-8`} ListHeaderComponent={ } /> )} {/* FAB for Assignment */} {selectedOrderIds.length > 0 && ( opt.value !== -1), { label: "None", value: -3 }, ]} value={-1} onValueChange={(val) => { if (val !== -1) { if (val === -3) { // Unassign selected orders const newSequence = { ...(deliverySequence as any) }; Object.keys(newSequence).forEach((userId) => { newSequence[userId] = newSequence[userId].filter( (id: number) => !selectedOrderIds.includes(id) ); }); // Save updateSequenceMutation.mutate( { id: selectedSlotId, deliverySequence: newSequence, }, { onSuccess: () => { setSelectedOrderIds([]); refetchSequence(); Alert.alert( "Success", "Orders unassigned successfully" ); }, onError: (error: any) => { Alert.alert( "Error", `Failed to unassign orders: ${ error.message || "Unknown error" }` ); }, } ); } else { // Assign selected orders to the user // Update deliverySequence map const newSequence = { ...(deliverySequence as any) }; // Remove from all other users Object.keys(newSequence).forEach((userId) => { newSequence[userId] = newSequence[userId].filter( (id: number) => !selectedOrderIds.includes(id) ); }); // Add to selected user if (!newSequence[String(val)]) newSequence[String(val)] = []; newSequence[String(val)] = [ ...new Set([ ...newSequence[String(val)], ...selectedOrderIds, ]), ]; // Save updateSequenceMutation.mutate( { id: selectedSlotId, deliverySequence: newSequence, }, { onSuccess: () => { setSelectedOrderIds([]); refetchSequence(); Alert.alert( "Success", "Orders assigned successfully" ); }, onError: (error: any) => { Alert.alert( "Error", `Failed to assign orders: ${ error.message || "Unknown error" }` ); }, } ); } } }} triggerComponent={({ onPress }) => ( )} /> )} {/* FAB for Save */} {hasSequenceChanged && ( )} ) : ( No slot selected. Please select a slot to view delivery sequence. router.back()} > Go Back )} {/* Order Menu Dialog */} setShowOrderMenu(false)} > {/* Handle Bar */} Order #{selectedOrder?.readableId} Select an action to perform {/* Actions */} { router.push(`/order-details/${selectedOrder?.id}`); setShowOrderMenu(false); }} disabled={updateAddressCoordsMutation.isPending} > View Details See full order information { if (!selectedOrder) return; try { await updatePackagedMutation.mutateAsync({ orderId: selectedOrder.id.toString(), isPackaged: !selectedOrder.isPackaged, }); refetchOrders(); refetchSequence(); setShowOrderMenu(false); } catch (error) { Alert.alert("Error", "Failed to update packaged status"); } }} disabled={updatePackagedMutation.isPending} > {selectedOrder?.isPackaged ? "Unmark Packaged" : "Mark Packaged"} {selectedOrder?.isPackaged ? "Revert to not packaged" : "Update status to packaged"} { if (!selectedOrder) return; try { await updateDeliveredMutation.mutateAsync({ orderId: selectedOrder.id.toString(), isDelivered: !selectedOrder.isDelivered, }); refetchOrders(); refetchSequence(); setShowOrderMenu(false); } catch (error) { Alert.alert("Error", "Failed to update delivered status"); } }} disabled={updateDeliveredMutation.isPending} > {selectedOrder?.isDelivered ? "Unmark Delivered" : "Mark Delivered"} {selectedOrder?.isDelivered ? "Revert delivery status" : "Complete the delivery"} { if (!selectedOrder) return; try { const { status } = await Location.requestForegroundPermissionsAsync(); if (status !== "granted") { Alert.alert( "Permission Denied", "Location permission is required to attach coordinates." ); return; } const location = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.High, }); const { latitude, longitude } = location.coords; await updateAddressCoordsMutation.mutateAsync({ addressId: selectedOrder.addressId, latitude, longitude, }); Alert.alert( "Success", "Location attached to address successfully." ); } catch (error) { Alert.alert( "Error", "Failed to attach location. Please try again." ); } setShowOrderMenu(false); }} > Attach Location Save coordinates to address { const phoneMatch = selectedOrder?.address.match(/Phone: (\d+)/); const phone = phoneMatch ? phoneMatch[1] : null; if (phone) { Linking.openURL(`whatsapp://send?phone=+91${phone}`); } else { Alert.alert("No phone number found"); } setShowOrderMenu(false); }} > Message On WhatsApp Send message via WhatsApp { const phoneMatch = selectedOrder?.address.match(/Phone: (\d+)/); const phone = phoneMatch ? phoneMatch[1] : null; if (phone) { Linking.openURL(`tel:${phone}`); } else { Alert.alert("No phone number found"); } setShowOrderMenu(false); }} > Dial Mobile Number Call customer directly ); }