enh
This commit is contained in:
parent
e96ed2334e
commit
d658022a51
9 changed files with 4056 additions and 346 deletions
|
|
@ -27,6 +27,9 @@ import { trpc } from "@/src/trpc-client";
|
||||||
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
|
||||||
import Entypo from "@expo/vector-icons/Entypo";
|
import Entypo from "@expo/vector-icons/Entypo";
|
||||||
import * as Location from "expo-location";
|
import * as Location from "expo-location";
|
||||||
|
import { OrderOptionsMenu } from "@/components/OrderOptionsMenu";
|
||||||
|
import CancelOrderDialog from "@/components/CancelOrderDialog";
|
||||||
|
import { OrderNotesForm } from "@/components/OrderNotesForm";
|
||||||
|
|
||||||
// Define types outside
|
// Define types outside
|
||||||
interface OrderWithSequence {
|
interface OrderWithSequence {
|
||||||
|
|
@ -270,6 +273,8 @@ export default function DeliverySequences() {
|
||||||
const [selectedUserId, setSelectedUserId] = useState<number>(-1);
|
const [selectedUserId, setSelectedUserId] = useState<number>(-1);
|
||||||
const [selectedOrderIds, setSelectedOrderIds] = useState<number[]>([]);
|
const [selectedOrderIds, setSelectedOrderIds] = useState<number[]>([]);
|
||||||
const [hasSequenceChanged, setHasSequenceChanged] = useState(false);
|
const [hasSequenceChanged, setHasSequenceChanged] = useState(false);
|
||||||
|
const [cancelDialogOpen, setCancelDialogOpen] = useState(false);
|
||||||
|
const [notesDialogOpen, setNotesDialogOpen] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { data: slotsData, refetch: refetchSlots } =
|
const { data: slotsData, refetch: refetchSlots } =
|
||||||
|
|
@ -705,273 +710,90 @@ export default function DeliverySequences() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Order Menu Dialog */}
|
{/* Order Menu Dialog */}
|
||||||
<BottomDialog
|
<OrderOptionsMenu
|
||||||
open={showOrderMenu}
|
open={showOrderMenu}
|
||||||
onClose={() => setShowOrderMenu(false)}
|
onClose={() => setShowOrderMenu(false)}
|
||||||
>
|
order={{
|
||||||
<View style={tw`pb-8 pt-2 px-4`}>
|
id: selectedOrder?.id || 0,
|
||||||
{/* Handle Bar */}
|
readableId: selectedOrder?.readableId || 0,
|
||||||
<View style={tw`items-center mb-6`}>
|
isPackaged: selectedOrder?.isPackaged || false,
|
||||||
<View style={tw`w-12 h-1.5 bg-gray-200 rounded-full mb-4`} />
|
isDelivered: selectedOrder?.isDelivered || false,
|
||||||
<MyText style={tw`text-lg font-bold text-gray-900`}>
|
address: selectedOrder?.address || '',
|
||||||
Order #{selectedOrder?.readableId}
|
addressId: selectedOrder?.addressId || 0,
|
||||||
</MyText>
|
adminNotes: selectedOrder?.adminNotes || null,
|
||||||
<MyText style={tw`text-sm text-gray-500`}>
|
latitude: selectedOrder?.latitude || null,
|
||||||
Select an action to perform
|
longitude: selectedOrder?.longitude || null,
|
||||||
</MyText>
|
}}
|
||||||
</View>
|
onViewDetails={() => {
|
||||||
|
if (selectedOrder) {
|
||||||
{/* Actions */}
|
router.push(`/order-details/${selectedOrder.id}`);
|
||||||
<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);
|
setShowOrderMenu(false);
|
||||||
}}
|
}}
|
||||||
disabled={updateAddressCoordsMutation.isPending}
|
onTogglePackaged={() => {
|
||||||
>
|
|
||||||
<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;
|
if (!selectedOrder) return;
|
||||||
try {
|
updatePackagedMutation.mutate(
|
||||||
await updatePackagedMutation.mutateAsync({
|
{ orderId: selectedOrder.id.toString(), isPackaged: !selectedOrder.isPackaged },
|
||||||
orderId: selectedOrder.id.toString(),
|
{
|
||||||
isPackaged: !selectedOrder.isPackaged,
|
onSuccess: () => {
|
||||||
});
|
|
||||||
refetchOrders();
|
refetchOrders();
|
||||||
refetchSequence();
|
refetchSequence();
|
||||||
setShowOrderMenu(false);
|
},
|
||||||
} catch (error) {
|
onError: () => {
|
||||||
Alert.alert("Error", "Failed to update packaged status");
|
Alert.alert("Error", "Failed to update packaged status");
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
disabled={updatePackagedMutation.isPending}
|
onToggleDelivered={() => {
|
||||||
>
|
|
||||||
<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;
|
if (!selectedOrder) return;
|
||||||
try {
|
updateDeliveredMutation.mutate(
|
||||||
await updateDeliveredMutation.mutateAsync({
|
{ orderId: selectedOrder.id.toString(), isDelivered: !selectedOrder.isDelivered },
|
||||||
orderId: selectedOrder.id.toString(),
|
{
|
||||||
isDelivered: !selectedOrder.isDelivered,
|
onSuccess: () => {
|
||||||
});
|
|
||||||
refetchOrders();
|
refetchOrders();
|
||||||
refetchSequence();
|
refetchSequence();
|
||||||
setShowOrderMenu(false);
|
},
|
||||||
} catch (error) {
|
onError: () => {
|
||||||
Alert.alert("Error", "Failed to update delivered status");
|
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);
|
|
||||||
}}
|
}}
|
||||||
>
|
onOpenAdminNotes={() => {
|
||||||
<View
|
setShowOrderMenu(false);
|
||||||
style={tw`w-10 h-10 rounded-full bg-orange-50 items-center justify-center mr-4`}
|
setNotesDialogOpen(true);
|
||||||
>
|
}}
|
||||||
<MaterialIcons
|
onCancelOrder={() => {
|
||||||
name="add-location-alt"
|
setShowOrderMenu(false);
|
||||||
size={20}
|
setCancelDialogOpen(true);
|
||||||
color="#ea580c"
|
}}
|
||||||
|
onAttachLocation={() => {}}
|
||||||
|
onWhatsApp={() => {}}
|
||||||
|
onDial={() => {}}
|
||||||
/>
|
/>
|
||||||
</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
|
<CancelOrderDialog
|
||||||
style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm`}
|
orderId={selectedOrder?.id || 0}
|
||||||
onPress={() => {
|
open={cancelDialogOpen}
|
||||||
const phoneMatch = selectedOrder?.address.match(/Phone: (\d+)/);
|
onClose={() => setCancelDialogOpen(false)}
|
||||||
const phone = phoneMatch ? phoneMatch[1] : null;
|
onSuccess={() => {
|
||||||
if (phone) {
|
refetchOrders();
|
||||||
Linking.openURL(`whatsapp://send?phone=+91${phone}`);
|
refetchSequence();
|
||||||
} 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
|
<BottomDialog open={notesDialogOpen} onClose={() => setNotesDialogOpen(false)}>
|
||||||
style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm`}
|
<OrderNotesForm
|
||||||
onPress={() => {
|
orderId={selectedOrder?.id || 0}
|
||||||
const phoneMatch = selectedOrder?.address.match(/Phone: (\d+)/);
|
initialNotes={selectedOrder?.adminNotes || ''}
|
||||||
const phone = phoneMatch ? phoneMatch[1] : null;
|
onSuccess={() => {
|
||||||
if (phone) {
|
refetchOrders();
|
||||||
Linking.openURL(`tel:${phone}`);
|
refetchSequence();
|
||||||
} else {
|
setNotesDialogOpen(false);
|
||||||
Alert.alert("No phone number found");
|
|
||||||
}
|
|
||||||
setShowOrderMenu(false);
|
|
||||||
}}
|
}}
|
||||||
>
|
onCancel={() => setNotesDialogOpen(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>
|
</BottomDialog>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState , useEffect } from 'react';
|
import React, { useState , useEffect } from 'react';
|
||||||
import { View, TouchableOpacity, Alert, TextInput, ActivityIndicator } from 'react-native';
|
import { View, TouchableOpacity, Alert, TextInput, ActivityIndicator, Linking } from 'react-native';
|
||||||
import { AppContainer, MyText, tw, MyFlatList, BottomDialog, BottomDropdown, Checkbox, theme, MyTextInput } from 'common-ui';
|
import { AppContainer, MyText, tw, MyFlatList, BottomDialog, BottomDropdown, Checkbox, theme, MyTextInput } from 'common-ui';
|
||||||
import { trpc } from '../../../src/trpc-client';
|
import { trpc } from '../../../src/trpc-client';
|
||||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||||
|
|
@ -7,6 +7,8 @@ import dayjs from 'dayjs';
|
||||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
import { Entypo } from '@expo/vector-icons';
|
import { Entypo } from '@expo/vector-icons';
|
||||||
import CancelOrderDialog from '@/components/CancelOrderDialog';
|
import CancelOrderDialog from '@/components/CancelOrderDialog';
|
||||||
|
import { OrderOptionsMenu } from '@/components/OrderOptionsMenu';
|
||||||
|
import * as Location from 'expo-location';
|
||||||
|
|
||||||
const AdminNotesForm = ({ orderId, existingNotes, onClose, refetch }: { orderId: string; existingNotes?: string | null; onClose: () => void; refetch: () => void }) => {
|
const AdminNotesForm = ({ orderId, existingNotes, onClose, refetch }: { orderId: string; existingNotes?: string | null; onClose: () => void; refetch: () => void }) => {
|
||||||
const [notesText, setNotesText] = useState(existingNotes || '');
|
const [notesText, setNotesText] = useState(existingNotes || '');
|
||||||
|
|
@ -111,7 +113,6 @@ const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }
|
||||||
{ orderId: order.orderId.toString(), isPackaged },
|
{ orderId: order.orderId.toString(), isPackaged },
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setMenuOpen(false);
|
|
||||||
refetch();
|
refetch();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -123,7 +124,6 @@ const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }
|
||||||
{ orderId: order.orderId.toString(), isDelivered },
|
{ orderId: order.orderId.toString(), isDelivered },
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setMenuOpen(false);
|
|
||||||
refetch();
|
refetch();
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -344,79 +344,38 @@ const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<BottomDialog open={menuOpen} onClose={() => setMenuOpen(false)}>
|
<OrderOptionsMenu
|
||||||
<View style={tw`p-6`}>
|
open={menuOpen}
|
||||||
<MyText style={tw`text-lg font-bold text-gray-800 mb-4`}>
|
onClose={() => setMenuOpen(false)}
|
||||||
Order Options
|
order={{
|
||||||
</MyText>
|
id: order.id,
|
||||||
{order.isFlashDelivery && (
|
readableId: order.readableId,
|
||||||
<View style={tw`bg-amber-50 border border-amber-200 rounded-lg p-3 mb-4 flex-row items-center`}>
|
isPackaged: order.isPackaged,
|
||||||
<MaterialIcons name="bolt" size={20} color="#D97706" />
|
isDelivered: order.isDelivered,
|
||||||
<View style={tw`ml-3 flex-1`}>
|
isFlashDelivery: order.isFlashDelivery,
|
||||||
<MyText style={tw`text-sm font-bold text-amber-900`}>Flash Delivery Order</MyText>
|
address: order.address,
|
||||||
<MyText style={tw`text-xs text-amber-700`}>
|
addressId: 0,
|
||||||
Deliver within 30 minutes • High Priority
|
adminNotes: order.adminNotes,
|
||||||
</MyText>
|
userNotes: order.userNotes,
|
||||||
</View>
|
latitude: null,
|
||||||
</View>
|
longitude: null,
|
||||||
)}
|
status: order.status,
|
||||||
<TouchableOpacity
|
}}
|
||||||
style={tw`flex-row items-center p-4 bg-gray-50 rounded-lg mb-3`}
|
onViewDetails={handleMenuOption}
|
||||||
onPress={() => handleMarkPackaged(!order.isPackaged)}
|
onTogglePackaged={() => handleMarkPackaged(!order.isPackaged)}
|
||||||
>
|
onToggleDelivered={() => handleMarkDelivered(!order.isDelivered)}
|
||||||
<Entypo name="box" size={20} color="#6B7280" />
|
onOpenAdminNotes={() => {
|
||||||
<MyText style={tw`text-gray-800 font-medium ml-3`}>
|
|
||||||
{order.isPackaged ? 'Mark Not Packaged' : 'Mark Packaged'}
|
|
||||||
</MyText>
|
|
||||||
</TouchableOpacity>
|
|
||||||
{order.isPackaged && (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={tw`flex-row items-center p-4 bg-gray-50 rounded-lg mb-3`}
|
|
||||||
onPress={() => handleMarkDelivered(!order.isDelivered)}
|
|
||||||
>
|
|
||||||
<Entypo name="location" size={20} color="#6B7280" />
|
|
||||||
<MyText style={tw`text-gray-800 font-medium ml-3`}>
|
|
||||||
{order.isDelivered ? 'Mark Not Delivered' : 'Mark Delivered'}
|
|
||||||
</MyText>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
<TouchableOpacity
|
|
||||||
style={tw`flex-row items-center p-4 bg-gray-50 rounded-lg mb-3`}
|
|
||||||
onPress={handleMenuOption}
|
|
||||||
>
|
|
||||||
<Entypo name="info-with-circle" size={20} color="#6B7280" />
|
|
||||||
<MyText style={tw`text-gray-800 font-medium ml-3`}>
|
|
||||||
Order Details
|
|
||||||
</MyText>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={tw`flex-row items-center p-4 bg-gray-50 rounded-lg mb-3`}
|
|
||||||
onPress={() => {
|
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
setNotesDialogOpen(true);
|
setNotesDialogOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
onCancelOrder={() => {
|
||||||
<Entypo name="edit" size={20} color="#6B7280" />
|
|
||||||
<MyText style={tw`text-gray-800 font-medium ml-3`}>
|
|
||||||
Admin Notes
|
|
||||||
</MyText>
|
|
||||||
</TouchableOpacity>
|
|
||||||
{order.status !== 'cancelled' && (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={tw`flex-row items-center p-4 bg-red-50 rounded-lg`}
|
|
||||||
onPress={() => {
|
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
setCancelDialogOpen(true);
|
setCancelDialogOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
onAttachLocation={() => {}}
|
||||||
<MaterialIcons name="cancel" size={20} color="#DC2626" />
|
onWhatsApp={() => {}}
|
||||||
<MyText style={tw`text-red-700 font-medium ml-3`}>
|
onDial={() => {}}
|
||||||
Cancel Order
|
/>
|
||||||
</MyText>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</BottomDialog>
|
|
||||||
|
|
||||||
<BottomDialog open={itemsDialogOpen} onClose={() => setItemsDialogOpen(false)}>
|
<BottomDialog open={itemsDialogOpen} onClose={() => setItemsDialogOpen(false)}>
|
||||||
<View style={tw`py-6`}>
|
<View style={tw`py-6`}>
|
||||||
|
|
|
||||||
306
apps/admin-ui/components/OrderOptionsMenu.tsx
Normal file
306
apps/admin-ui/components/OrderOptionsMenu.tsx
Normal file
|
|
@ -0,0 +1,306 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { View, TouchableOpacity, Linking, Alert, TextInput, ScrollView, Dimensions } from 'react-native';
|
||||||
|
import * as Location from 'expo-location';
|
||||||
|
|
||||||
|
const { height: SCREEN_HEIGHT } = Dimensions.get('window');
|
||||||
|
import {
|
||||||
|
MyText,
|
||||||
|
tw,
|
||||||
|
BottomDialog,
|
||||||
|
} from 'common-ui';
|
||||||
|
import { trpc } from '@/src/trpc-client';
|
||||||
|
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||||
|
import Ionicons from '@expo/vector-icons/Ionicons';
|
||||||
|
|
||||||
|
interface OrderOptionsMenuProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
order: {
|
||||||
|
id: number;
|
||||||
|
readableId: number;
|
||||||
|
isPackaged: boolean;
|
||||||
|
isDelivered: boolean;
|
||||||
|
isFlashDelivery?: boolean;
|
||||||
|
address: string;
|
||||||
|
addressId: number;
|
||||||
|
adminNotes?: string | null;
|
||||||
|
userNotes?: string | null;
|
||||||
|
latitude?: number | null;
|
||||||
|
longitude?: number | null;
|
||||||
|
status?: string;
|
||||||
|
};
|
||||||
|
onViewDetails: () => void;
|
||||||
|
onTogglePackaged: () => void;
|
||||||
|
onToggleDelivered: () => void;
|
||||||
|
onOpenAdminNotes: () => void;
|
||||||
|
onCancelOrder: () => void;
|
||||||
|
onAttachLocation: () => void;
|
||||||
|
onWhatsApp: () => void;
|
||||||
|
onDial: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OrderOptionsMenu({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
order,
|
||||||
|
onViewDetails,
|
||||||
|
onTogglePackaged,
|
||||||
|
onToggleDelivered,
|
||||||
|
onOpenAdminNotes,
|
||||||
|
onCancelOrder,
|
||||||
|
onAttachLocation,
|
||||||
|
onWhatsApp,
|
||||||
|
onDial,
|
||||||
|
}: OrderOptionsMenuProps) {
|
||||||
|
const updateAddressCoordsMutation = trpc.admin.order.updateAddressCoords.useMutation();
|
||||||
|
|
||||||
|
const handleAttachLocation = async () => {
|
||||||
|
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: order.addressId,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
});
|
||||||
|
Alert.alert('Success', 'Location attached to address successfully.');
|
||||||
|
onAttachLocation();
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('Error', 'Failed to attach location. Please try again.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractPhone = (address: string) => {
|
||||||
|
const phoneMatch = address.match(/Phone: (\d+)/);
|
||||||
|
return phoneMatch ? phoneMatch[1] : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWhatsApp = () => {
|
||||||
|
const phone = extractPhone(order.address);
|
||||||
|
if (phone) {
|
||||||
|
Linking.openURL(`whatsapp://send?phone=+91${phone}`);
|
||||||
|
} else {
|
||||||
|
Alert.alert('No phone number found');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDial = () => {
|
||||||
|
const phone = extractPhone(order.address);
|
||||||
|
if (phone) {
|
||||||
|
Linking.openURL(`tel:${phone}`);
|
||||||
|
} else {
|
||||||
|
Alert.alert('No phone number found');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BottomDialog open={open} onClose={onClose}>
|
||||||
|
<View style={{ maxHeight: SCREEN_HEIGHT * 0.7 }}>
|
||||||
|
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{ flexGrow: 1 }}>
|
||||||
|
<View style={tw`pb-8 pt-2 px-4`}>
|
||||||
|
<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 #{order.readableId}
|
||||||
|
</MyText>
|
||||||
|
<MyText style={tw`text-sm text-gray-500`}>
|
||||||
|
Select an action to perform
|
||||||
|
</MyText>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm`}
|
||||||
|
onPress={() => {
|
||||||
|
onViewDetails();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<View style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm`}>
|
||||||
|
<View
|
||||||
|
style={tw`p-1`}
|
||||||
|
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
|
||||||
|
onStartShouldSetResponder={() => true}
|
||||||
|
onResponderEnd={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onTogglePackaged();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={order.isPackaged ? 'checkbox' : 'square-outline'}
|
||||||
|
size={24}
|
||||||
|
color={order.isPackaged ? '#10B981' : '#1570EF'}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={tw`ml-3 flex-1`}>
|
||||||
|
<MyText style={tw`font-semibold text-gray-800 text-base`}>
|
||||||
|
Packaged
|
||||||
|
</MyText>
|
||||||
|
<MyText style={tw`text-gray-500 text-xs`}>
|
||||||
|
{order.isPackaged ? 'Mark as not packaged' : 'Mark as packaged'}
|
||||||
|
</MyText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm`}>
|
||||||
|
<View
|
||||||
|
style={tw`p-1`}
|
||||||
|
hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
|
||||||
|
onStartShouldSetResponder={() => true}
|
||||||
|
onResponderEnd={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onToggleDelivered();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={order.isDelivered ? 'checkbox' : 'square-outline'}
|
||||||
|
size={24}
|
||||||
|
color={order.isDelivered ? '#10B981' : '#1570EF'}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={tw`ml-3 flex-1`}>
|
||||||
|
<MyText style={tw`font-semibold text-gray-800 text-base`}>
|
||||||
|
Delivered
|
||||||
|
</MyText>
|
||||||
|
<MyText style={tw`text-gray-500 text-xs`}>
|
||||||
|
{order.isDelivered ? 'Mark as not delivered' : 'Mark as delivered'}
|
||||||
|
</MyText>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm`}
|
||||||
|
onPress={() => {
|
||||||
|
onOpenAdminNotes();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={tw`w-10 h-10 rounded-full bg-yellow-50 items-center justify-center mr-4`}>
|
||||||
|
<MaterialIcons name="edit" size={20} color="#ca8a04" />
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<MyText style={tw`font-semibold text-gray-800 text-base`}>
|
||||||
|
Admin Notes
|
||||||
|
</MyText>
|
||||||
|
<MyText style={tw`text-gray-500 text-xs`}>
|
||||||
|
{order.adminNotes ? 'Edit existing notes' : 'Add admin notes'}
|
||||||
|
</MyText>
|
||||||
|
</View>
|
||||||
|
<MaterialIcons name="chevron-right" size={24} color="#9ca3af" style={tw`ml-auto`} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{order.status !== 'cancelled' && (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm`}
|
||||||
|
onPress={() => {
|
||||||
|
onCancelOrder();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={tw`w-10 h-10 rounded-full bg-red-50 items-center justify-center mr-4`}>
|
||||||
|
<MaterialIcons name="cancel" size={20} color="#dc2626" />
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<MyText style={tw`font-semibold text-red-700 text-base`}>
|
||||||
|
Cancel Order
|
||||||
|
</MyText>
|
||||||
|
<MyText style={tw`text-red-500 text-xs`}>
|
||||||
|
Cancel and provide reason
|
||||||
|
</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={() => {
|
||||||
|
handleAttachLocation();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
disabled={updateAddressCoordsMutation.isPending}
|
||||||
|
>
|
||||||
|
<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 GPS 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={() => {
|
||||||
|
handleWhatsApp();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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={() => {
|
||||||
|
handleDial();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</BottomDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
2
apps/backend/drizzle/0068_colossal_magma.sql
Normal file
2
apps/backend/drizzle/0068_colossal_magma.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE "mf"."addresses" ADD COLUMN "admin_latitude" real;--> statement-breakpoint
|
||||||
|
ALTER TABLE "mf"."addresses" ADD COLUMN "admin_longitude" real;
|
||||||
3612
apps/backend/drizzle/meta/0068_snapshot.json
Normal file
3612
apps/backend/drizzle/meta/0068_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -477,6 +477,13 @@
|
||||||
"when": 1769280779210,
|
"when": 1769280779210,
|
||||||
"tag": "0067_messy_earthquake",
|
"tag": "0067_messy_earthquake",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 68,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1769709890336,
|
||||||
|
"tag": "0068_colossal_magma",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -48,6 +48,8 @@ export const addresses = mf.table('addresses', {
|
||||||
isDefault: boolean('is_default').notNull().default(false),
|
isDefault: boolean('is_default').notNull().default(false),
|
||||||
latitude: real('latitude'),
|
latitude: real('latitude'),
|
||||||
longitude: real('longitude'),
|
longitude: real('longitude'),
|
||||||
|
adminLatitude: real('admin_latitude'),
|
||||||
|
adminLongitude: real('admin_longitude'),
|
||||||
zoneId: integer('zone_id').references(() => addressZones.id),
|
zoneId: integer('zone_id').references(() => addressZones.id),
|
||||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -520,8 +520,8 @@ export const orderRouter = router({
|
||||||
order.address.pincode
|
order.address.pincode
|
||||||
}, Phone: ${order.address.phone}`,
|
}, Phone: ${order.address.phone}`,
|
||||||
addressId: order.addressId,
|
addressId: order.addressId,
|
||||||
latitude: order.address.latitude,
|
latitude: order.address.adminLatitude ?? order.address.latitude,
|
||||||
longitude: order.address.longitude,
|
longitude: order.address.adminLongitude ?? order.address.longitude,
|
||||||
totalAmount: parseFloat(order.totalAmount),
|
totalAmount: parseFloat(order.totalAmount),
|
||||||
items,
|
items,
|
||||||
deliveryTime: order.slot?.deliveryTime.toISOString() || null,
|
deliveryTime: order.slot?.deliveryTime.toISOString() || null,
|
||||||
|
|
@ -646,8 +646,8 @@ export const orderRouter = router({
|
||||||
const result = await db
|
const result = await db
|
||||||
.update(addresses)
|
.update(addresses)
|
||||||
.set({
|
.set({
|
||||||
latitude,
|
adminLatitude: latitude,
|
||||||
longitude,
|
adminLongitude: longitude,
|
||||||
})
|
})
|
||||||
.where(eq(addresses.id, addressId))
|
.where(eq(addresses.id, addressId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
@ -794,8 +794,8 @@ export const orderRouter = router({
|
||||||
order.address.pincode
|
order.address.pincode
|
||||||
}, Phone: ${order.address.phone}`,
|
}, Phone: ${order.address.phone}`,
|
||||||
addressId: order.addressId,
|
addressId: order.addressId,
|
||||||
latitude: order.address.latitude,
|
latitude: order.address.adminLatitude ?? order.address.latitude,
|
||||||
longitude: order.address.longitude,
|
longitude: order.address.adminLongitude ?? order.address.longitude,
|
||||||
totalAmount: parseFloat(order.totalAmount),
|
totalAmount: parseFloat(order.totalAmount),
|
||||||
deliveryCharge: parseFloat(order.deliveryCharge || "0"),
|
deliveryCharge: parseFloat(order.deliveryCharge || "0"),
|
||||||
items,
|
items,
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,8 @@ const isDevMode = Constants.executionEnvironment !== "standalone";
|
||||||
// const BASE_API_URL = API_URL;
|
// const BASE_API_URL = API_URL;
|
||||||
// const BASE_API_URL = 'http://10.0.2.2:4000';
|
// const BASE_API_URL = 'http://10.0.2.2:4000';
|
||||||
// const BASE_API_URL = 'http://192.168.100.101:4000';
|
// const BASE_API_URL = 'http://192.168.100.101:4000';
|
||||||
// const BASE_API_URL = 'http://192.168.1.14:4000';
|
const BASE_API_URL = 'http://192.168.1.14:4000';
|
||||||
let BASE_API_URL = "https://mf.freshyo.in";
|
// let BASE_API_URL = "https://mf.freshyo.in";
|
||||||
// let BASE_API_URL = 'http://192.168.100.103:4000';
|
// let BASE_API_URL = 'http://192.168.100.103:4000';
|
||||||
// let BASE_API_URL = 'http://192.168.29.219:4000';
|
// let BASE_API_URL = 'http://192.168.29.219:4000';
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue