Compare commits

..

No commits in common. "a8e52583d4550506ed1c42ad8282110a5838da4e" and "e96ed2334ed122368190b2aa2c4fe8f9458b21ad" have entirely different histories.

21 changed files with 435 additions and 8008 deletions

View file

@ -5,7 +5,6 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { MyText, tw } from 'common-ui'; import { MyText, tw } from 'common-ui';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { theme } from 'common-ui/src/theme'; import { theme } from 'common-ui/src/theme';
import { trpc } from '@/src/trpc-client';
interface MenuItem { interface MenuItem {
title: string; title: string;
@ -15,7 +14,6 @@ interface MenuItem {
category: 'quick' | 'products' | 'orders' | 'marketing' | 'settings'; category: 'quick' | 'products' | 'orders' | 'marketing' | 'settings';
iconColor?: string; iconColor?: string;
iconBg?: string; iconBg?: string;
badgeCount?: number;
} }
interface MenuItemComponentProps { interface MenuItemComponentProps {
@ -23,9 +21,7 @@ interface MenuItemComponentProps {
router: any; router: any;
} }
const MenuItemComponent: React.FC<MenuItemComponentProps> = ({ item, router }) => { const MenuItemComponent: React.FC<MenuItemComponentProps> = ({ item, router }) => (
return (
<Pressable <Pressable
key={item.route} key={item.route}
onPress={() => router.push(item.route as any)} onPress={() => router.push(item.route as any)}
@ -43,23 +39,13 @@ const MenuItemComponent: React.FC<MenuItemComponentProps> = ({ item, router }) =
<MyText style={tw`text-gray-500 text-xs`}>{item.description}</MyText> <MyText style={tw`text-gray-500 text-xs`}>{item.description}</MyText>
)} )}
</View> </View>
{item.badgeCount ? ( <MaterialIcons name="chevron-right" size={24} color="#D1D5DB" />
<View style={tw`bg-red-500 px-2 py-1 rounded-full`}>
<MyText style={tw`text-white text-xs font-bold`}>{item.badgeCount}</MyText>
</View>
) : null}
<MaterialIcons name="chevron-right" size={24} color="#D1D5DB" style={tw`ml-2`} />
</Pressable> </Pressable>
) );
}
export default function Dashboard() { export default function Dashboard() {
const router = useRouter(); const router = useRouter();
const { data: essentialsData } = trpc.admin.user.getEssentials.useQuery();
console.log({essentialsData})
const menuItems: MenuItem[] = [ const menuItems: MenuItem[] = [
{ {
title: 'Manage Orders', title: 'Manage Orders',
@ -105,7 +91,6 @@ export default function Dashboard() {
category: 'quick', category: 'quick',
iconColor: '#F59E0B', iconColor: '#F59E0B',
iconBg: '#FEF3C7', iconBg: '#FEF3C7',
badgeCount: essentialsData?.unresolvedComplaints,
}, },
{ {
title: 'Products', title: 'Products',
@ -222,11 +207,6 @@ export default function Dashboard() {
> >
<View style={[tw`w-10 h-10 rounded-lg items-center justify-center mb-2`, { backgroundColor: item.iconBg }]}> <View style={[tw`w-10 h-10 rounded-lg items-center justify-center mb-2`, { backgroundColor: item.iconBg }]}>
<MaterialIcons name={item.icon as any} size={20} color={item.iconColor} /> <MaterialIcons name={item.icon as any} size={20} color={item.iconColor} />
{item.badgeCount ? (
<View style={tw`absolute -top-1 -right-1 bg-red-500 min-w-5 h-5 rounded-full items-center justify-center px-1`}>
<MyText style={tw`text-white text-[10px] font-bold`}>{item.badgeCount}</MyText>
</View>
) : null}
</View> </View>
<MyText style={tw`text-gray-900 font-bold text-xs text-center`} numberOfLines={2}> <MyText style={tw`text-gray-900 font-bold text-xs text-center`} numberOfLines={2}>
{item.title} {item.title}

View file

@ -27,9 +27,6 @@ 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 {
@ -273,8 +270,6 @@ 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 } =
@ -710,90 +705,273 @@ export default function DeliverySequences() {
)} )}
{/* Order Menu Dialog */} {/* Order Menu Dialog */}
<OrderOptionsMenu <BottomDialog
open={showOrderMenu} open={showOrderMenu}
onClose={() => setShowOrderMenu(false)} onClose={() => setShowOrderMenu(false)}
order={{ >
id: selectedOrder?.id || 0, <View style={tw`pb-8 pt-2 px-4`}>
readableId: selectedOrder?.readableId || 0, {/* Handle Bar */}
isPackaged: selectedOrder?.isPackaged || false, <View style={tw`items-center mb-6`}>
isDelivered: selectedOrder?.isDelivered || false, <View style={tw`w-12 h-1.5 bg-gray-200 rounded-full mb-4`} />
address: selectedOrder?.address || '', <MyText style={tw`text-lg font-bold text-gray-900`}>
addressId: selectedOrder?.addressId || 0, Order #{selectedOrder?.readableId}
adminNotes: selectedOrder?.adminNotes || null, </MyText>
latitude: selectedOrder?.latitude || null, <MyText style={tw`text-sm text-gray-500`}>
longitude: selectedOrder?.longitude || null, Select an action to perform
}} </MyText>
onViewDetails={() => { </View>
if (selectedOrder) {
router.push(`/order-details/${selectedOrder.id}`); {/* Actions */}
} <TouchableOpacity
setShowOrderMenu(false); style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm`}
}} onPress={() => {
onTogglePackaged={() => { router.push(`/order-details/${selectedOrder?.id}`);
if (!selectedOrder) return; setShowOrderMenu(false);
updatePackagedMutation.mutate( }}
{ orderId: selectedOrder.id.toString(), isPackaged: !selectedOrder.isPackaged }, disabled={updateAddressCoordsMutation.isPending}
{ >
onSuccess: () => { <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(); refetchOrders();
refetchSequence(); refetchSequence();
}, setShowOrderMenu(false);
onError: () => { } catch (error) {
Alert.alert("Error", "Failed to update packaged status"); Alert.alert("Error", "Failed to update packaged status");
}, }
} }}
); disabled={updatePackagedMutation.isPending}
}} >
onToggleDelivered={() => { <View
if (!selectedOrder) return; style={tw`w-10 h-10 rounded-full bg-blue-50 items-center justify-center mr-4`}
updateDeliveredMutation.mutate( >
{ orderId: selectedOrder.id.toString(), isDelivered: !selectedOrder.isDelivered }, <MaterialIcons name="inventory" size={20} color="#2563eb" />
{ </View>
onSuccess: () => { <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(); refetchOrders();
refetchSequence(); refetchSequence();
}, setShowOrderMenu(false);
onError: () => { } catch (error) {
Alert.alert("Error", "Failed to update delivered status"); Alert.alert("Error", "Failed to update delivered status");
}, }
} }}
); disabled={updateDeliveredMutation.isPending}
}} >
onOpenAdminNotes={() => { <View
setShowOrderMenu(false); style={tw`w-10 h-10 rounded-full bg-green-50 items-center justify-center mr-4`}
setNotesDialogOpen(true); >
}} <MaterialIcons name="local-shipping" size={20} color="#16a34a" />
onCancelOrder={() => { </View>
setShowOrderMenu(false); <View>
setCancelDialogOpen(true); <MyText style={tw`font-semibold text-gray-800 text-base`}>
}} {selectedOrder?.isDelivered
onAttachLocation={() => {}} ? "Unmark Delivered"
onWhatsApp={() => {}} : "Mark Delivered"}
onDial={() => {}} </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>
<CancelOrderDialog <TouchableOpacity
orderId={selectedOrder?.id || 0} style={tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm ${
open={cancelDialogOpen} updateAddressCoordsMutation.isPending ? "opacity-50" : ""
onClose={() => setCancelDialogOpen(false)} }`}
onSuccess={() => { onPress={async () => {
refetchOrders(); if (!selectedOrder) return;
refetchSequence(); 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>
<BottomDialog open={notesDialogOpen} onClose={() => setNotesDialogOpen(false)}> <TouchableOpacity
<OrderNotesForm 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={() => {
initialNotes={selectedOrder?.adminNotes || ''} const phoneMatch = selectedOrder?.address.match(/Phone: (\d+)/);
onSuccess={() => { const phone = phoneMatch ? phoneMatch[1] : null;
refetchOrders(); if (phone) {
refetchSequence(); Linking.openURL(`whatsapp://send?phone=+91${phone}`);
setNotesDialogOpen(false); } else {
}} Alert.alert("No phone number found");
onCancel={() => setNotesDialogOpen(false)} }
/> 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> </BottomDialog>
</View> </View>
); );

View file

@ -1,5 +1,5 @@
import React, { useState , useEffect } from 'react'; import React, { useState , useEffect } from 'react';
import { View, TouchableOpacity, Alert, TextInput, ActivityIndicator, Linking } from 'react-native'; import { View, TouchableOpacity, Alert, TextInput, ActivityIndicator } 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,8 +7,6 @@ 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 || '');
@ -113,6 +111,7 @@ const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }
{ orderId: order.orderId.toString(), isPackaged }, { orderId: order.orderId.toString(), isPackaged },
{ {
onSuccess: () => { onSuccess: () => {
setMenuOpen(false);
refetch(); refetch();
}, },
} }
@ -124,6 +123,7 @@ 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,40 +344,81 @@ const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }
</View> </View>
</TouchableOpacity> </TouchableOpacity>
<OrderOptionsMenu <BottomDialog open={menuOpen} onClose={() => setMenuOpen(false)}>
open={menuOpen} <View style={tw`p-6`}>
onClose={() => setMenuOpen(false)} <MyText style={tw`text-lg font-bold text-gray-800 mb-4`}>
order={{ Order Options
id: order.id, </MyText>
readableId: order.readableId, {order.isFlashDelivery && (
isPackaged: order.isPackaged, <View style={tw`bg-amber-50 border border-amber-200 rounded-lg p-3 mb-4 flex-row items-center`}>
isDelivered: order.isDelivered, <MaterialIcons name="bolt" size={20} color="#D97706" />
isFlashDelivery: order.isFlashDelivery, <View style={tw`ml-3 flex-1`}>
address: order.address, <MyText style={tw`text-sm font-bold text-amber-900`}>Flash Delivery Order</MyText>
addressId: 0, <MyText style={tw`text-xs text-amber-700`}>
adminNotes: order.adminNotes, Deliver within 30 minutes High Priority
userNotes: order.userNotes, </MyText>
latitude: null, </View>
longitude: null, </View>
status: order.status, )}
}} <TouchableOpacity
onViewDetails={handleMenuOption} style={tw`flex-row items-center p-4 bg-gray-50 rounded-lg mb-3`}
onTogglePackaged={() => handleMarkPackaged(!order.isPackaged)} onPress={() => handleMarkPackaged(!order.isPackaged)}
onToggleDelivered={() => handleMarkDelivered(!order.isDelivered)} >
onOpenAdminNotes={() => { <Entypo name="box" size={20} color="#6B7280" />
setMenuOpen(false); <MyText style={tw`text-gray-800 font-medium ml-3`}>
setNotesDialogOpen(true); {order.isPackaged ? 'Mark Not Packaged' : 'Mark Packaged'}
}} </MyText>
onCancelOrder={() => { </TouchableOpacity>
setMenuOpen(false); {order.isPackaged && (
setCancelDialogOpen(true); <TouchableOpacity
}} style={tw`flex-row items-center p-4 bg-gray-50 rounded-lg mb-3`}
onAttachLocation={() => {}} onPress={() => handleMarkDelivered(!order.isDelivered)}
onWhatsApp={() => {}} >
onDial={() => {}} <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);
setNotesDialogOpen(true);
}}
>
<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);
setCancelDialogOpen(true);
}}
>
<MaterialIcons name="cancel" size={20} color="#DC2626" />
<MyText style={tw`text-red-700 font-medium ml-3`}>
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`}>
<View style={tw`flex-row items-center justify-between mb-4`}> <View style={tw`flex-row items-center justify-between mb-4`}>
<MyText style={tw`text-lg font-bold text-gray-800`}> <MyText style={tw`text-lg font-bold text-gray-800`}>
@ -782,4 +823,4 @@ export default function Orders() {
</BottomDialog> </BottomDialog>
</> </>
); );
} }

View file

@ -1,306 +0,0 @@
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>
);
}

View file

@ -1,2 +0,0 @@
ALTER TABLE "mf"."addresses" ADD COLUMN "admin_latitude" real;--> statement-breakpoint
ALTER TABLE "mf"."addresses" ADD COLUMN "admin_longitude" real;

View file

@ -1 +0,0 @@
ALTER TABLE "mf"."addresses" ADD COLUMN "google_maps_url" varchar(500);

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -477,20 +477,6 @@
"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
},
{
"idx": 69,
"version": "7",
"when": 1769718702463,
"tag": "0069_violet_smiling_tiger",
"breakpoints": true
} }
] ]
} }

View file

@ -48,9 +48,6 @@ 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'),
googleMapsUrl: varchar('google_maps_url', { length: 500 }),
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(),
}); });
@ -645,4 +642,4 @@ export const staffPermissionsRelations = relations(staffPermissions, ({ many })
export const staffRolePermissionsRelations = relations(staffRolePermissions, ({ one }) => ({ export const staffRolePermissionsRelations = relations(staffRolePermissions, ({ one }) => ({
role: one(staffRoles, { fields: [staffRolePermissions.staffRoleId], references: [staffRoles.id] }), role: one(staffRoles, { fields: [staffRolePermissions.staffRoleId], references: [staffRoles.id] }),
permission: one(staffPermissions, { fields: [staffRolePermissions.staffPermissionId], references: [staffPermissions.id] }), permission: one(staffPermissions, { fields: [staffRolePermissions.staffPermissionId], references: [staffPermissions.id] }),
})); }));

View file

@ -1,20 +0,0 @@
import axios from 'axios';
export async function extractCoordsFromRedirectUrl(url: string): Promise<{ latitude: string; longitude: string } | null> {
try {
await axios.get(url, { maxRedirects: 0 });
return null;
} catch (error: any) {
if (error.response?.status === 302 || error.response?.status === 301) {
const redirectUrl = error.response.headers.location;
const coordsMatch = redirectUrl.match(/!3d([-\d.]+)!4d([-\d.]+)/);
if (coordsMatch) {
return {
latitude: coordsMatch[1],
longitude: coordsMatch[2],
};
}
}
return null;
}
}

View file

@ -519,9 +519,9 @@ export const orderRouter = router({
}, ${order.address.city}, ${order.address.state} - ${ }, ${order.address.city}, ${order.address.state} - ${
order.address.pincode order.address.pincode
}, Phone: ${order.address.phone}`, }, Phone: ${order.address.phone}`,
addressId: order.addressId, addressId: order.addressId,
latitude: order.address.adminLatitude ?? order.address.latitude, latitude: order.address.latitude,
longitude: order.address.adminLongitude ?? order.address.longitude, longitude: 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({
adminLatitude: latitude, latitude,
adminLongitude: longitude, longitude,
}) })
.where(eq(addresses.id, addressId)) .where(eq(addresses.id, addressId))
.returning(); .returning();
@ -793,9 +793,9 @@ export const orderRouter = router({
}, ${order.address.city}, ${order.address.state} - ${ }, ${order.address.city}, ${order.address.state} - ${
order.address.pincode order.address.pincode
}, Phone: ${order.address.phone}`, }, Phone: ${order.address.phone}`,
addressId: order.addressId, addressId: order.addressId,
latitude: order.address.adminLatitude ?? order.address.latitude, latitude: order.address.latitude,
longitude: order.address.adminLongitude ?? order.address.longitude, longitude: order.address.longitude,
totalAmount: parseFloat(order.totalAmount), totalAmount: parseFloat(order.totalAmount),
deliveryCharge: parseFloat(order.deliveryCharge || "0"), deliveryCharge: parseFloat(order.deliveryCharge || "0"),
items, items,

View file

@ -1,7 +1,7 @@
import { protectedProcedure } from '../trpc-index'; import { protectedProcedure } from '../trpc-index';
import { z } from 'zod'; import { z } from 'zod';
import { db } from '../../db/db_index'; import { db } from '../../db/db_index';
import { users, complaints } from '../../db/schema'; import { users } from '../../db/schema';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { ApiError } from '../../lib/api-error'; import { ApiError } from '../../lib/api-error';
@ -51,16 +51,4 @@ export const userRouter = {
data: newUser, data: newUser,
}; };
}), }),
getEssentials: protectedProcedure
.query(async () => {
const [result] = await db
.select({ count: db.$count(complaints) })
.from(complaints)
.where(eq(complaints.isResolved, false));
return {
unresolvedComplaints: result.count || 0,
};
}),
}; };

View file

@ -4,7 +4,6 @@ import { db } from '../../db/db_index';
import { addresses, orders, orderStatus, deliverySlotInfo } from '../../db/schema'; import { addresses, orders, orderStatus, deliverySlotInfo } from '../../db/schema';
import { eq, and, gte } from 'drizzle-orm'; import { eq, and, gte } from 'drizzle-orm';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { extractCoordsFromRedirectUrl } from '../../lib/license-util';
export const addressRouter = router({ export const addressRouter = router({
getDefaultAddress: protectedProcedure getDefaultAddress: protectedProcedure
@ -37,23 +36,10 @@ export const addressRouter = router({
state: z.string().min(1, 'State is required'), state: z.string().min(1, 'State is required'),
pincode: z.string().min(1, 'Pincode is required'), pincode: z.string().min(1, 'Pincode is required'),
isDefault: z.boolean().optional(), isDefault: z.boolean().optional(),
latitude: z.number().optional(),
longitude: z.number().optional(),
googleMapsUrl: z.string().optional(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsUrl } = input; const { name, phone, addressLine1, addressLine2, city, state, pincode, isDefault } = input;
let { latitude, longitude } = input;
if (googleMapsUrl && latitude === undefined && longitude === undefined) {
const coords = await extractCoordsFromRedirectUrl(googleMapsUrl);
if (coords) {
latitude = Number(coords.latitude);
longitude = Number(coords.longitude);
}
}
// Validate required fields // Validate required fields
if (!name || !phone || !addressLine1 || !city || !state || !pincode) { if (!name || !phone || !addressLine1 || !city || !state || !pincode) {
@ -75,9 +61,6 @@ export const addressRouter = router({
state, state,
pincode, pincode,
isDefault: isDefault || false, isDefault: isDefault || false,
latitude,
longitude,
googleMapsUrl,
}).returning(); }).returning();
return { success: true, data: newAddress }; return { success: true, data: newAddress };
@ -94,23 +77,10 @@ export const addressRouter = router({
state: z.string().min(1, 'State is required'), state: z.string().min(1, 'State is required'),
pincode: z.string().min(1, 'Pincode is required'), pincode: z.string().min(1, 'Pincode is required'),
isDefault: z.boolean().optional(), isDefault: z.boolean().optional(),
latitude: z.number().optional(),
longitude: z.number().optional(),
googleMapsUrl: z.string().optional(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const userId = ctx.user.userId; const userId = ctx.user.userId;
const { id, name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsUrl } = input; const { id, name, phone, addressLine1, addressLine2, city, state, pincode, isDefault } = input;
let { latitude, longitude } = input;
if (googleMapsUrl && latitude === undefined && longitude === undefined) {
const coords = await extractCoordsFromRedirectUrl(googleMapsUrl);
if (coords) {
latitude = Number(coords.latitude);
longitude = Number(coords.longitude);
}
}
// Check if address exists and belongs to user // Check if address exists and belongs to user
const existingAddress = await db.select().from(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).limit(1); const existingAddress = await db.select().from(addresses).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).limit(1);
@ -123,7 +93,7 @@ export const addressRouter = router({
await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId)); await db.update(addresses).set({ isDefault: false }).where(eq(addresses.userId, userId));
} }
const updateData: any = { const [updatedAddress] = await db.update(addresses).set({
name, name,
phone, phone,
addressLine1, addressLine1,
@ -132,17 +102,7 @@ export const addressRouter = router({
state, state,
pincode, pincode,
isDefault: isDefault || false, isDefault: isDefault || false,
googleMapsUrl, }).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).returning();
};
if (latitude !== undefined) {
updateData.latitude = latitude;
}
if (longitude !== undefined) {
updateData.longitude = longitude;
}
const [updatedAddress] = await db.update(addresses).set(updateData).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).returning();
return { success: true, data: updatedAddress }; return { success: true, data: updatedAddress };
}), }),

View file

@ -1,5 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { View, Dimensions, Image, Alert, ScrollView, StatusBar as RNStatusBar, RefreshControl } from "react-native"; import { View, Dimensions, Image, Alert, ScrollView, StatusBar as RNStatusBar } from "react-native";
import { StatusBar as ExpoStatusBar } from 'expo-status-bar'; import { StatusBar as ExpoStatusBar } from 'expo-status-bar';
import { LinearGradient } from "expo-linear-gradient"; import { LinearGradient } from "expo-linear-gradient";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
@ -119,7 +119,6 @@ export default function Dashboard() {
const [isLoadingMore, setIsLoadingMore] = useState(false); const [isLoadingMore, setIsLoadingMore] = useState(false);
const { backgroundColor } = useStatusBarStore(); const { backgroundColor } = useStatusBarStore();
const { getQuickestSlot } = useProductSlotIdentifier(); const { getQuickestSlot } = useProductSlotIdentifier();
const [isRefreshing, setIsRefreshing] = useState(false);
const { const {
data: productsData, data: productsData,
@ -131,11 +130,11 @@ export default function Dashboard() {
tagId: selectedTagId || undefined, tagId: selectedTagId || undefined,
}); });
const { data: essentialConsts, isLoading: isLoadingConsts, error: constsError, refetch: refetchConsts } = useGetEssentialConsts(); const { data: essentialConsts, isLoading: isLoadingConsts, error: constsError } = useGetEssentialConsts();
const { data: storesData, refetch: refetchStores } = trpc.user.stores.getStores.useQuery(); const { data: storesData } = trpc.user.stores.getStores.useQuery();
const { data: slotsData, refetch: refetchSlots } = trpc.user.slots.getSlotsWithProducts.useQuery(); const { data: slotsData } = trpc.user.slots.getSlotsWithProducts.useQuery();
const products = productsData?.products || []; const products = productsData?.products || [];
@ -203,26 +202,12 @@ export default function Dashboard() {
.filter((product): product is NonNullable<typeof product> => product != null); .filter((product): product is NonNullable<typeof product> => product != null);
const handleRefresh = async () => {
setIsRefreshing(true);
try {
await Promise.all([
refetch(),
refetchStores(),
refetchSlots(),
refetchConsts(),
]);
} finally {
setIsRefreshing(false);
}
};
useManualRefresh(() => { useManualRefresh(() => {
handleRefresh(); refetch();
}); });
useMarkDataFetchers(() => { useMarkDataFetchers(() => {
handleRefresh(); refetch();
}); });
const handleScroll = (event: any) => { const handleScroll = (event: any) => {
@ -258,14 +243,6 @@ export default function Dashboard() {
<ScrollView <ScrollView
style={[tw`flex-1 bg-white`, { position: 'relative' }]} style={[tw`flex-1 bg-white`, { position: 'relative' }]}
stickyHeaderIndices={[2]} stickyHeaderIndices={[2]}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={handleRefresh}
tintColor="#3b82f6"
colors={["#3b82f6"]}
/>
}
onScroll={(e) => { onScroll={(e) => {
handleScroll(e); handleScroll(e);

View file

@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { View, Alert } from 'react-native'; import { View, Alert, Modal } from 'react-native';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { AppContainer, MyText, tw, useMarkDataFetchers, MyFlatList, MyTouchableOpacity, BottomDialog } from 'common-ui'; import { AppContainer, MyText, tw, useMarkDataFetchers, MyFlatList, MyTouchableOpacity } from 'common-ui';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import AddressForm from '@/src/components/AddressForm'; import AddressForm from '@/src/components/AddressForm';
@ -16,9 +16,6 @@ interface Address {
state: string; state: string;
pincode: string; pincode: string;
isDefault: boolean; isDefault: boolean;
latitude?: number | null;
longitude?: number | null;
googleMapsUrl?: string | null;
} }
function AddressCard({ address, onEdit, onDelete, onSetDefault, isDeleting }: { function AddressCard({ address, onEdit, onDelete, onSetDefault, isDeleting }: {
@ -242,9 +239,14 @@ export default function Addresses() {
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
/> />
<BottomDialog open={modalVisible} onClose={() => setModalVisible(false)}> <Modal
<View style={tw`pt-4`}> visible={modalVisible}
<View style={tw`flex-row justify-between items-center pb-4 border-b border-gray-100`}> animationType="slide"
presentationStyle="pageSheet"
onRequestClose={() => setModalVisible(false)}
>
<AppContainer>
<View style={tw`flex-row justify-between items-center pb-2`}>
<MyText weight="semibold" style={tw`text-lg text-gray-800`}> <MyText weight="semibold" style={tw`text-lg text-gray-800`}>
{editingAddress ? 'Edit Address' : 'Add Address'} {editingAddress ? 'Edit Address' : 'Add Address'}
</MyText> </MyText>
@ -255,28 +257,22 @@ export default function Addresses() {
<MaterialIcons name="close" size={24} color="#6B7280" /> <MaterialIcons name="close" size={24} color="#6B7280" />
</MyTouchableOpacity> </MyTouchableOpacity>
</View> </View>
<View style={tw`pt-4`}> <AddressForm
<AddressForm onSuccess={handleAddressSubmit}
onSuccess={handleAddressSubmit} initialValues={editingAddress ? {
initialValues={editingAddress ? { name: editingAddress.name,
id: editingAddress.id, phone: editingAddress.phone,
name: editingAddress.name, addressLine1: editingAddress.addressLine1,
phone: editingAddress.phone, addressLine2: editingAddress.addressLine2 || '',
addressLine1: editingAddress.addressLine1, city: editingAddress.city,
addressLine2: editingAddress.addressLine2 || '', state: editingAddress.state,
city: editingAddress.city, pincode: editingAddress.pincode,
state: editingAddress.state, isDefault: editingAddress.isDefault,
pincode: editingAddress.pincode, } : undefined}
isDefault: editingAddress.isDefault, isEdit={!!editingAddress}
latitude: editingAddress.googleMapsUrl ? undefined : (editingAddress.latitude ?? undefined), />
longitude: editingAddress.googleMapsUrl ? undefined : (editingAddress.longitude ?? undefined), </AppContainer>
googleMapsUrl: editingAddress.googleMapsUrl ?? undefined, </Modal>
} : undefined}
isEdit={!!editingAddress}
/>
</View>
</View>
</BottomDialog>
</> </>
); );
} }

View file

@ -24,7 +24,6 @@ import FirstUserWrapper from "@/components/FirstUserWrapper";
import UpdateChecker from "@/components/UpdateChecker"; import UpdateChecker from "@/components/UpdateChecker";
import { RefreshProvider } from "../../../packages/ui/src/lib/refresh-context"; import { RefreshProvider } from "../../../packages/ui/src/lib/refresh-context";
import WebViewWrapper from "@/components/WebViewWrapper"; import WebViewWrapper from "@/components/WebViewWrapper";
import BackHandlerWrapper from "@/components/BackHandler";
import React from "react"; import React from "react";
export default function RootLayout() { export default function RootLayout() {
@ -48,30 +47,29 @@ export default function RootLayout() {
<SafeAreaView edges={['bottom', 'left', 'right']} style={{ flex: 1, paddingTop: 0 }}> <SafeAreaView edges={['bottom', 'left', 'right']} style={{ flex: 1, paddingTop: 0 }}>
<View style={{ flex: 1, backgroundColor: theme.colors.gray1, }}> <View style={{ flex: 1, backgroundColor: theme.colors.gray1, }}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<trpc.Provider client={trpcClient} queryClient={queryClient}> <trpc.Provider client={trpcClient} queryClient={queryClient}>
<UpdateChecker> <UpdateChecker>
<HealthTestWrapper> <HealthTestWrapper>
<WebViewWrapper> <WebViewWrapper>
<FirstUserWrapper> <FirstUserWrapper>
<AuthProvider> <AuthProvider>
<NotificationProvider> <NotificationProvider>
<PaperProvider> <PaperProvider>
<LocationTestWrapper> <LocationTestWrapper>
<RefreshProvider queryClient={queryClient}> <RefreshProvider queryClient={queryClient}>
<BackHandlerWrapper /> <Stack screenOptions={{ headerShown: false }} />
<Stack screenOptions={{ headerShown: false }} /> </RefreshProvider>
</RefreshProvider> </LocationTestWrapper>
</LocationTestWrapper> </PaperProvider>
</PaperProvider> </NotificationProvider>
</NotificationProvider> </AuthProvider>
</AuthProvider> </FirstUserWrapper>
</FirstUserWrapper> </WebViewWrapper>
</WebViewWrapper> </HealthTestWrapper>
</HealthTestWrapper> </UpdateChecker>
</UpdateChecker> </trpc.Provider>
</trpc.Provider> </QueryClientProvider>
</QueryClientProvider>
</View> </View>
</SafeAreaView> </SafeAreaView>
</SafeAreaProvider> </SafeAreaProvider>

View file

@ -1,32 +0,0 @@
import { useEffect } from 'react';
import { BackHandler, Alert } from 'react-native';
import { useRouter, usePathname } from 'expo-router';
export default function BackHandlerWrapper() {
const router = useRouter();
const pathname = usePathname();
const isHomeScreen =
!router.canGoBack() &&
(pathname.includes('home') || pathname === '/');
useEffect(() => {
const onBackPress = () => {
if (isHomeScreen) {
Alert.alert('Exit App', 'Are you sure you want to exit?', [
{ text: 'Cancel', style: 'cancel' },
{ text: 'Exit', onPress: () => BackHandler.exitApp() },
]);
return true;
}
return false;
};
const subscription = BackHandler.addEventListener('hardwareBackPress', onBackPress);
return () => {
subscription.remove();
};
}, [isHomeScreen]);
return null;
}

View file

@ -55,15 +55,6 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
const { getQuickestSlot } = useProductSlotIdentifier(); const { getQuickestSlot } = useProductSlotIdentifier();
const { setShouldNavigateToCart } = useFlashNavigationStore(); const { setShouldNavigateToCart } = useFlashNavigationStore();
const sortedDeliverySlots = useMemo(() => {
if (!productDetail?.deliverySlots) return []
return [...productDetail.deliverySlots].sort((a, b) => {
const deliveryDiff = new Date(a.deliveryTime).getTime() - new Date(b.deliveryTime).getTime()
if (deliveryDiff !== 0) return deliveryDiff
return new Date(a.freezeTime).getTime() - new Date(b.freezeTime).getTime()
})
}, [productDetail?.deliverySlots])
// Find current quantity from cart data // Find current quantity from cart data
const cartItem = productDetail ? cartData?.data?.items?.find((item: any) => item.productId === productDetail.id) : null; const cartItem = productDetail ? cartData?.data?.items?.find((item: any) => item.productId === productDetail.id) : null;
const quantity = cartItem?.quantity || 0; const quantity = cartItem?.quantity || 0;
@ -350,11 +341,11 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
<MyText style={tw`text-lg font-bold text-gray-900`}>Available Slots</MyText> <MyText style={tw`text-lg font-bold text-gray-900`}>Available Slots</MyText>
</View> </View>
{sortedDeliverySlots.length === 0 ? ( {productDetail.deliverySlots.length === 0 ? (
<MyText style={tw`text-gray-400 italic`}>No delivery slots available currently</MyText> <MyText style={tw`text-gray-400 italic`}>No delivery slots available currently</MyText>
) : ( ) : (
<> <>
{sortedDeliverySlots.slice(0, 2).map((slot, index) => ( {productDetail.deliverySlots.slice(0, 2).map((slot, index) => (
<MyTouchableOpacity <MyTouchableOpacity
key={index} key={index}
style={tw`flex-row items-start mb-4 bg-gray-50 p-3 rounded-xl border border-gray-100`} style={tw`flex-row items-start mb-4 bg-gray-50 p-3 rounded-xl border border-gray-100`}
@ -374,12 +365,12 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
<MaterialIcons name="add" size={20} color="#3B82F6" style={tw`mt-0.5`} /> <MaterialIcons name="add" size={20} color="#3B82F6" style={tw`mt-0.5`} />
</MyTouchableOpacity> </MyTouchableOpacity>
))} ))}
{sortedDeliverySlots.length > 2 && ( {productDetail.deliverySlots.length > 2 && (
<MyTouchableOpacity <MyTouchableOpacity
onPress={() => setShowAllSlots(true)} onPress={() => setShowAllSlots(true)}
style={tw`items-center py-2`} style={tw`items-center py-2`}
> >
<MyText style={tw`text-brand500 font-bold text-sm`}>View All {sortedDeliverySlots.length} Slots</MyText> <MyText style={tw`text-brand500 font-bold text-sm`}>View All {productDetail.deliverySlots.length} Slots</MyText>
</MyTouchableOpacity> </MyTouchableOpacity>
)} )}
</> </>
@ -566,7 +557,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
</View> </View>
<ScrollView showsVerticalScrollIndicator={false}> <ScrollView showsVerticalScrollIndicator={false}>
{sortedDeliverySlots.map((slot, index) => ( {productDetail.deliverySlots.map((slot, index) => (
<MyTouchableOpacity <MyTouchableOpacity
key={index} key={index}
style={tw`flex-row items-start mb-4 bg-gray-50 p-4 rounded-xl border border-gray-100`} style={tw`flex-row items-start mb-4 bg-gray-50 p-4 rounded-xl border border-gray-100`}

View file

@ -1,8 +1,7 @@
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
export const useGetEssentialConsts = () => { export const useGetEssentialConsts = () => {
const query = trpc.common.essentialConsts.useQuery(undefined, { return trpc.common.essentialConsts.useQuery(undefined, {
refetchInterval: 60000, refetchInterval: 60000,
}); });
return { ...query, refetch: query.refetch };
}; };

View file

@ -10,7 +10,6 @@ import { trpc } from '../trpc-client';
interface AddressFormProps { interface AddressFormProps {
onSuccess: () => void; onSuccess: () => void;
initialValues?: { initialValues?: {
id?: number;
name: string; name: string;
phone: string; phone: string;
addressLine1: string; addressLine1: string;
@ -19,9 +18,6 @@ interface AddressFormProps {
state: string; state: string;
pincode: string; pincode: string;
isDefault: boolean; isDefault: boolean;
latitude?: number;
longitude?: number;
googleMapsUrl?: string;
}; };
isEdit?: boolean; isEdit?: boolean;
} }
@ -30,17 +26,11 @@ const AddressForm: React.FC<AddressFormProps> = ({ onSuccess, initialValues, isE
const [locationLoading, setLocationLoading] = useState(false); const [locationLoading, setLocationLoading] = useState(false);
const [locationError, setLocationError] = useState<string | null>(null); const [locationError, setLocationError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [showGoogleMapsField, setShowGoogleMapsField] = useState(!!initialValues?.googleMapsUrl);
const [currentLocation, setCurrentLocation] = useState<{ latitude: number; longitude: number } | null>(
initialValues?.latitude && initialValues?.longitude
? { latitude: initialValues.latitude, longitude: initialValues.longitude }
: null
);
const createAddressMutation = trpc.user.address.createAddress.useMutation({ const createAddressMutation = trpc.user.address.createAddress.useMutation({
onSuccess: () => { onSuccess: () => {
setIsSubmitting(false); setIsSubmitting(false);
setTimeout(() => onSuccess(), 100); onSuccess();
}, },
onError: (error: any) => { onError: (error: any) => {
setIsSubmitting(false); setIsSubmitting(false);
@ -48,22 +38,12 @@ const AddressForm: React.FC<AddressFormProps> = ({ onSuccess, initialValues, isE
}, },
}); });
const updateAddressMutation = trpc.user.address.updateAddress.useMutation({ const attachCurrentLocation = async (setFieldValue: (field: string, value: any) => void) => {
onSuccess: () => {
setIsSubmitting(false);
setTimeout(() => onSuccess(), 100);
},
onError: (error: any) => {
setIsSubmitting(false);
Alert.alert('Error', error.message || 'Failed to update address');
},
});
const attachCurrentLocation = async () => {
setLocationLoading(true); setLocationLoading(true);
setLocationError(null); setLocationError(null);
try { try {
// Request location permission
const { status } = await Location.requestForegroundPermissionsAsync(); const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') { if (status !== 'granted') {
@ -71,13 +51,28 @@ const AddressForm: React.FC<AddressFormProps> = ({ onSuccess, initialValues, isE
return; return;
} }
// Get current position
const location = await Location.getCurrentPositionAsync({ const location = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.High, accuracy: Location.Accuracy.High,
}); });
const { latitude, longitude } = location.coords; // Reverse geocode to get address
setCurrentLocation({ latitude, longitude }); const address = await Location.reverseGeocodeAsync({
Alert.alert('Success', 'Location attached successfully'); latitude: location.coords.latitude,
longitude: location.coords.longitude,
});
// Populate form fields with geocoded data
if (address[0]) {
const addr = address[0];
const addressLine1 = `${addr.streetNumber || ''} ${addr.street || ''}`.trim();
setFieldValue('addressLine1', addressLine1 || addr.name || '');
setFieldValue('city', addr.city || addr.subregion || '');
setFieldValue('state', addr.region || '');
setFieldValue('pincode', addr.postalCode || '');
} else {
setLocationError('Unable to determine address from your location');
}
} catch (error) { } catch (error) {
console.error('Location error:', error); console.error('Location error:', error);
setLocationError('Unable to fetch location. Please check your GPS settings.'); setLocationError('Unable to fetch location. Please check your GPS settings.');
@ -110,24 +105,11 @@ const AddressForm: React.FC<AddressFormProps> = ({ onSuccess, initialValues, isE
state: 'Telangana', state: 'Telangana',
pincode: '509001', pincode: '509001',
isDefault: false, isDefault: false,
latitude: undefined,
longitude: undefined,
googleMapsUrl: '',
}} }}
validationSchema={validationSchema} validationSchema={validationSchema}
onSubmit={(values) => { onSubmit={(values) => {
setIsSubmitting(true); setIsSubmitting(true);
const payload = { createAddressMutation.mutate(values);
...values,
latitude: currentLocation?.latitude,
longitude: currentLocation?.longitude,
googleMapsUrl: values.googleMapsUrl || undefined,
};
if (isEdit && initialValues?.id) {
updateAddressMutation.mutate({ id: initialValues.id, ...payload });
} else {
createAddressMutation.mutate(payload);
}
}} }}
> >
{({ handleChange, handleBlur, handleSubmit, values, errors, touched, setFieldValue }) => ( {({ handleChange, handleBlur, handleSubmit, values, errors, touched, setFieldValue }) => (
@ -199,61 +181,6 @@ const AddressForm: React.FC<AddressFormProps> = ({ onSuccess, initialValues, isE
/> />
{touched.pincode && errors.pincode && <MyText style={tw`text-red-500 mb-2`}>{errors.pincode}</MyText>} {touched.pincode && errors.pincode && <MyText style={tw`text-red-500 mb-2`}>{errors.pincode}</MyText>}
{locationLoading ? (
<MyText style={tw`text-blue-500 text-sm mb-2`}>Fetching location...</MyText>
) : locationError ? (
<MyText style={tw`text-red-500 text-sm mb-2`}>{locationError}</MyText>
) : currentLocation ? (
<View style={tw`flex-row items-center mb-4`}>
<MyText style={tw`text-green-600 text-sm font-medium`}>Location Attached</MyText>
<MyTouchableOpacity
onPress={() => attachCurrentLocation()}
disabled={locationLoading}
style={tw`ml-4`}
>
<MyText style={tw`text-blue-500 text-sm font-medium`}>Attach Current</MyText>
</MyTouchableOpacity>
</View>
) : (
<MyTouchableOpacity
onPress={() => attachCurrentLocation()}
disabled={locationLoading}
style={tw`mb-4`}
>
<MyText style={tw`text-blue-500 text-sm font-medium`}>
Attach Current Location
</MyText>
</MyTouchableOpacity>
)}
<MyTouchableOpacity
onPress={() => setShowGoogleMapsField(true)}
disabled={false}
style={tw`mb-1`}
>
<MyText style={tw`text-blue-500 text-sm font-medium`}>
Attach with Google Maps
</MyText>
</MyTouchableOpacity>
{showGoogleMapsField && (
<View style={tw`mb-2`}>
<MyText style={tw`text-gray-500 text-xs mb-2`}>
1. Open Google Maps and Find location{'\n'}
2. Long press the desired location{'\n'}
3. Click on Share and Click on Copy{'\n'}
4. Paste the copied url here in the field.
</MyText>
<MyTextInput
placeholder="Google Maps Shared URL"
shrunkPadding={true}
onChangeText={handleChange('googleMapsUrl')}
onBlur={handleBlur('googleMapsUrl')}
value={values.googleMapsUrl}
/>
</View>
)}
<View style={tw`flex-row items-center mb-4`}> <View style={tw`flex-row items-center mb-4`}>
<Checkbox <Checkbox
checked={values.isDefault} checked={values.isDefault}