From 28c720701616aad3f8bc2daa148ef1c437421bfc Mon Sep 17 00:00:00 2001 From: shafi54 <108669266+shafi-aviz@users.noreply.github.com> Date: Fri, 30 Jan 2026 01:56:41 +0530 Subject: [PATCH] enh --- .../admin-ui/app/(drawer)/dashboard/index.tsx | 26 +++- apps/backend/src/lib/license-util.ts | 20 +++ apps/backend/src/trpc/admin-apis/user.ts | 14 ++- apps/backend/src/trpc/user-apis/address.ts | 35 +++++- .../app/(drawer)/(tabs)/home/index.tsx | 35 +++++- .../(drawer)/(tabs)/me/addresses/index.tsx | 50 ++++---- apps/user-ui/app/_layout.tsx | 48 ++++---- apps/user-ui/components/BackHandler.tsx | 32 +++++ apps/user-ui/components/ProductDetail.tsx | 19 ++- .../src/api-hooks/essential-consts.api.ts | 3 +- apps/user-ui/src/components/AddressForm.tsx | 116 ++++++++++++++---- 11 files changed, 309 insertions(+), 89 deletions(-) create mode 100644 apps/backend/src/lib/license-util.ts create mode 100644 apps/user-ui/components/BackHandler.tsx diff --git a/apps/admin-ui/app/(drawer)/dashboard/index.tsx b/apps/admin-ui/app/(drawer)/dashboard/index.tsx index 55d5b4a..cfee608 100644 --- a/apps/admin-ui/app/(drawer)/dashboard/index.tsx +++ b/apps/admin-ui/app/(drawer)/dashboard/index.tsx @@ -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 = ({ item, router }) => ( +const MenuItemComponent: React.FC = ({ item, router }) => { + + return ( router.push(item.route as any)} @@ -39,13 +43,23 @@ const MenuItemComponent: React.FC = ({ item, router }) = {item.description} )} - + {item.badgeCount ? ( + + {item.badgeCount} + + ) : null} + -); +) +} 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() { > + {item.badgeCount ? ( + + {item.badgeCount} + + ) : null} {item.title} diff --git a/apps/backend/src/lib/license-util.ts b/apps/backend/src/lib/license-util.ts new file mode 100644 index 0000000..dd928e8 --- /dev/null +++ b/apps/backend/src/lib/license-util.ts @@ -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; + } +} diff --git a/apps/backend/src/trpc/admin-apis/user.ts b/apps/backend/src/trpc/admin-apis/user.ts index 74493ae..177f135 100644 --- a/apps/backend/src/trpc/admin-apis/user.ts +++ b/apps/backend/src/trpc/admin-apis/user.ts @@ -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, + }; + }), }; \ No newline at end of file diff --git a/apps/backend/src/trpc/user-apis/address.ts b/apps/backend/src/trpc/user-apis/address.ts index 71a0553..9dff42c 100644 --- a/apps/backend/src/trpc/user-apis/address.ts +++ b/apps/backend/src/trpc/user-apis/address.ts @@ -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 }; diff --git a/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx b/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx index 890c707..5074cb9 100755 --- a/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx +++ b/apps/user-ui/app/(drawer)/(tabs)/home/index.tsx @@ -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 => 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() { + } onScroll={(e) => { handleScroll(e); diff --git a/apps/user-ui/app/(drawer)/(tabs)/me/addresses/index.tsx b/apps/user-ui/app/(drawer)/(tabs)/me/addresses/index.tsx index 7a9eb41..2b7cd69 100644 --- a/apps/user-ui/app/(drawer)/(tabs)/me/addresses/index.tsx +++ b/apps/user-ui/app/(drawer)/(tabs)/me/addresses/index.tsx @@ -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} /> - setModalVisible(false)} - > - - + setModalVisible(false)}> + + {editingAddress ? 'Edit Address' : 'Add Address'} @@ -257,22 +252,25 @@ export default function Addresses() { - - - + + + + + ); } \ No newline at end of file diff --git a/apps/user-ui/app/_layout.tsx b/apps/user-ui/app/_layout.tsx index 4d1331d..23b8acb 100755 --- a/apps/user-ui/app/_layout.tsx +++ b/apps/user-ui/app/_layout.tsx @@ -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() { @@ -47,29 +48,30 @@ export default function RootLayout() { - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/user-ui/components/BackHandler.tsx b/apps/user-ui/components/BackHandler.tsx new file mode 100644 index 0000000..d4d9973 --- /dev/null +++ b/apps/user-ui/components/BackHandler.tsx @@ -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; +} \ No newline at end of file diff --git a/apps/user-ui/components/ProductDetail.tsx b/apps/user-ui/components/ProductDetail.tsx index 2ecf558..5b93f5b 100644 --- a/apps/user-ui/components/ProductDetail.tsx +++ b/apps/user-ui/components/ProductDetail.tsx @@ -55,6 +55,15 @@ const ProductDetail: React.FC = ({ 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 = ({ productId, isFlashDeliver Available Slots - {productDetail.deliverySlots.length === 0 ? ( + {sortedDeliverySlots.length === 0 ? ( No delivery slots available currently ) : ( <> - {productDetail.deliverySlots.slice(0, 2).map((slot, index) => ( + {sortedDeliverySlots.slice(0, 2).map((slot, index) => ( = ({ productId, isFlashDeliver ))} - {productDetail.deliverySlots.length > 2 && ( + {sortedDeliverySlots.length > 2 && ( setShowAllSlots(true)} style={tw`items-center py-2`} > - View All {productDetail.deliverySlots.length} Slots + View All {sortedDeliverySlots.length} Slots )} @@ -557,7 +566,7 @@ const ProductDetail: React.FC = ({ productId, isFlashDeliver - {productDetail.deliverySlots.map((slot, index) => ( + {sortedDeliverySlots.map((slot, index) => ( { - return trpc.common.essentialConsts.useQuery(undefined, { + const query = trpc.common.essentialConsts.useQuery(undefined, { refetchInterval: 60000, }); + return { ...query, refetch: query.refetch }; }; diff --git a/apps/user-ui/src/components/AddressForm.tsx b/apps/user-ui/src/components/AddressForm.tsx index c824913..7ebc81e 100644 --- a/apps/user-ui/src/components/AddressForm.tsx +++ b/apps/user-ui/src/components/AddressForm.tsx @@ -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 = ({ onSuccess, initialValues, isE const [locationLoading, setLocationLoading] = useState(false); const [locationError, setLocationError] = useState(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 = ({ 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 = ({ 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 = ({ 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 = ({ onSuccess, initialValues, isE /> {touched.pincode && errors.pincode && {errors.pincode}} + {locationLoading ? ( + Fetching location... + ) : locationError ? ( + {locationError} + ) : currentLocation ? ( + + Current Location Attached + attachCurrentLocation()} + disabled={locationLoading} + style={tw`ml-4`} + > + Change + + + ) : ( + attachCurrentLocation()} + disabled={locationLoading} + style={tw`mb-4`} + > + + Attach Current Location + + + )} + + setShowGoogleMapsField(true)} + disabled={false} + style={tw`mb-1`} + > + + Attach with Google Maps + + + + {showGoogleMapsField && ( + + + 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. + + + + )} +