enh
This commit is contained in:
parent
d658022a51
commit
28c7207016
11 changed files with 309 additions and 89 deletions
|
|
@ -5,6 +5,7 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
|||
import { MyText, tw } from 'common-ui';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { theme } from 'common-ui/src/theme';
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
|
||||
interface MenuItem {
|
||||
title: string;
|
||||
|
|
@ -14,6 +15,7 @@ interface MenuItem {
|
|||
category: 'quick' | 'products' | 'orders' | 'marketing' | 'settings';
|
||||
iconColor?: string;
|
||||
iconBg?: string;
|
||||
badgeCount?: number;
|
||||
}
|
||||
|
||||
interface MenuItemComponentProps {
|
||||
|
|
@ -21,7 +23,9 @@ interface MenuItemComponentProps {
|
|||
router: any;
|
||||
}
|
||||
|
||||
const MenuItemComponent: React.FC<MenuItemComponentProps> = ({ item, router }) => (
|
||||
const MenuItemComponent: React.FC<MenuItemComponentProps> = ({ item, router }) => {
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
key={item.route}
|
||||
onPress={() => router.push(item.route as any)}
|
||||
|
|
@ -39,13 +43,23 @@ const MenuItemComponent: React.FC<MenuItemComponentProps> = ({ item, router }) =
|
|||
<MyText style={tw`text-gray-500 text-xs`}>{item.description}</MyText>
|
||||
)}
|
||||
</View>
|
||||
<MaterialIcons name="chevron-right" size={24} color="#D1D5DB" />
|
||||
{item.badgeCount ? (
|
||||
<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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const router = useRouter();
|
||||
|
||||
const { data: essentialsData } = trpc.admin.user.getEssentials.useQuery();
|
||||
|
||||
console.log({essentialsData})
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{
|
||||
title: 'Manage Orders',
|
||||
|
|
@ -91,6 +105,7 @@ export default function Dashboard() {
|
|||
category: 'quick',
|
||||
iconColor: '#F59E0B',
|
||||
iconBg: '#FEF3C7',
|
||||
badgeCount: essentialsData?.unresolvedComplaints,
|
||||
},
|
||||
{
|
||||
title: 'Products',
|
||||
|
|
@ -207,6 +222,11 @@ export default function Dashboard() {
|
|||
>
|
||||
<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} />
|
||||
{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>
|
||||
<MyText style={tw`text-gray-900 font-bold text-xs text-center`} numberOfLines={2}>
|
||||
{item.title}
|
||||
|
|
|
|||
20
apps/backend/src/lib/license-util.ts
Normal file
20
apps/backend/src/lib/license-util.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { protectedProcedure } from '../trpc-index';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../db/db_index';
|
||||
import { users } from '../../db/schema';
|
||||
import { users, complaints } from '../../db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { ApiError } from '../../lib/api-error';
|
||||
|
||||
|
|
@ -51,4 +51,16 @@ export const userRouter = {
|
|||
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,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
|
@ -4,6 +4,7 @@ import { db } from '../../db/db_index';
|
|||
import { addresses, orders, orderStatus, deliverySlotInfo } from '../../db/schema';
|
||||
import { eq, and, gte } from 'drizzle-orm';
|
||||
import dayjs from 'dayjs';
|
||||
import { extractCoordsFromRedirectUrl } from '../../lib/license-util';
|
||||
|
||||
export const addressRouter = router({
|
||||
getDefaultAddress: protectedProcedure
|
||||
|
|
@ -36,10 +37,23 @@ export const addressRouter = router({
|
|||
state: z.string().min(1, 'State is required'),
|
||||
pincode: z.string().min(1, 'Pincode is required'),
|
||||
isDefault: z.boolean().optional(),
|
||||
latitude: z.number().optional(),
|
||||
longitude: z.number().optional(),
|
||||
googleMapsLocation: z.string().optional(),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
const { name, phone, addressLine1, addressLine2, city, state, pincode, isDefault } = input;
|
||||
const { name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsLocation } = input;
|
||||
|
||||
let { latitude, longitude } = input;
|
||||
|
||||
if (googleMapsLocation && latitude === undefined && longitude === undefined) {
|
||||
const coords = await extractCoordsFromRedirectUrl(googleMapsLocation);
|
||||
if (coords) {
|
||||
latitude = Number(coords.latitude);
|
||||
longitude = Number(coords.longitude);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!name || !phone || !addressLine1 || !city || !state || !pincode) {
|
||||
|
|
@ -61,6 +75,8 @@ export const addressRouter = router({
|
|||
state,
|
||||
pincode,
|
||||
isDefault: isDefault || false,
|
||||
latitude,
|
||||
longitude,
|
||||
}).returning();
|
||||
|
||||
return { success: true, data: newAddress };
|
||||
|
|
@ -77,10 +93,23 @@ export const addressRouter = router({
|
|||
state: z.string().min(1, 'State is required'),
|
||||
pincode: z.string().min(1, 'Pincode is required'),
|
||||
isDefault: z.boolean().optional(),
|
||||
latitude: z.number().optional(),
|
||||
longitude: z.number().optional(),
|
||||
googleMapsLocation: z.string().optional(),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
const { id, name, phone, addressLine1, addressLine2, city, state, pincode, isDefault } = input;
|
||||
const { id, name, phone, addressLine1, addressLine2, city, state, pincode, isDefault, googleMapsLocation } = input;
|
||||
|
||||
let { latitude, longitude } = input;
|
||||
|
||||
if (googleMapsLocation && latitude === undefined && longitude === undefined) {
|
||||
const coords = await extractCoordsFromRedirectUrl(googleMapsLocation);
|
||||
if (coords) {
|
||||
latitude = Number(coords.latitude);
|
||||
longitude = Number(coords.longitude);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
|
@ -102,6 +131,8 @@ export const addressRouter = router({
|
|||
state,
|
||||
pincode,
|
||||
isDefault: isDefault || false,
|
||||
latitude,
|
||||
longitude,
|
||||
}).where(and(eq(addresses.id, id), eq(addresses.userId, userId))).returning();
|
||||
|
||||
return { success: true, data: updatedAddress };
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState } from "react";
|
||||
import { View, Dimensions, Image, Alert, ScrollView, StatusBar as RNStatusBar } from "react-native";
|
||||
import { View, Dimensions, Image, Alert, ScrollView, StatusBar as RNStatusBar, RefreshControl } from "react-native";
|
||||
import { StatusBar as ExpoStatusBar } from 'expo-status-bar';
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { useRouter } from "expo-router";
|
||||
|
|
@ -119,6 +119,7 @@ export default function Dashboard() {
|
|||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
const { backgroundColor } = useStatusBarStore();
|
||||
const { getQuickestSlot } = useProductSlotIdentifier();
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
const {
|
||||
data: productsData,
|
||||
|
|
@ -130,11 +131,11 @@ export default function Dashboard() {
|
|||
tagId: selectedTagId || undefined,
|
||||
});
|
||||
|
||||
const { data: essentialConsts, isLoading: isLoadingConsts, error: constsError } = useGetEssentialConsts();
|
||||
const { data: essentialConsts, isLoading: isLoadingConsts, error: constsError, refetch: refetchConsts } = useGetEssentialConsts();
|
||||
|
||||
|
||||
const { data: storesData } = trpc.user.stores.getStores.useQuery();
|
||||
const { data: slotsData } = trpc.user.slots.getSlotsWithProducts.useQuery();
|
||||
const { data: storesData, refetch: refetchStores } = trpc.user.stores.getStores.useQuery();
|
||||
const { data: slotsData, refetch: refetchSlots } = trpc.user.slots.getSlotsWithProducts.useQuery();
|
||||
|
||||
const products = productsData?.products || [];
|
||||
|
||||
|
|
@ -202,12 +203,26 @@ export default function Dashboard() {
|
|||
.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(() => {
|
||||
refetch();
|
||||
handleRefresh();
|
||||
});
|
||||
|
||||
useMarkDataFetchers(() => {
|
||||
refetch();
|
||||
handleRefresh();
|
||||
});
|
||||
|
||||
const handleScroll = (event: any) => {
|
||||
|
|
@ -243,6 +258,14 @@ export default function Dashboard() {
|
|||
<ScrollView
|
||||
style={[tw`flex-1 bg-white`, { position: 'relative' }]}
|
||||
stickyHeaderIndices={[2]}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isRefreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor="#3b82f6"
|
||||
colors={["#3b82f6"]}
|
||||
/>
|
||||
}
|
||||
onScroll={(e) => {
|
||||
handleScroll(e);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState } from 'react';
|
||||
import { View, Alert, Modal } from 'react-native';
|
||||
import { View, Alert } from 'react-native';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { AppContainer, MyText, tw, useMarkDataFetchers, MyFlatList, MyTouchableOpacity } from 'common-ui';
|
||||
import { AppContainer, MyText, tw, useMarkDataFetchers, MyFlatList, MyTouchableOpacity, BottomDialog } from 'common-ui';
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import AddressForm from '@/src/components/AddressForm';
|
||||
|
|
@ -239,14 +239,9 @@ export default function Addresses() {
|
|||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
animationType="slide"
|
||||
presentationStyle="pageSheet"
|
||||
onRequestClose={() => setModalVisible(false)}
|
||||
>
|
||||
<AppContainer>
|
||||
<View style={tw`flex-row justify-between items-center pb-2`}>
|
||||
<BottomDialog open={modalVisible} onClose={() => setModalVisible(false)}>
|
||||
<View style={tw`pt-4`}>
|
||||
<View style={tw`flex-row justify-between items-center pb-4 border-b border-gray-100`}>
|
||||
<MyText weight="semibold" style={tw`text-lg text-gray-800`}>
|
||||
{editingAddress ? 'Edit Address' : 'Add Address'}
|
||||
</MyText>
|
||||
|
|
@ -257,9 +252,11 @@ export default function Addresses() {
|
|||
<MaterialIcons name="close" size={24} color="#6B7280" />
|
||||
</MyTouchableOpacity>
|
||||
</View>
|
||||
<View style={tw`pt-4`}>
|
||||
<AddressForm
|
||||
onSuccess={handleAddressSubmit}
|
||||
initialValues={editingAddress ? {
|
||||
id: editingAddress.id,
|
||||
name: editingAddress.name,
|
||||
phone: editingAddress.phone,
|
||||
addressLine1: editingAddress.addressLine1,
|
||||
|
|
@ -271,8 +268,9 @@ export default function Addresses() {
|
|||
} : undefined}
|
||||
isEdit={!!editingAddress}
|
||||
/>
|
||||
</AppContainer>
|
||||
</Modal>
|
||||
</View>
|
||||
</View>
|
||||
</BottomDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ import FirstUserWrapper from "@/components/FirstUserWrapper";
|
|||
import UpdateChecker from "@/components/UpdateChecker";
|
||||
import { RefreshProvider } from "../../../packages/ui/src/lib/refresh-context";
|
||||
import WebViewWrapper from "@/components/WebViewWrapper";
|
||||
import BackHandlerWrapper from "@/components/BackHandler";
|
||||
import React from "react";
|
||||
|
||||
export default function RootLayout() {
|
||||
|
|
@ -58,6 +59,7 @@ export default function RootLayout() {
|
|||
<PaperProvider>
|
||||
<LocationTestWrapper>
|
||||
<RefreshProvider queryClient={queryClient}>
|
||||
<BackHandlerWrapper />
|
||||
<Stack screenOptions={{ headerShown: false }} />
|
||||
</RefreshProvider>
|
||||
</LocationTestWrapper>
|
||||
|
|
|
|||
32
apps/user-ui/components/BackHandler.tsx
Normal file
32
apps/user-ui/components/BackHandler.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -55,6 +55,15 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
|||
const { getQuickestSlot } = useProductSlotIdentifier();
|
||||
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
|
||||
const cartItem = productDetail ? cartData?.data?.items?.find((item: any) => item.productId === productDetail.id) : null;
|
||||
const quantity = cartItem?.quantity || 0;
|
||||
|
|
@ -341,11 +350,11 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
|||
<MyText style={tw`text-lg font-bold text-gray-900`}>Available Slots</MyText>
|
||||
</View>
|
||||
|
||||
{productDetail.deliverySlots.length === 0 ? (
|
||||
{sortedDeliverySlots.length === 0 ? (
|
||||
<MyText style={tw`text-gray-400 italic`}>No delivery slots available currently</MyText>
|
||||
) : (
|
||||
<>
|
||||
{productDetail.deliverySlots.slice(0, 2).map((slot, index) => (
|
||||
{sortedDeliverySlots.slice(0, 2).map((slot, index) => (
|
||||
<MyTouchableOpacity
|
||||
key={index}
|
||||
style={tw`flex-row items-start mb-4 bg-gray-50 p-3 rounded-xl border border-gray-100`}
|
||||
|
|
@ -365,12 +374,12 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
|||
<MaterialIcons name="add" size={20} color="#3B82F6" style={tw`mt-0.5`} />
|
||||
</MyTouchableOpacity>
|
||||
))}
|
||||
{productDetail.deliverySlots.length > 2 && (
|
||||
{sortedDeliverySlots.length > 2 && (
|
||||
<MyTouchableOpacity
|
||||
onPress={() => setShowAllSlots(true)}
|
||||
style={tw`items-center py-2`}
|
||||
>
|
||||
<MyText style={tw`text-brand500 font-bold text-sm`}>View All {productDetail.deliverySlots.length} Slots</MyText>
|
||||
<MyText style={tw`text-brand500 font-bold text-sm`}>View All {sortedDeliverySlots.length} Slots</MyText>
|
||||
</MyTouchableOpacity>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -557,7 +566,7 @@ const ProductDetail: React.FC<ProductDetailProps> = ({ productId, isFlashDeliver
|
|||
</View>
|
||||
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
{productDetail.deliverySlots.map((slot, index) => (
|
||||
{sortedDeliverySlots.map((slot, index) => (
|
||||
<MyTouchableOpacity
|
||||
key={index}
|
||||
style={tw`flex-row items-start mb-4 bg-gray-50 p-4 rounded-xl border border-gray-100`}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { trpc } from '@/src/trpc-client';
|
||||
|
||||
export const useGetEssentialConsts = () => {
|
||||
return trpc.common.essentialConsts.useQuery(undefined, {
|
||||
const query = trpc.common.essentialConsts.useQuery(undefined, {
|
||||
refetchInterval: 60000,
|
||||
});
|
||||
return { ...query, refetch: query.refetch };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { trpc } from '../trpc-client';
|
|||
interface AddressFormProps {
|
||||
onSuccess: () => void;
|
||||
initialValues?: {
|
||||
id?: number;
|
||||
name: string;
|
||||
phone: string;
|
||||
addressLine1: string;
|
||||
|
|
@ -18,6 +19,9 @@ interface AddressFormProps {
|
|||
state: string;
|
||||
pincode: string;
|
||||
isDefault: boolean;
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
googleMapsLocation?: string;
|
||||
};
|
||||
isEdit?: boolean;
|
||||
}
|
||||
|
|
@ -26,11 +30,17 @@ const AddressForm: React.FC<AddressFormProps> = ({ onSuccess, initialValues, isE
|
|||
const [locationLoading, setLocationLoading] = useState(false);
|
||||
const [locationError, setLocationError] = useState<string | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [showGoogleMapsField, setShowGoogleMapsField] = useState(false);
|
||||
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({
|
||||
onSuccess: () => {
|
||||
setIsSubmitting(false);
|
||||
onSuccess();
|
||||
setTimeout(() => onSuccess(), 100);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
setIsSubmitting(false);
|
||||
|
|
@ -38,12 +48,22 @@ const AddressForm: React.FC<AddressFormProps> = ({ onSuccess, initialValues, isE
|
|||
},
|
||||
});
|
||||
|
||||
const attachCurrentLocation = async (setFieldValue: (field: string, value: any) => void) => {
|
||||
const updateAddressMutation = trpc.user.address.updateAddress.useMutation({
|
||||
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);
|
||||
setLocationError(null);
|
||||
|
||||
try {
|
||||
// Request location permission
|
||||
const { status } = await Location.requestForegroundPermissionsAsync();
|
||||
|
||||
if (status !== 'granted') {
|
||||
|
|
@ -51,28 +71,12 @@ const AddressForm: React.FC<AddressFormProps> = ({ onSuccess, initialValues, isE
|
|||
return;
|
||||
}
|
||||
|
||||
// Get current position
|
||||
const location = await Location.getCurrentPositionAsync({
|
||||
accuracy: Location.Accuracy.High,
|
||||
});
|
||||
|
||||
// Reverse geocode to get address
|
||||
const address = await Location.reverseGeocodeAsync({
|
||||
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');
|
||||
}
|
||||
const { latitude, longitude } = location.coords;
|
||||
setCurrentLocation({ latitude, longitude });
|
||||
} catch (error) {
|
||||
console.error('Location error:', error);
|
||||
setLocationError('Unable to fetch location. Please check your GPS settings.');
|
||||
|
|
@ -105,11 +109,24 @@ const AddressForm: React.FC<AddressFormProps> = ({ onSuccess, initialValues, isE
|
|||
state: 'Telangana',
|
||||
pincode: '509001',
|
||||
isDefault: false,
|
||||
latitude: undefined,
|
||||
longitude: undefined,
|
||||
googleMapsLocation: '',
|
||||
}}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={(values) => {
|
||||
setIsSubmitting(true);
|
||||
createAddressMutation.mutate(values);
|
||||
const payload = {
|
||||
...values,
|
||||
latitude: currentLocation?.latitude,
|
||||
longitude: currentLocation?.longitude,
|
||||
googleMapsLocation: values.googleMapsLocation || undefined,
|
||||
};
|
||||
if (isEdit && initialValues?.id) {
|
||||
updateAddressMutation.mutate({ id: initialValues.id, ...payload });
|
||||
} else {
|
||||
createAddressMutation.mutate(payload);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ handleChange, handleBlur, handleSubmit, values, errors, touched, setFieldValue }) => (
|
||||
|
|
@ -181,6 +198,61 @@ const AddressForm: React.FC<AddressFormProps> = ({ onSuccess, initialValues, isE
|
|||
/>
|
||||
{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`}>Current Location Attached</MyText>
|
||||
<MyTouchableOpacity
|
||||
onPress={() => attachCurrentLocation()}
|
||||
disabled={locationLoading}
|
||||
style={tw`ml-4`}
|
||||
>
|
||||
<MyText style={tw`text-blue-500 text-sm font-medium`}>Change</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('googleMapsLocation')}
|
||||
onBlur={handleBlur('googleMapsLocation')}
|
||||
value={values.googleMapsLocation}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={tw`flex-row items-center mb-4`}>
|
||||
<Checkbox
|
||||
checked={values.isDefault}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue