978 lines
34 KiB
TypeScript
978 lines
34 KiB
TypeScript
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<SlotProgressProps> = ({ 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 (
|
|
<View style={tw`bg-white px-4 py-3 border-b border-gray-200 mb-2`}>
|
|
<MyText style={tw`text-gray-700 text-sm mb-3`}>
|
|
Slot {slotId} Statistics
|
|
</MyText>
|
|
<View style={tw`flex-row flex-wrap`}>
|
|
{stats.map((stat, index) => (
|
|
<View key={index} style={tw`w-1/2 p-2`}>
|
|
<View style={tw`bg-gray-50 p-3 rounded-lg border border-gray-200`}>
|
|
<MyText style={tw`text-gray-900 font-bold text-lg`}>
|
|
{stat.value}
|
|
</MyText>
|
|
<MyText style={tw`text-gray-500 text-xs`}>{stat.label}</MyText>
|
|
</View>
|
|
</View>
|
|
))}
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
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<OrderItemProps> = ({
|
|
item,
|
|
drag,
|
|
isActive,
|
|
isPending,
|
|
setSelectedOrder,
|
|
setShowOrderMenu,
|
|
selectedOrderIds,
|
|
onToggleOrder,
|
|
assignedUserName,
|
|
staffData,
|
|
setSelectedUserId,
|
|
}) => {
|
|
const orderItem = item;
|
|
|
|
return (
|
|
<ScaleDecorator>
|
|
<TouchableOpacity
|
|
onLongPress={drag}
|
|
disabled={isPending}
|
|
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`}>
|
|
{/* Checkbox and Drag Handle */}
|
|
<View style={tw`mr-4 flex-col items-center`}>
|
|
<Checkbox
|
|
checked={selectedOrderIds.includes(orderItem.id)}
|
|
onPress={() => onToggleOrder(orderItem.id)}
|
|
size={20}
|
|
fillColor="#3b82f6"
|
|
checkColor="#FFFFFF"
|
|
/>
|
|
<View style={tw`mt-1`}>
|
|
<MaterialIcons
|
|
name="drag-indicator"
|
|
size={24}
|
|
color={isActive ? "#3b82f6" : "#9ca3af"}
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Content */}
|
|
<View style={tw`flex-1`}>
|
|
<View style={tw`flex-row justify-between items-start mb-2`}>
|
|
<View style={tw`flex-row items-center gap-2`}>
|
|
<MyText style={tw`font-bold text-gray-800 text-lg`}>
|
|
#{orderItem.readableId}
|
|
</MyText>
|
|
<View style={tw`flex-row items-center gap-1`}>
|
|
<MyText style={tw`text-xs font-medium text-gray-600`}>
|
|
Pkg
|
|
</MyText>
|
|
<Checkbox
|
|
checked={orderItem.isPackaged}
|
|
size={16}
|
|
fillColor="#6B7280"
|
|
checkColor="#FFFFFF"
|
|
/>
|
|
</View>
|
|
{/* Optional: Add time if available, or just keep ID */}
|
|
</View>
|
|
<View style={tw`flex-row items-center`}>
|
|
<View
|
|
style={tw`bg-green-50 px-2 py-1 rounded-lg border border-green-100 mr-2`}
|
|
>
|
|
<MyText style={tw`font-bold text-green-700 text-sm`}>
|
|
₹{orderItem.totalAmount}
|
|
</MyText>
|
|
</View>
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
setSelectedOrder(orderItem);
|
|
setShowOrderMenu(true);
|
|
}}
|
|
style={tw`p-2`}
|
|
>
|
|
<Entypo
|
|
name="dots-three-vertical"
|
|
size={20}
|
|
color="#6b7280"
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={tw`flex-row items-start`}>
|
|
<MaterialIcons
|
|
name="location-on"
|
|
size={14}
|
|
color="#6b7280"
|
|
style={tw`mr-1 mt-0.5`}
|
|
/>
|
|
<MyText
|
|
style={tw`text-gray-500 text-xs flex-1`}
|
|
numberOfLines={2}
|
|
>
|
|
{orderItem.address}
|
|
</MyText>
|
|
{orderItem.latitude && orderItem.longitude && (
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
Linking.openURL(
|
|
`https://www.google.com/maps?q=${orderItem.latitude},${orderItem.longitude}`
|
|
);
|
|
}}
|
|
style={tw`ml-2`}
|
|
>
|
|
<MaterialIcons name="map" size={16} color="#3b82f6" />
|
|
</TouchableOpacity>
|
|
)}
|
|
</View>
|
|
{assignedUserName && (
|
|
<View style={tw`mt-2 flex-row items-center`}>
|
|
<MyText style={tw`text-xs text-gray-500`}>
|
|
Assigned to{" "}
|
|
</MyText>
|
|
<TouchableOpacity
|
|
onPress={() => {
|
|
const assignedUserId = staffData?.staff?.find(
|
|
(s: { id: number; name: string }) => s.name === assignedUserName
|
|
)?.id;
|
|
if (assignedUserId && setSelectedUserId) {
|
|
setSelectedUserId(assignedUserId);
|
|
}
|
|
}}
|
|
>
|
|
<MyText style={tw`text-xs text-blue-600 underline`}>
|
|
{assignedUserName}
|
|
</MyText>
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
</View>
|
|
</View>
|
|
{/* Admin Notes */}
|
|
{orderItem.adminNotes && (
|
|
<View
|
|
style={tw`bg-yellow-50 p-2 rounded-lg border border-yellow-100 mt-2`}
|
|
>
|
|
<MyText style={tw`text-xs text-yellow-900 leading-4`}>
|
|
{orderItem.adminNotes}
|
|
</MyText>
|
|
</View>
|
|
)}
|
|
</View>
|
|
</TouchableOpacity>
|
|
</ScaleDecorator>
|
|
);
|
|
};
|
|
|
|
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<OrderWithSequence | null>(
|
|
null
|
|
);
|
|
const [selectedUserId, setSelectedUserId] = useState<number>(-1);
|
|
const [selectedOrderIds, setSelectedOrderIds] = useState<number[]>([]);
|
|
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<number, number>();
|
|
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 (
|
|
<View style={tw`flex-1 justify-center items-center bg-gray-50 pt-6`}>
|
|
<ActivityIndicator size="large" color="#3b82f6" />
|
|
<MyText style={tw`text-gray-500 mt-4`}>Loading slots...</MyText>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (slotsData.slots.length === 0) {
|
|
return (
|
|
<View style={tw`flex-1 justify-center items-center p-8 pt-6`}>
|
|
<MaterialIcons name="event-busy" size={64} color="#e5e7eb" />
|
|
<MyText style={tw`text-gray-500 mt-4 text-center text-lg`}>
|
|
No slots available
|
|
</MyText>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={tw`flex-1 bg-gray-50 pt-6`}>
|
|
{selectedSlotId ? (
|
|
<>
|
|
{/* Header Section */}
|
|
<View
|
|
style={tw`bg-white px-4 py-4 border-b border-gray-200 flex-row items-center shadow-sm z-10`}
|
|
>
|
|
<TouchableOpacity
|
|
onPress={() => router.back()}
|
|
style={tw`p-2 -ml-4`}
|
|
accessibilityLabel="Go back"
|
|
>
|
|
<MaterialIcons name="chevron-left" size={24} color="#374151" />
|
|
</TouchableOpacity>
|
|
|
|
<View style={tw`flex-2 mx-2`}>
|
|
<BottomDropdown
|
|
label="Select Slot"
|
|
options={slotOptions}
|
|
value={selectedSlotId || ""}
|
|
onValueChange={(val) => {
|
|
if (val) {
|
|
router.replace(`/delivery-sequences?slotId=${val}`);
|
|
}
|
|
}}
|
|
placeholder="Select slot"
|
|
/>
|
|
</View>
|
|
<View style={tw`flex-1 mx-2`}>
|
|
<BottomDropdown
|
|
label="Select User"
|
|
options={userOptions}
|
|
value={selectedUserId}
|
|
onValueChange={(val) => setSelectedUserId(val as number)}
|
|
placeholder="Select user"
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Content Section */}
|
|
{ordersLoading ? (
|
|
<View style={tw`flex-1 justify-center items-center`}>
|
|
<ActivityIndicator size="large" color="#3b82f6" />
|
|
<MyText style={tw`text-gray-500 mt-4`}>Loading orders...</MyText>
|
|
</View>
|
|
) : localOrderedOrders.length === 0 ? (
|
|
<View style={tw`flex-1 justify-center items-center p-8`}>
|
|
<MaterialIcons name="assignment-late" size={64} color="#e5e7eb" />
|
|
<MyText style={tw`text-gray-500 mt-4 text-center text-lg`}>
|
|
No orders found for this slot
|
|
</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={localOrderedOrders}
|
|
renderItem={({ item, drag, isActive }) => (
|
|
<OrderItem
|
|
item={item}
|
|
drag={drag}
|
|
isActive={isActive}
|
|
isPending={updateSequenceMutation.isPending}
|
|
setSelectedOrder={setSelectedOrder}
|
|
setShowOrderMenu={setShowOrderMenu}
|
|
selectedOrderIds={selectedOrderIds}
|
|
onToggleOrder={(id) => {
|
|
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={
|
|
<SlotProgress slotId={selectedSlotId!} orders={orders} />
|
|
}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
{/* FAB for Assignment */}
|
|
{selectedOrderIds.length > 0 && (
|
|
<View style={tw`absolute bottom-4 right-4`}>
|
|
<BottomDropdown
|
|
label="Assign To"
|
|
options={[
|
|
...userOptions.filter((opt) => 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 }) => (
|
|
<TouchableOpacity
|
|
onPress={onPress}
|
|
style={tw`bg-blue-600 p-4 rounded-full shadow-lg`}
|
|
>
|
|
<MaterialIcons name="person-add" size={24} color="white" />
|
|
</TouchableOpacity>
|
|
)}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
{/* FAB for Save */}
|
|
{hasSequenceChanged && (
|
|
<View style={tw`absolute bottom-4 left-4`}>
|
|
<TouchableOpacity
|
|
onPress={handleSaveSequence}
|
|
style={tw`bg-blue-600 p-4 rounded-full shadow-lg`}
|
|
disabled={updateSequenceMutation.isPending}
|
|
accessibilityLabel="Save sequence"
|
|
>
|
|
<MaterialIcons name="save" size={24} color="white" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
</>
|
|
) : (
|
|
<View style={tw`flex-1 justify-center items-center p-8`}>
|
|
<MaterialIcons name="event-busy" size={64} color="#e5e7eb" />
|
|
<MyText style={tw`text-gray-500 mt-4 text-center text-lg`}>
|
|
No slot selected. Please select a slot to view delivery sequence.
|
|
</MyText>
|
|
<TouchableOpacity
|
|
style={tw`mt-6 bg-blue-600 px-6 py-3 rounded-full`}
|
|
onPress={() => router.back()}
|
|
>
|
|
<MyText style={tw`text-white font-bold`}>Go Back</MyText>
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
|
|
{/* Order Menu Dialog */}
|
|
<BottomDialog
|
|
open={showOrderMenu}
|
|
onClose={() => setShowOrderMenu(false)}
|
|
>
|
|
<View style={tw`pb-8 pt-2 px-4`}>
|
|
{/* Handle Bar */}
|
|
<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`}>
|
|
Order #{selectedOrder?.readableId}
|
|
</MyText>
|
|
<MyText style={tw`text-sm text-gray-500`}>
|
|
Select an action to perform
|
|
</MyText>
|
|
</View>
|
|
|
|
{/* Actions */}
|
|
<TouchableOpacity
|
|
style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm`}
|
|
onPress={() => {
|
|
router.push(`/order-details/${selectedOrder?.id}`);
|
|
setShowOrderMenu(false);
|
|
}}
|
|
disabled={updateAddressCoordsMutation.isPending}
|
|
>
|
|
<View
|
|
style={tw`w-10 h-10 rounded-full bg-purple-50 items-center justify-center mr-4`}
|
|
>
|
|
<MaterialIcons name="visibility" size={20} color="#9333ea" />
|
|
</View>
|
|
<View>
|
|
<MyText style={tw`font-semibold text-gray-800 text-base`}>
|
|
View Details
|
|
</MyText>
|
|
<MyText style={tw`text-gray-500 text-xs`}>
|
|
See full order information
|
|
</MyText>
|
|
</View>
|
|
<MaterialIcons
|
|
name="chevron-right"
|
|
size={24}
|
|
color="#9ca3af"
|
|
style={tw`ml-auto`}
|
|
/>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm ${
|
|
updatePackagedMutation.isPending ? "opacity-50" : ""
|
|
}`}
|
|
onPress={async () => {
|
|
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}
|
|
>
|
|
<View
|
|
style={tw`w-10 h-10 rounded-full bg-blue-50 items-center justify-center mr-4`}
|
|
>
|
|
<MaterialIcons name="inventory" size={20} color="#2563eb" />
|
|
</View>
|
|
<View>
|
|
<MyText style={tw`font-semibold text-gray-800 text-base`}>
|
|
{selectedOrder?.isPackaged
|
|
? "Unmark Packaged"
|
|
: "Mark Packaged"}
|
|
</MyText>
|
|
<MyText style={tw`text-gray-500 text-xs`}>
|
|
{selectedOrder?.isPackaged
|
|
? "Revert to not packaged"
|
|
: "Update status to packaged"}
|
|
</MyText>
|
|
</View>
|
|
<MaterialIcons
|
|
name="chevron-right"
|
|
size={24}
|
|
color="#9ca3af"
|
|
style={tw`ml-auto`}
|
|
/>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm ${
|
|
updateDeliveredMutation.isPending ? "opacity-50" : ""
|
|
}`}
|
|
onPress={async () => {
|
|
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}
|
|
>
|
|
<View
|
|
style={tw`w-10 h-10 rounded-full bg-green-50 items-center justify-center mr-4`}
|
|
>
|
|
<MaterialIcons name="local-shipping" size={20} color="#16a34a" />
|
|
</View>
|
|
<View>
|
|
<MyText style={tw`font-semibold text-gray-800 text-base`}>
|
|
{selectedOrder?.isDelivered
|
|
? "Unmark Delivered"
|
|
: "Mark Delivered"}
|
|
</MyText>
|
|
<MyText style={tw`text-gray-500 text-xs`}>
|
|
{selectedOrder?.isDelivered
|
|
? "Revert delivery status"
|
|
: "Complete the delivery"}
|
|
</MyText>
|
|
</View>
|
|
<MaterialIcons
|
|
name="chevron-right"
|
|
size={24}
|
|
color="#9ca3af"
|
|
style={tw`ml-auto`}
|
|
/>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm ${
|
|
updateAddressCoordsMutation.isPending ? "opacity-50" : ""
|
|
}`}
|
|
onPress={async () => {
|
|
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);
|
|
}}
|
|
>
|
|
<View
|
|
style={tw`w-10 h-10 rounded-full bg-orange-50 items-center justify-center mr-4`}
|
|
>
|
|
<MaterialIcons
|
|
name="add-location-alt"
|
|
size={20}
|
|
color="#ea580c"
|
|
/>
|
|
</View>
|
|
<View>
|
|
<MyText style={tw`font-semibold text-gray-800 text-base`}>
|
|
Attach Location
|
|
</MyText>
|
|
<MyText style={tw`text-gray-500 text-xs`}>
|
|
Save coordinates to address
|
|
</MyText>
|
|
</View>
|
|
<MaterialIcons
|
|
name="chevron-right"
|
|
size={24}
|
|
color="#9ca3af"
|
|
style={tw`ml-auto`}
|
|
/>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm`}
|
|
onPress={() => {
|
|
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);
|
|
}}
|
|
>
|
|
<View
|
|
style={tw`w-10 h-10 rounded-full bg-green-50 items-center justify-center mr-4`}
|
|
>
|
|
<MaterialIcons name="message" size={20} color="#16a34a" />
|
|
</View>
|
|
<View>
|
|
<MyText style={tw`font-semibold text-gray-800 text-base`}>
|
|
Message On WhatsApp
|
|
</MyText>
|
|
<MyText style={tw`text-gray-500 text-xs`}>
|
|
Send message via WhatsApp
|
|
</MyText>
|
|
</View>
|
|
<MaterialIcons
|
|
name="chevron-right"
|
|
size={24}
|
|
color="#9ca3af"
|
|
style={tw`ml-auto`}
|
|
/>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm`}
|
|
onPress={() => {
|
|
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);
|
|
}}
|
|
>
|
|
<View
|
|
style={tw`w-10 h-10 rounded-full bg-green-50 items-center justify-center mr-4`}
|
|
>
|
|
<MaterialIcons name="phone" size={20} color="#16a34a" />
|
|
</View>
|
|
<View>
|
|
<MyText style={tw`font-semibold text-gray-800 text-base`}>
|
|
Dial Mobile Number
|
|
</MyText>
|
|
<MyText style={tw`text-gray-500 text-xs`}>
|
|
Call customer directly
|
|
</MyText>
|
|
</View>
|
|
<MaterialIcons
|
|
name="chevron-right"
|
|
size={24}
|
|
color="#9ca3af"
|
|
style={tw`ml-auto`}
|
|
/>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</BottomDialog>
|
|
</View>
|
|
);
|
|
}
|