Compare commits

..

No commits in common. "1dca7a34548c9f8d386f4bf07fdd7d68b244ec03" and "10d13408d38bc569f8c7d91415fed4eca23d039a" have entirely different histories.

31 changed files with 219 additions and 4367 deletions

View file

@ -1,58 +1,25 @@
import React, { useState, useCallback, useMemo } from "react"; import React, { useState } from "react";
import { View, TouchableOpacity, Alert, ActivityIndicator } from "react-native"; import { View, Text, TouchableOpacity, Alert } from "react-native";
import { MaterialIcons } from "@expo/vector-icons"; import { tw, ConfirmationDialog, MyText, MyFlatList, useMarkDataFetchers, usePagination, ImageViewerURI } from "common-ui";
import { useRouter } from "expo-router";
import { tw, ConfirmationDialog, MyText, MyFlatList, useMarkDataFetchers, ImageViewerURI } from "common-ui";
import { trpc } from "@/src/trpc-client"; import { trpc } from "@/src/trpc-client";
export default function Complaints() { export default function Complaints() {
const router = useRouter(); const { currentPage, pageSize, PaginationComponent } = usePagination(5); // 5 complaints per page for testing
const [dialogOpen, setDialogOpen] = useState(false); const { data, isLoading, error, refetch } = trpc.admin.complaint.getAll.useQuery({
const [selectedComplaintId, setSelectedComplaintId] = useState<number | null>(null); page: currentPage,
limit: pageSize,
const { });
data, const resolveComplaint = trpc.admin.complaint.resolve.useMutation();
isLoading,
isError,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
refetch,
} = trpc.admin.complaint.getAll.useInfiniteQuery(
{ limit: 20 },
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);
useMarkDataFetchers(() => { useMarkDataFetchers(() => {
refetch(); refetch();
}); });
const resolveComplaint = trpc.admin.complaint.resolve.useMutation(); const [dialogOpen, setDialogOpen] = useState(false);
const [selectedComplaintId, setSelectedComplaintId] = useState<number | null>(null);
const complaints = useMemo(() => { const complaints = data?.complaints || [];
const allComplaints = data?.pages.flatMap((page) => page.complaints) || []; const totalCount = data?.totalCount || 0;
return allComplaints.filter(
(complaint, index, self) =>
index === self.findIndex((c) => c.id === complaint.id)
);
}, [data]);
const handleLoadMore = useCallback(() => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
const handleUserPress = useCallback((userId: number) => {
router.push(`/(drawer)/user-management/${userId}`);
}, [router]);
const handleOrderPress = useCallback((orderId: number) => {
router.push(`/(drawer)/manage-orders/order-details/${orderId}`);
}, [router]);
const handleMarkResolved = (id: number) => { const handleMarkResolved = (id: number) => {
setSelectedComplaintId(id); setSelectedComplaintId(id);
@ -83,158 +50,109 @@ export default function Complaints() {
); );
}; };
if (isLoading) { if (isLoading) {
return ( return (
<View style={tw`flex-1 justify-center items-center bg-gray-50`}> <View style={tw`flex-1 justify-center items-center`}>
<ActivityIndicator size="large" color="#3B82F6" /> <MyText style={tw`text-gray-600`}>Loading complaints...</MyText>
<MyText style={tw`text-gray-500 mt-4`}>Loading complaints...</MyText> </View>
</View> );
); }
}
if (isError) { if (error) {
return ( return (
<View style={tw`flex-1 justify-center items-center bg-gray-50 p-6`}> <View style={tw`flex-1 justify-center items-center`}>
<MaterialIcons name="error-outline" size={48} color="#EF4444" /> <MyText style={tw`text-red-600`}>Error loading complaints</MyText>
<MyText style={tw`text-gray-900 text-lg font-bold mt-4`}>Error</MyText> </View>
<MyText style={tw`text-gray-500 text-center mt-2 mb-6`}> );
{error?.message || "Failed to load complaints"} }
</MyText>
<TouchableOpacity
onPress={() => refetch()}
style={tw`bg-blue-600 px-6 py-3 rounded-full`}
>
<MyText style={tw`text-white font-semibold`}>Retry</MyText>
</TouchableOpacity>
</View>
);
}
return ( return (
<View style={tw`flex-1 bg-gray-50`}> <View style={tw`flex-1`}>
<MyFlatList <MyFlatList
style={tw`flex-1`} style={tw`flex-1 bg-white`}
contentContainerStyle={tw`px-4 py-4`} contentContainerStyle={tw`px-4 pb-6`}
data={complaints} data={complaints}
keyExtractor={(item) => item.id.toString()} keyExtractor={(item) => item.id.toString()}
onEndReached={handleLoadMore} renderItem={({ item }) => (
onEndReachedThreshold={0.5} <View style={tw`bg-white p-4 mb-4 rounded-2xl shadow-lg`}>
renderItem={({ item }) => ( <MyText style={tw`text-lg font-bold mb-2 text-gray-800`}>Complaint #{item.id}</MyText>
<View style={tw`bg-white p-4 mb-4 rounded-2xl shadow-sm border border-gray-100`}> <MyText style={tw`text-base mb-2 text-gray-700`}>{item.text}</MyText>
<View style={tw`flex-row justify-between items-start mb-2`}>
<MyText style={tw`text-lg font-bold text-gray-900`}>
Complaint #{item.id}
</MyText>
<View
style={tw`px-2.5 py-1 rounded-full ${
item.status === "resolved"
? "bg-green-100 border border-green-200"
: "bg-amber-100 border border-amber-200"
}`}
>
<MyText
style={tw`text-xs font-semibold ${
item.status === "resolved" ? "text-green-700" : "text-amber-700"
}`}
>
{item.status === "resolved" ? "Resolved" : "Pending"}
</MyText>
</View>
</View>
<MyText style={tw`text-base text-gray-700 mb-3 leading-5`}> {item.images && item.images.length > 0 && (
{item.text} <View style={tw`mt-3 mb-3`}>
</MyText> <MyText style={tw`text-sm font-semibold text-gray-700 mb-2`}>Attached Images:</MyText>
<View style={tw`flex-row flex-wrap gap-2`}>
{item.images && item.images.length > 0 && ( {item.images.map((imageUri: string, index: number) => (
<View style={tw`mb-3`}> <ImageViewerURI
<MyText style={tw`text-sm font-semibold text-gray-700 mb-2`}> key={index}
Attached Images: uri={imageUri}
</MyText> style={tw`w-16 h-16 rounded-lg border border-gray-200`}
<View style={tw`flex-row flex-wrap gap-2`}> />
{item.images.map((imageUri: string, index: number) => ( ))}
<ImageViewerURI </View>
key={index}
uri={imageUri}
style={tw`w-16 h-16 rounded-lg border border-gray-200`}
/>
))}
</View> </View>
</View>
)}
<View style={tw`flex-row items-center gap-2 mb-3`}>
<MaterialIcons name="person" size={14} color="#6B7280" />
<TouchableOpacity
onPress={() => item.userId && handleUserPress(item.userId)}
>
<MyText style={tw`text-sm text-blue-600 underline`}>
{item.userName || item.userMobile || "Unknown User"}
</MyText>
</TouchableOpacity>
{item.orderId && (
<>
<MyText style={tw`text-sm text-gray-400`}>|</MyText>
<MaterialIcons name="shopping-bag" size={14} color="#6B7280" />
<TouchableOpacity
onPress={() => item.orderId && handleOrderPress(item.orderId)}
>
<MyText style={tw`text-sm text-blue-600 underline`}>
Order #{item.orderId}
</MyText>
</TouchableOpacity>
</>
)} )}
</View>
{item.status === "pending" && ( <View style={tw`flex-row items-center mb-2`}>
<TouchableOpacity <TouchableOpacity
onPress={() => handleMarkResolved(item.id)} onPress={() =>
style={tw`bg-blue-500 py-3 rounded-xl items-center shadow-sm mt-2`} Alert.alert("User Page", "User page coming soon")
> }
<MyText style={tw`text-white font-semibold`}> >
Resolve Complaint <MyText style={tw`text-sm text-blue-600 underline`}>
</MyText> {item.userName}
</TouchableOpacity> </MyText>
)} </TouchableOpacity>
<MyText style={tw`text-sm text-gray-600 mx-2`}>|</MyText>
{item.orderId && (
<TouchableOpacity
onPress={() =>
Alert.alert("Order Page", "Order page coming soon")
}
>
<MyText style={tw`text-sm text-blue-600 underline`}>
Order #{item.orderId}
</MyText>
</TouchableOpacity>
)}
</View>
<MyText
style={tw`text-sm ${
item.status === "resolved" ? "text-green-600" : "text-red-600"
}`}
>
Status: {item.status}
</MyText>
{item.status === "pending" && (
<TouchableOpacity
onPress={() => handleMarkResolved(item.id)}
style={tw`mt-2 bg-blue-500 p-3 rounded-lg shadow-md`}
>
<MyText style={tw`text-white text-center font-semibold`}>Mark as Resolved</MyText>
</TouchableOpacity>
)}
</View> </View>
)} )}
ListEmptyComponent={ ListEmptyComponent={
<View style={tw`flex-1 justify-center items-center py-20`}> <View style={tw`flex-1 justify-center items-center py-10`}>
<View style={tw`bg-white p-6 rounded-full shadow-sm mb-4`}> <MyText style={tw`text-gray-500 text-center`}>No complaints found</MyText>
<MaterialIcons name="inbox" size={48} color="#D1D5DB" /> </View>
</View> }
<MyText style={tw`text-gray-900 text-lg font-bold`}> />
No complaints <PaginationComponent totalCount={totalCount} />
</MyText> <ConfirmationDialog
<MyText style={tw`text-gray-500 text-center mt-2`}> open={dialogOpen}
All complaints will appear here positiveAction={handleConfirmResolve}
</MyText> commentNeeded={true}
</View> negativeAction={() => {
} setDialogOpen(false);
ListFooterComponent={ setSelectedComplaintId(null);
isFetchingNextPage ? ( }}
<View style={tw`py-4 items-center flex-row justify-center`}> title="Mark as Resolved"
<ActivityIndicator size="small" color="#3B82F6" /> message="Add admin notes for this resolution:"
<MyText style={tw`text-gray-500 ml-2`}>Loading more...</MyText> confirmText="Resolve"
</View> cancelText="Cancel"
) : null />
} </View>
/>
<ConfirmationDialog
open={dialogOpen}
positiveAction={handleConfirmResolve}
commentNeeded={true}
negativeAction={() => {
setDialogOpen(false);
setSelectedComplaintId(null);
}}
title="Resolve Complaint"
message="Add admin notes for this resolution:"
confirmText="Resolve"
cancelText="Cancel"
isLoading={resolveComplaint.isPending}
/>
</View>
); );
} }

View file

@ -82,16 +82,6 @@ export default function OrderDetails() {
}, },
}); });
const removeDeliveryChargeMutation = trpc.admin.order.removeDeliveryCharge.useMutation({
onSuccess: () => {
Alert.alert("Success", "Delivery charge has been removed");
refetch();
},
onError: (error: any) => {
Alert.alert("Error", error.message || "Failed to remove delivery charge");
},
});
if (isLoading) { if (isLoading) {
return ( return (
<View style={tw`flex-1 justify-center items-center bg-gray-50`}> <View style={tw`flex-1 justify-center items-center bg-gray-50`}>
@ -277,23 +267,6 @@ export default function OrderDetails() {
</View> </View>
</View> </View>
{/* Cancellation Reason */}
{order.status === "cancelled" && order.cancelReason && (
<View
style={tw`bg-red-50 p-5 rounded-2xl border border-red-100 mb-4`}
>
<View style={tw`flex-row items-center mb-2`}>
<MaterialIcons name="cancel" size={18} color="#DC2626" />
<MyText style={tw`text-sm font-bold text-red-800 ml-2`}>
Cancellation Reason
</MyText>
</View>
<MyText style={tw`text-sm text-red-900 leading-5`}>
{order.cancelReason}
</MyText>
</View>
)}
{/* Order Progress (Simplified Timeline) */} {/* Order Progress (Simplified Timeline) */}
{order.status !== "cancelled" && ( {order.status !== "cancelled" && (
<View <View
@ -518,40 +491,6 @@ export default function OrderDetails() {
-{discountAmount} -{discountAmount}
</MyText> </MyText>
</View> </View>
)}
{order.deliveryCharge > 0 && (
<View style={tw`flex-row justify-between items-center mb-2`}>
<View style={tw`flex-row items-center`}>
<MyText style={tw`text-gray-600 font-medium`}>
Delivery Charge
</MyText>
<TouchableOpacity
onPress={() => {
Alert.alert(
'Remove Delivery Cost',
'Are you sure you want to remove the delivery cost from this order?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Remove',
style: 'destructive',
onPress: () => removeDeliveryChargeMutation.mutate({ orderId: order.id }),
},
]
);
}}
disabled={removeDeliveryChargeMutation.isPending}
style={tw`ml-2 px-2 py-1 bg-red-100 rounded-md`}
>
<MyText style={tw`text-xs font-bold text-red-600`}>
{removeDeliveryChargeMutation.isPending ? 'Removing...' : 'Remove'}
</MyText>
</TouchableOpacity>
</View>
<MyText style={tw`text-gray-600 font-medium`}>
{order.deliveryCharge}
</MyText>
</View>
)} )}
<View style={tw`flex-row justify-between items-center pt-2 border-t border-gray-200`}> <View style={tw`flex-row justify-between items-center pt-2 border-t border-gray-200`}>
<View style={tw`flex-row items-center`}> <View style={tw`flex-row items-center`}>

View file

@ -57,9 +57,6 @@ interface OrderType {
readableId: number; readableId: number;
customerName: string | null; customerName: string | null;
address: string; address: string;
addressId: number;
latitude: number | null;
longitude: number | null;
totalAmount: number; totalAmount: number;
deliveryCharge: number; deliveryCharge: number;
items: { items: {
@ -362,11 +359,11 @@ const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }
isDelivered: order.isDelivered, isDelivered: order.isDelivered,
isFlashDelivery: order.isFlashDelivery, isFlashDelivery: order.isFlashDelivery,
address: order.address, address: order.address,
addressId: order.addressId, addressId: 0,
adminNotes: order.adminNotes, adminNotes: order.adminNotes,
userNotes: order.userNotes, userNotes: order.userNotes,
latitude: order.latitude, latitude: null,
longitude: order.longitude, longitude: null,
status: order.status, status: order.status,
}} }}
onViewDetails={handleMenuOption} onViewDetails={handleMenuOption}
@ -380,7 +377,7 @@ const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }
setMenuOpen(false); setMenuOpen(false);
setCancelDialogOpen(true); setCancelDialogOpen(true);
}} }}
onAttachLocation={() => refetch()} onAttachLocation={() => {}}
onWhatsApp={() => {}} onWhatsApp={() => {}}
onDial={() => {}} onDial={() => {}}
/> />

View file

@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { MaterialCommunityIcons, Entypo } from '@expo/vector-icons'; import { MaterialCommunityIcons, Entypo } from '@expo/vector-icons';
import { View, TouchableOpacity, FlatList, Alert, ActivityIndicator } from 'react-native'; import { View, TouchableOpacity, FlatList, Alert } from 'react-native';
import { AppContainer, MyText, tw, MyFlatList , BottomDialog, MyTouchableOpacity, Checkbox } from 'common-ui'; import { AppContainer, MyText, tw, MyFlatList , BottomDialog, MyTouchableOpacity } from 'common-ui';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@ -12,7 +12,6 @@ interface SlotItemProps {
router: any; router: any;
setDialogProducts: React.Dispatch<React.SetStateAction<any[]>>; setDialogProducts: React.Dispatch<React.SetStateAction<any[]>>;
setDialogOpen: React.Dispatch<React.SetStateAction<boolean>>; setDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
refetch: () => void;
} }
const SlotItemComponent: React.FC<SlotItemProps> = ({ const SlotItemComponent: React.FC<SlotItemProps> = ({
@ -20,7 +19,6 @@ const SlotItemComponent: React.FC<SlotItemProps> = ({
router, router,
setDialogProducts, setDialogProducts,
setDialogOpen, setDialogOpen,
refetch,
}) => { }) => {
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const slotProducts = slot.products?.map((p: any) => p.name).filter(Boolean) || []; const slotProducts = slot.products?.map((p: any) => p.name).filter(Boolean) || [];
@ -30,29 +28,6 @@ const SlotItemComponent: React.FC<SlotItemProps> = ({
const statusColor = isActive ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'; const statusColor = isActive ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700';
const statusText = isActive ? 'Active' : 'Inactive'; const statusText = isActive ? 'Active' : 'Inactive';
const updateSlotCapacity = trpc.admin.slots.updateSlotCapacity.useMutation();
const handleCapacityToggle = () => {
updateSlotCapacity.mutate(
{ slotId: slot.id, isCapacityFull: !slot.isCapacityFull },
{
onSuccess: () => {
setMenuOpen(false);
refetch();
Alert.alert(
'Success',
slot.isCapacityFull
? 'Slot capacity reset. It will now be visible to users.'
: 'Slot marked as full capacity. It will be hidden from users.'
);
},
onError: (error: any) => {
Alert.alert('Error', error.message || 'Failed to update slot capacity');
},
}
);
};
return ( return (
<TouchableOpacity <TouchableOpacity
onPress={() => router.push(`/(drawer)/slots/slot-details?slotId=${slot.id}`)} onPress={() => router.push(`/(drawer)/slots/slot-details?slotId=${slot.id}`)}
@ -80,15 +55,10 @@ const SlotItemComponent: React.FC<SlotItemProps> = ({
<MyText style={tw`text-xs font-bold text-pink1`}>Edit</MyText> <MyText style={tw`text-xs font-bold text-pink1`}>Edit</MyText>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
<View style={tw`px-3 py-1 rounded-full ${statusColor.split(' ')[0]}`}> <View style={tw`px-3 py-1 rounded-full ${statusColor.split(' ')[0]}`}>
<MyText style={tw`text-xs font-bold ${statusColor.split(' ')[1]}`}>{statusText}</MyText> <MyText style={tw`text-xs font-bold ${statusColor.split(' ')[1]}`}>{statusText}</MyText>
</View> </View>
{slot.isCapacityFull && ( <TouchableOpacity
<View style={tw`px-2 py-1 rounded-full bg-red-500 ml-2`}>
<MyText style={tw`text-xs font-bold text-white`}>FULL</MyText>
</View>
)}
<TouchableOpacity
onPress={() => setMenuOpen(true)} onPress={() => setMenuOpen(true)}
style={tw`ml-2 p-1`} style={tw`ml-2 p-1`}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
@ -98,75 +68,33 @@ const SlotItemComponent: React.FC<SlotItemProps> = ({
</View> </View>
</View> </View>
{/* Replicate Menu Dialog */} {/* Replicate Menu Dialog */}
<BottomDialog open={menuOpen} onClose={() => setMenuOpen(false)}> <BottomDialog open={menuOpen} onClose={() => setMenuOpen(false)}>
<View style={tw`p-4`}> <View style={tw`p-4`}>
<MyText style={tw`text-lg font-bold mb-4`}>Slot #{slot.id} Actions</MyText> <MyText style={tw`text-lg font-bold mb-4`}>Slot #{slot.id} Actions</MyText>
{/* Capacity Toggle */}
<TouchableOpacity <TouchableOpacity
onPress={handleCapacityToggle} onPress={() => {
disabled={updateSlotCapacity.isPending} setMenuOpen(false);
router.push(`/slots/add?baseslot=${slot.id}` as any);
}}
style={tw`py-4 border-b border-gray-200`} style={tw`py-4 border-b border-gray-200`}
>
<View style={tw`flex-row items-center justify-between`}>
<View style={tw`flex-row items-center flex-1`}>
{updateSlotCapacity.isPending ? (
<ActivityIndicator size="small" color="#EF4444" style={tw`mr-3`} />
) : (
<MaterialCommunityIcons
name={slot.isCapacityFull ? "package-variant-closed" : "package-variant"}
size={20}
color={slot.isCapacityFull ? "#EF4444" : "#4B5563"}
style={tw`mr-3`}
/>
)}
<View>
<MyText style={tw`text-base text-gray-800`}>Mark as Full Capacity</MyText>
<MyText style={tw`text-xs text-gray-500 mt-0.5`}>
{slot.isCapacityFull
? "Slot is hidden from users"
: "Hidden from users when full"}
</MyText>
</View>
</View>
{updateSlotCapacity.isPending ? (
<ActivityIndicator size="small" color="#EF4444" />
) : (
<Checkbox
checked={slot.isCapacityFull}
onPress={handleCapacityToggle}
size={22}
fillColor="#EF4444"
checkColor="#FFFFFF"
/>
)}
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
setMenuOpen(false);
router.push(`/slots/add?baseslot=${slot.id}` as any);
}}
style={tw`py-4 border-b border-gray-200`}
>
<View style={tw`flex-row items-center`}>
<MaterialCommunityIcons name="content-copy" size={20} color="#4B5563" style={tw`mr-3`} />
<MyText style={tw`text-base text-gray-800`}>Replicate Slot</MyText>
</View>
</TouchableOpacity>
<TouchableOpacity
onPress={() => setMenuOpen(false)}
style={tw`py-4 mt-2`}
> >
<View style={tw`flex-row items-center`}> <View style={tw`flex-row items-center`}>
<MaterialCommunityIcons name="close" size={20} color="#EF4444" style={tw`mr-3`} /> <MaterialCommunityIcons name="content-copy" size={20} color="#4B5563" style={tw`mr-3`} />
<MyText style={tw`text-base text-red-500`}>Cancel</MyText> <MyText style={tw`text-base text-gray-800`}>Replicate Slot</MyText>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
</View> <TouchableOpacity
</BottomDialog> onPress={() => setMenuOpen(false)}
style={tw`py-4 mt-2`}
>
<View style={tw`flex-row items-center`}>
<MaterialCommunityIcons name="close" size={20} color="#EF4444" style={tw`mr-3`} />
<MyText style={tw`text-base text-red-500`}>Cancel</MyText>
</View>
</TouchableOpacity>
</View>
</BottomDialog>
{/* Divider */} {/* Divider */}
<View style={tw`h-[1px] bg-gray-100 mb-4`} /> <View style={tw`h-[1px] bg-gray-100 mb-4`} />
@ -265,7 +193,6 @@ export default function Slots() {
router={router} router={router}
setDialogProducts={setDialogProducts} setDialogProducts={setDialogProducts}
setDialogOpen={setDialogOpen} setDialogOpen={setDialogOpen}
refetch={refetch}
/> />
)} )}
contentContainerStyle={tw`p-4`} contentContainerStyle={tw`p-4`}

View file

@ -1,6 +1,6 @@
ENV_MODE=PROD ENV_MODE=PROD
# DATABASE_URL=postgresql://postgres:meatfarmer_master_password@57.128.212.174:7447/meatfarmer #technocracy DATABASE_URL=postgresql://postgres:meatfarmer_master_password@57.128.212.174:7447/meatfarmer #technocracy
DATABASE_URL=postgres://postgres:meatfarmer_master_password@5.223.55.14:7447/meatfarmer #hetzner # DATABASE_URL=postgres://postgres:meatfarmer_master_password@5.223.55.14:7447/meatfarmer #hetzner
PHONE_PE_BASE_URL=https://api-preprod.phonepe.com/ PHONE_PE_BASE_URL=https://api-preprod.phonepe.com/
PHONE_PE_CLIENT_ID=TEST-M23F2IGP34ZAR_25090 PHONE_PE_CLIENT_ID=TEST-M23F2IGP34ZAR_25090
PHONE_PE_CLIENT_VERSION=1 PHONE_PE_CLIENT_VERSION=1

View file

@ -1 +0,0 @@
This is a demo file.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
ALTER TABLE "mf"."delivery_slot_info" ADD COLUMN "is_capacity_full" boolean DEFAULT false NOT NULL;

File diff suppressed because it is too large Load diff

View file

@ -519,13 +519,6 @@
"when": 1770561175889, "when": 1770561175889,
"tag": "0073_faithful_gravity", "tag": "0073_faithful_gravity",
"breakpoints": true "breakpoints": true
},
{
"idx": 74,
"version": "7",
"when": 1771674555093,
"tag": "0074_outgoing_black_cat",
"breakpoints": true
} }
] ]
} }

View file

@ -163,15 +163,6 @@ if (fs.existsSync(fallbackUiIndex)) {
console.warn(`Fallback UI build not found at ${fallbackUiIndex}`) console.warn(`Fallback UI build not found at ${fallbackUiIndex}`)
} }
// Serve /assets/public folder at /assets route
const assetsPublicDir = path.resolve(__dirname, './assets/public');
if (fs.existsSync(assetsPublicDir)) {
app.use('/assets', express.static(assetsPublicDir));
console.log('Serving /assets from', assetsPublicDir);
} else {
console.warn('Assets public folder not found at', assetsPublicDir);
}
// Global error handler // Global error handler
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error(err); console.error(err);

View file

@ -192,7 +192,6 @@ export const deliverySlotInfo = mf.table('delivery_slot_info', {
freezeTime: timestamp('freeze_time').notNull(), freezeTime: timestamp('freeze_time').notNull(),
isActive: boolean('is_active').notNull().default(true), isActive: boolean('is_active').notNull().default(true),
isFlash: boolean('is_flash').notNull().default(false), isFlash: boolean('is_flash').notNull().default(false),
isCapacityFull: boolean('is_capacity_full').notNull().default(false),
deliverySequence: jsonb('delivery_sequence').$defaultFn(() => {}), deliverySequence: jsonb('delivery_sequence').$defaultFn(() => {}),
groupIds: jsonb('group_ids').$defaultFn(() => []), groupIds: jsonb('group_ids').$defaultFn(() => []),
}); });

View file

@ -21,7 +21,7 @@ interface Product {
productQuantity: number; productQuantity: number;
isFlashAvailable: boolean; isFlashAvailable: boolean;
flashPrice: string | null; flashPrice: string | null;
deliverySlots: Array<{ id: number; deliveryTime: Date; freezeTime: Date; isCapacityFull: boolean }>; deliverySlots: Array<{ id: number; deliveryTime: Date; freezeTime: Date }>;
specialDeals: Array<{ quantity: string; price: string; validTill: Date }>; specialDeals: Array<{ quantity: string; price: string; validTill: Date }>;
productTags: string[]; productTags: string[];
} }
@ -57,21 +57,19 @@ export async function initializeProducts(): Promise<void> {
}); });
const storeMap = new Map(allStores.map(s => [s.id, s])); const storeMap = new Map(allStores.map(s => [s.id, s]));
// Fetch all delivery slots (excluding full capacity slots) // Fetch all delivery slots
const allDeliverySlots = await db const allDeliverySlots = await db
.select({ .select({
productId: productSlots.productId, productId: productSlots.productId,
id: deliverySlotInfo.id, id: deliverySlotInfo.id,
deliveryTime: deliverySlotInfo.deliveryTime, deliveryTime: deliverySlotInfo.deliveryTime,
freezeTime: deliverySlotInfo.freezeTime, freezeTime: deliverySlotInfo.freezeTime,
isCapacityFull: deliverySlotInfo.isCapacityFull,
}) })
.from(productSlots) .from(productSlots)
.innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id)) .innerJoin(deliverySlotInfo, eq(productSlots.slotId, deliverySlotInfo.id))
.where( .where(
and( and(
eq(deliverySlotInfo.isActive, true), eq(deliverySlotInfo.isActive, true),
eq(deliverySlotInfo.isCapacityFull, false),
gt(deliverySlotInfo.deliveryTime, sql`NOW()`) gt(deliverySlotInfo.deliveryTime, sql`NOW()`)
) )
); );
@ -134,7 +132,7 @@ export async function initializeProducts(): Promise<void> {
productQuantity: product.productQuantity, productQuantity: product.productQuantity,
isFlashAvailable: product.isFlashAvailable, isFlashAvailable: product.isFlashAvailable,
flashPrice: product.flashPrice?.toString() || null, flashPrice: product.flashPrice?.toString() || null,
deliverySlots: deliverySlots.map(s => ({ id: s.id, deliveryTime: s.deliveryTime, freezeTime: s.freezeTime, isCapacityFull: s.isCapacityFull })), deliverySlots: deliverySlots.map(s => ({ id: s.id, deliveryTime: s.deliveryTime, freezeTime: s.freezeTime })),
specialDeals: specialDeals.map(d => ({ quantity: d.quantity.toString(), price: d.price.toString(), validTill: d.validTill })), specialDeals: specialDeals.map(d => ({ quantity: d.quantity.toString(), price: d.price.toString(), validTill: d.validTill })),
productTags: productTags, productTags: productTags,
}; };

View file

@ -11,7 +11,6 @@ interface SlotWithProducts {
deliveryTime: Date; deliveryTime: Date;
freezeTime: Date; freezeTime: Date;
isActive: boolean; isActive: boolean;
isCapacityFull: boolean;
products: Array<{ products: Array<{
id: number; id: number;
name: string; name: string;
@ -31,7 +30,6 @@ interface SlotInfo {
id: number; id: number;
deliveryTime: Date; deliveryTime: Date;
freezeTime: Date; freezeTime: Date;
isCapacityFull: boolean;
} }
export async function initializeSlotStore(): Promise<void> { export async function initializeSlotStore(): Promise<void> {
@ -82,7 +80,6 @@ export async function initializeSlotStore(): Promise<void> {
deliveryTime: slot.deliveryTime, deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime, freezeTime: slot.freezeTime,
isActive: slot.isActive, isActive: slot.isActive,
isCapacityFull: slot.isCapacityFull,
products: await Promise.all( products: await Promise.all(
slot.productSlots.map(async (productSlot) => ({ slot.productSlots.map(async (productSlot) => ({
id: productSlot.product.id, id: productSlot.product.id,
@ -121,7 +118,6 @@ export async function initializeSlotStore(): Promise<void> {
id: slot.id, id: slot.id,
deliveryTime: slot.deliveryTime, deliveryTime: slot.deliveryTime,
freezeTime: slot.freezeTime, freezeTime: slot.freezeTime,
isCapacityFull: slot.isCapacityFull,
}); });
} }
} }
@ -229,9 +225,7 @@ export async function getMultipleProductsSlots(productIds: number[]): Promise<Re
for (let i = 0; i < productIds.length; i++) { for (let i = 0; i < productIds.length; i++) {
const data = productsData[i]; const data = productsData[i];
if (data) { if (data) {
const slots = JSON.parse(data) as SlotInfo[]; result[productIds[i]] = JSON.parse(data) as SlotInfo[];
// Filter out slots that are at full capacity
result[productIds[i]] = slots.filter(slot => !slot.isCapacityFull);
} }
} }

View file

@ -2,45 +2,47 @@ import { router, 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 { complaints, users } from '../../db/schema'; import { complaints, users } from '../../db/schema';
import { eq, desc, lt, and } from 'drizzle-orm'; import { eq, desc } from 'drizzle-orm';
import { generateSignedUrlsFromS3Urls } from '../../lib/s3-client'; import { generateSignedUrlsFromS3Urls } from '../../lib/s3-client';
export const complaintRouter = router({ export const complaintRouter = router({
getAll: protectedProcedure getAll: protectedProcedure
.input(z.object({ .input(z.object({
cursor: z.number().optional(), page: z.number().optional().default(1),
limit: z.number().default(20), limit: z.number().optional().default(10),
})) }))
.query(async ({ input }) => { .query(async ({ input }) => {
const { cursor, limit } = input; const page = input.page;
const limit = input.limit;
const offset = (page - 1) * limit;
let whereCondition = cursor const [complaintsData, totalCountResult] = await Promise.all([
? lt(complaints.id, cursor) db
: undefined; .select({
id: complaints.id,
complaintBody: complaints.complaintBody,
userId: complaints.userId,
orderId: complaints.orderId,
isResolved: complaints.isResolved,
createdAt: complaints.createdAt,
userName: users.name,
images: complaints.images,
})
.from(complaints)
.leftJoin(users, eq(complaints.userId, users.id))
.orderBy(desc(complaints.createdAt))
.limit(limit)
.offset(offset),
db
.select({ count: db.$count(complaints) })
.from(complaints),
]);
const complaintsData = await db const totalCount = totalCountResult[0].count;
.select({
id: complaints.id,
complaintBody: complaints.complaintBody,
userId: complaints.userId,
orderId: complaints.orderId,
isResolved: complaints.isResolved,
createdAt: complaints.createdAt,
userName: users.name,
userMobile: users.mobile,
images: complaints.images,
})
.from(complaints)
.leftJoin(users, eq(complaints.userId, users.id))
.where(whereCondition)
.orderBy(desc(complaints.id))
.limit(limit + 1);
const hasMore = complaintsData.length > limit;
const complaintsToReturn = hasMore ? complaintsData.slice(0, limit) : complaintsData;
// Generate signed URLs for images
const complaintsWithSignedImages = await Promise.all( const complaintsWithSignedImages = await Promise.all(
complaintsToReturn.map(async (c) => { complaintsData.map(async (c) => {
const signedImages = c.images const signedImages = c.images
? await generateSignedUrlsFromS3Urls(c.images as string[]) ? await generateSignedUrlsFromS3Urls(c.images as string[])
: []; : [];
@ -50,7 +52,6 @@ export const complaintRouter = router({
text: c.complaintBody, text: c.complaintBody,
userId: c.userId, userId: c.userId,
userName: c.userName, userName: c.userName,
userMobile: c.userMobile,
orderId: c.orderId, orderId: c.orderId,
status: c.isResolved ? 'resolved' : 'pending', status: c.isResolved ? 'resolved' : 'pending',
createdAt: c.createdAt, createdAt: c.createdAt,
@ -61,9 +62,7 @@ export const complaintRouter = router({
return { return {
complaints: complaintsWithSignedImages, complaints: complaintsWithSignedImages,
nextCursor: hasMore totalCount,
? complaintsToReturn[complaintsToReturn.length - 1].id
: undefined,
}; };
}), }),

View file

@ -325,8 +325,7 @@ export const orderRouter = router({
: null, : null,
isCod: orderData.isCod, isCod: orderData.isCod,
isOnlinePayment: orderData.isOnlinePayment, isOnlinePayment: orderData.isOnlinePayment,
totalAmount: parseFloat(orderData.totalAmount?.toString() || '0') - parseFloat(orderData.deliveryCharge?.toString() || '0'), totalAmount: orderData.totalAmount,
deliveryCharge: parseFloat(orderData.deliveryCharge?.toString() || '0'),
adminNotes: orderData.adminNotes, adminNotes: orderData.adminNotes,
userNotes: orderData.userNotes, userNotes: orderData.userNotes,
createdAt: orderData.createdAt, createdAt: orderData.createdAt,
@ -461,34 +460,6 @@ export const orderRouter = router({
return { success: true }; return { success: true };
}), }),
removeDeliveryCharge: protectedProcedure
.input(z.object({ orderId: z.number() }))
.mutation(async ({ input }) => {
const { orderId } = input;
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
});
if (!order) {
throw new Error('Order not found');
}
const currentDeliveryCharge = parseFloat(order.deliveryCharge?.toString() || '0');
const currentTotalAmount = parseFloat(order.totalAmount?.toString() || '0');
const newTotalAmount = currentTotalAmount - currentDeliveryCharge;
await db
.update(orders)
.set({
deliveryCharge: '0',
totalAmount: newTotalAmount.toString()
})
.where(eq(orders.id, orderId));
return { success: true, message: 'Delivery charge removed' };
}),
getSlotOrders: protectedProcedure getSlotOrders: protectedProcedure
.input(getSlotOrdersSchema) .input(getSlotOrdersSchema)
.query(async ({ input }) => { .query(async ({ input }) => {

View file

@ -574,36 +574,4 @@ export const slotsRouter = router({
message: "Delivery sequence updated successfully", message: "Delivery sequence updated successfully",
}; };
}), }),
updateSlotCapacity: protectedProcedure
.input(z.object({
slotId: z.number(),
isCapacityFull: z.boolean(),
}))
.mutation(async ({ input, ctx }) => {
if (!ctx.staffUser?.id) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "Access denied" });
}
const { slotId, isCapacityFull } = input;
const [updatedSlot] = await db
.update(deliverySlotInfo)
.set({ isCapacityFull })
.where(eq(deliverySlotInfo.id, slotId))
.returning();
if (!updatedSlot) {
throw new ApiError("Slot not found", 404);
}
// Reinitialize stores to reflect changes
await initializeAllStores();
return {
success: true,
slot: updatedSlot,
message: `Slot ${isCapacityFull ? 'marked as full capacity' : 'capacity reset'}`,
};
}),
}); });

View file

@ -17,7 +17,6 @@ export const getNextDeliveryDate = async (productId: number): Promise<Date | nul
and( and(
eq(productSlots.productId, productId), eq(productSlots.productId, productId),
eq(deliverySlotInfo.isActive, true), eq(deliverySlotInfo.isActive, true),
eq(deliverySlotInfo.isCapacityFull, false),
gt(deliverySlotInfo.deliveryTime, sql`NOW()`) gt(deliverySlotInfo.deliveryTime, sql`NOW()`)
) )
) )

View file

@ -27,7 +27,6 @@ import { RazorpayPaymentService } from "../../lib/payments-utils";
import { getNextDeliveryDate } from "../common-apis/common"; import { getNextDeliveryDate } from "../common-apis/common";
import { CONST_KEYS, getConstant, getConstants } from "../../lib/const-store"; import { CONST_KEYS, getConstant, getConstants } from "../../lib/const-store";
import { publishFormattedOrder, publishCancellation } from "../../lib/post-order-handler"; import { publishFormattedOrder, publishCancellation } from "../../lib/post-order-handler";
import { getSlotById } from "../../stores/slot-store";
const validateAndGetCoupon = async ( const validateAndGetCoupon = async (
@ -405,17 +404,6 @@ export const orderRouter = router({
} }
} }
// Check if any selected slot is at full capacity (only for regular delivery)
if (!isFlashDelivery) {
const slotIds = [...new Set(selectedItems.filter(i => i.slotId !== null).map(i => i.slotId as number))];
for (const slotId of slotIds) {
const slot = await getSlotById(slotId);
if (slot?.isCapacityFull) {
throw new ApiError("Selected delivery slot is at full capacity. Please choose another slot.", 403);
}
}
}
let processedItems = selectedItems; let processedItems = selectedItems;
// Handle flash delivery slot resolution // Handle flash delivery slot resolution

View file

@ -47,10 +47,10 @@ export const productRouter = router({
const cachedProduct = await getProductByIdFromCache(productId); const cachedProduct = await getProductByIdFromCache(productId);
if (cachedProduct) { if (cachedProduct) {
// Filter delivery slots to only include those with future freeze times and not at full capacity // Filter delivery slots to only include those with future freeze times
const currentTime = new Date(); const currentTime = new Date();
const filteredSlots = cachedProduct.deliverySlots.filter(slot => const filteredSlots = cachedProduct.deliverySlots.filter(slot =>
dayjs(slot.freezeTime).isAfter(currentTime) && !slot.isCapacityFull dayjs(slot.freezeTime).isAfter(currentTime)
); );
return { return {
@ -107,7 +107,6 @@ export const productRouter = router({
and( and(
eq(productSlots.productId, productId), eq(productSlots.productId, productId),
eq(deliverySlotInfo.isActive, true), eq(deliverySlotInfo.isActive, true),
eq(deliverySlotInfo.isCapacityFull, false),
gt(deliverySlotInfo.deliveryTime, sql`NOW()`), gt(deliverySlotInfo.deliveryTime, sql`NOW()`),
gt(deliverySlotInfo.freezeTime, sql`NOW()`) gt(deliverySlotInfo.freezeTime, sql`NOW()`)
) )

View file

@ -48,7 +48,7 @@ export const slotsRouter = router({
const allSlots = await getAllSlotsFromCache(); const allSlots = await getAllSlotsFromCache();
const currentTime = new Date(); const currentTime = new Date();
const validSlots = allSlots const validSlots = allSlots
.filter((slot) => dayjs(slot.freezeTime).isAfter(currentTime) && !slot.isCapacityFull) .filter((slot) => dayjs(slot.freezeTime).isAfter(currentTime))
.sort((a, b) => dayjs(a.deliveryTime).valueOf() - dayjs(b.deliveryTime).valueOf()); .sort((a, b) => dayjs(a.deliveryTime).valueOf() - dayjs(b.deliveryTime).valueOf());
return { return {

View file

@ -35,30 +35,10 @@ const StoreCard = ({
const navigateToStore = () => router.push(`/(drawer)/(tabs)/stores/store-detail/${item.id}`); const navigateToStore = () => router.push(`/(drawer)/(tabs)/stores/store-detail/${item.id}`);
const isMeatStore = item.name.toLowerCase().includes('meat');
const ASSETS_BASE_URL = 'http://localhost:4000/assets';
return ( return (
<View <View
style={tw`bg-white rounded-[24px] mb-4 shadow-lg shadow-slate-200 border border-slate-200 overflow-hidden`} style={tw`bg-white rounded-[24px] mb-4 shadow-lg shadow-slate-200 border border-slate-200 overflow-hidden`}
> >
{/* Meat Store Images - Show at top if store name contains 'meat' */}
{isMeatStore && (
<View style={tw`flex-row h-16`}>
<Image
source={{ uri: `${ASSETS_BASE_URL}/halal.jpg` }}
style={tw`flex-1 h-full`}
contentFit="cover"
/>
<View style={tw`w-px bg-white`} />
<Image
source={{ uri: `${ASSETS_BASE_URL}/preservs.jpg` }}
style={tw`flex-1 h-full`}
contentFit="cover"
/>
</View>
)}
{/* Top Header Section - Touchable */} {/* Top Header Section - Touchable */}
<MyTouchableOpacity <MyTouchableOpacity
onPress={navigateToStore} onPress={navigateToStore}

View file

@ -1,7 +1,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { View, Dimensions, ScrollView, TouchableOpacity } from "react-native"; import { View, Dimensions, ScrollView, TouchableOpacity } from "react-native";
import { useRouter, useLocalSearchParams } from "expo-router"; import { useRouter, useLocalSearchParams } from "expo-router";
import { Image } from 'expo-image';
import { import {
theme, theme,
tw, tw,
@ -142,22 +141,6 @@ export default function StoreDetail() {
contentContainerStyle={[tw`px-4 pb-24`, { gap: 16 }]} contentContainerStyle={[tw`px-4 pb-24`, { gap: 16 }]}
ListHeaderComponent={ ListHeaderComponent={
<View style={tw`pt-4 pb-6`}> <View style={tw`pt-4 pb-6`}>
{/* Meat Store Images - Show at top if store name contains 'meat' */}
{storeData?.store?.name?.toLowerCase().includes('meat') && (
<View style={tw`flex-row h-16 mb-4`}>
<Image
source={{ uri: 'http://localhost:4000/assets/halal.jpg' }}
style={tw`flex-1 h-full`}
contentFit="cover"
/>
<View style={tw`w-px bg-white`} />
<Image
source={{ uri: 'http://localhost:4000/assets/preservs.jpg' }}
style={tw`flex-1 h-full`}
contentFit="cover"
/>
</View>
)}
<View <View
style={tw`bg-white p-6 rounded-2xl shadow-sm border border-gray-100 items-center`} style={tw`bg-white p-6 rounded-2xl shadow-sm border border-gray-100 items-center`}
> >

View file

@ -1,12 +1,10 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { View, Text, TouchableOpacity, ScrollView, Alert } from 'react-native'; import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
import { tw, BottomDialog, RawBottomDialog } from 'common-ui'; import { tw, BottomDialog, RawBottomDialog } from 'common-ui';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import AddressForm from '@/src/components/AddressForm'; import AddressForm from '@/src/components/AddressForm';
import LocationAttacher from '@/src/components/LocationAttacher';
import MaterialIcons from '@expo/vector-icons/MaterialIcons'; import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import * as Location from 'expo-location';
interface AddressSelectorProps { interface AddressSelectorProps {
selectedAddress: number | null; selectedAddress: number | null;
@ -18,24 +16,10 @@ const CheckoutAddressSelector: React.FC<AddressSelectorProps> = ({
onAddressSelect, onAddressSelect,
}) => { }) => {
const [showAddAddress, setShowAddAddress] = useState(false); const [showAddAddress, setShowAddAddress] = useState(false);
const [editingLocationAddressId, setEditingLocationAddressId] = useState<number | null>(null);
const [locationLoading, setLocationLoading] = useState(false);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const scrollViewRef = useRef<ScrollView>(null); const scrollViewRef = useRef<ScrollView>(null);
const { data: addresses } = trpc.user.address.getUserAddresses.useQuery(); const { data: addresses } = trpc.user.address.getUserAddresses.useQuery();
const updateAddressMutation = trpc.user.address.updateAddress.useMutation({
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['user.address.getUserAddresses'] });
setEditingLocationAddressId(null);
Alert.alert('Success', 'Location attached successfully');
},
onError: (error: any) => {
setEditingLocationAddressId(null);
Alert.alert('Error', error.message || 'Failed to attach location');
},
});
// Sort addresses with selected first, then default, then others // Sort addresses with selected first, then default, then others
const sortedAddresses = React.useMemo(() => { const sortedAddresses = React.useMemo(() => {
if (!addresses?.data) return []; if (!addresses?.data) return [];
@ -68,45 +52,6 @@ const CheckoutAddressSelector: React.FC<AddressSelectorProps> = ({
scrollViewRef.current?.scrollTo({ x: 0, y: 0, animated: true }); scrollViewRef.current?.scrollTo({ x: 0, y: 0, animated: true });
}; };
const handleAttachLocation = async (address: any) => {
setEditingLocationAddressId(address.id);
setLocationLoading(true);
try {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
Alert.alert('Permission Denied', 'Location permission is required to attach your current location');
setLocationLoading(false);
setEditingLocationAddressId(null);
return;
}
const location = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.High,
});
const { latitude, longitude } = location.coords;
updateAddressMutation.mutate({
id: address.id,
name: address.name,
phone: address.phone,
addressLine1: address.addressLine1,
addressLine2: address.addressLine2 || '',
city: address.city,
state: address.state,
pincode: address.pincode,
isDefault: address.isDefault,
latitude,
longitude,
});
} catch (error) {
Alert.alert('Error', 'Unable to fetch location. Please check your GPS settings.');
setLocationLoading(false);
setEditingLocationAddressId(null);
}
};
return ( return (
<> <>
<View style={tw`bg-white p-5 rounded-2xl shadow-sm mb-4 border border-gray-100`}> <View style={tw`bg-white p-5 rounded-2xl shadow-sm mb-4 border border-gray-100`}>
@ -135,19 +80,19 @@ const CheckoutAddressSelector: React.FC<AddressSelectorProps> = ({
</TouchableOpacity> </TouchableOpacity>
</View> </View>
) : ( ) : (
<ScrollView <ScrollView
ref={scrollViewRef} ref={scrollViewRef}
horizontal horizontal
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
style={tw`pb-2`} style={tw`pb-2`}
> >
{sortedAddresses.map((address) => ( {sortedAddresses.map((address) => (
<TouchableOpacity <TouchableOpacity
key={address.id} key={address.id}
onPress={() => { onPress={() => {
onAddressSelect(address.id); onAddressSelect(address.id);
resetScrollToLeft(); resetScrollToLeft();
}} }}
style={tw`w-72 p-4 mr-3 bg-gray-50 rounded-xl border-2 ${selectedAddress === address.id ? 'border-brand500 bg-blue-50' : 'border-gray-200' style={tw`w-72 p-4 mr-3 bg-gray-50 rounded-xl border-2 ${selectedAddress === address.id ? 'border-brand500 bg-blue-50' : 'border-gray-200'
} shadow-sm`} } shadow-sm`}
> >
@ -181,25 +126,8 @@ const CheckoutAddressSelector: React.FC<AddressSelectorProps> = ({
))} ))}
</ScrollView> </ScrollView>
)} )}
{/* Attach Location for selected address - outside the white box */}
{selectedAddress && (() => {
const selectedAddr = sortedAddresses.find(a => a.id === selectedAddress);
return selectedAddr && !selectedAddr.latitude && !selectedAddr.longitude ? (
<TouchableOpacity
onPress={() => handleAttachLocation(selectedAddr)}
disabled={locationLoading && editingLocationAddressId === selectedAddr.id}
style={tw`mt-3 py-2 px-3 bg-blue-50 rounded-lg self-start`}
>
<Text style={tw`text-blue-600 text-sm font-bold`}>
{locationLoading && editingLocationAddressId === selectedAddr.id ? 'Attaching...' : '+ Attach Current Location'}
</Text>
</TouchableOpacity>
) : null;
})()}
</View> </View>
{/* <BottomDialog open={showAddAddress} onClose={() => setShowAddAddress(false)}> */} {/* <BottomDialog open={showAddAddress} onClose={() => setShowAddAddress(false)}> */}
<RawBottomDialog open={showAddAddress} onClose={() => setShowAddAddress(false)}> <RawBottomDialog open={showAddAddress} onClose={() => setShowAddAddress(false)}>
@ -212,11 +140,11 @@ const CheckoutAddressSelector: React.FC<AddressSelectorProps> = ({
} }
queryClient.invalidateQueries(); queryClient.invalidateQueries();
}} }}
/> />
</RawBottomDialog> </RawBottomDialog>
{/* </BottomDialog> */} {/* </BottomDialog> */}
</> </>
); );
}; };
export default CheckoutAddressSelector; export default CheckoutAddressSelector;

View file

@ -52,7 +52,16 @@ const HealthTestWrapper: React.FC<HealthTestWrapperProps> = ({ children }) => {
} }
}, [versionFromBackend]); }, [versionFromBackend]);
if (error) { if (isLoading) {
return (
<View style={tw`flex-1 justify-center items-center bg-gray-50`}>
<ActivityIndicator size="large" color={theme.colors.brand500} />
<MyText style={tw`text-gray-500 mt-4 font-medium`}>Checking service status...</MyText>
</View>
);
}
if (error || data?.status !== "ok") {
return ( return (
<View style={tw`flex-1 justify-center items-center bg-gray-50 p-6`}> <View style={tw`flex-1 justify-center items-center bg-gray-50 p-6`}>
<View style={tw`w-16 h-16 bg-red-100 rounded-full items-center justify-center mb-4`}> <View style={tw`w-16 h-16 bg-red-100 rounded-full items-center justify-center mb-4`}>

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { View, TouchableOpacity, ActivityIndicator, Linking } from 'react-native'; import { View, TouchableOpacity, ActivityIndicator } from 'react-native';
import { useRouter } from 'expo-router'; import { useRouter } from 'expo-router';
import { tw, MyText } from 'common-ui'; import { tw, MyText } from 'common-ui';
import { useNavigationTarget } from 'common-ui/hooks/useNavigationTarget'; import { useNavigationTarget } from 'common-ui/hooks/useNavigationTarget';
@ -8,7 +8,6 @@ import dayjs from 'dayjs';
import { trpc } from '@/src/trpc-client'; import { trpc } from '@/src/trpc-client';
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import { orderStatusManipulator } from '@/src/lib/string-manipulators'; import { orderStatusManipulator } from '@/src/lib/string-manipulators';
import { useGetEssentialConsts } from '@/src/api-hooks/essential-consts.api';
interface OrderItem { interface OrderItem {
productName: string; productName: string;
@ -48,15 +47,23 @@ export default function NextOrderGlimpse() {
pageSize: 50, pageSize: 50,
}); });
const { data: essentialConsts } = useGetEssentialConsts();
const allOrders: Order[] = ordersData?.data || []; const allOrders: Order[] = ordersData?.data || [];
const now = dayjs();
const upcomingOrders = allOrders.filter(order => { const upcomingOrders = allOrders.filter(order => {
if (order.orderStatus.toLowerCase() === 'cancelled') return false; if (order.orderStatus.toLowerCase() === 'cancelled') return false;
if (order.deliveryStatus.toLowerCase() === 'success') return false; if (order.deliveryStatus.toLowerCase() === 'success') return false;
return true; if (order.isFlashDelivery) {
return true;
}
if (order.deliveryDate) {
return dayjs(order.deliveryDate).isAfter(now);
}
return false;
}); });
upcomingOrders.sort((a, b) => { upcomingOrders.sort((a, b) => {
@ -170,19 +177,6 @@ export default function NextOrderGlimpse() {
<Ionicons name="chevron-forward" size={14} color="#D97706" style={tw`ml-auto`} /> <Ionicons name="chevron-forward" size={14} color="#D97706" style={tw`ml-auto`} />
</View> </View>
)} )}
{/* Support Mobile Number */}
{essentialConsts?.supportMobile && (
<TouchableOpacity
style={tw`flex-row items-center mt-3 pt-3 border-t border-amber-200`}
onPress={() => Linking.openURL(`tel:${essentialConsts.supportMobile}`)}
>
<MaterialIcons name="phone-in-talk" size={14} color="#D97706" style={tw`mr-1.5`} />
<MyText style={tw`text-xs text-amber-700 font-medium`}>
Need help? Call {essentialConsts.supportMobile}
</MyText>
</TouchableOpacity>
)}
</View> </View>
</TouchableOpacity> </TouchableOpacity>
); );

View file

@ -11,19 +11,6 @@ import { useGetEssentialConsts } from '@/src/api-hooks/essential-consts.api';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
const formatTimeRange = (deliveryTime: string) => {
const time = dayjs(deliveryTime);
const endTime = time.add(1, 'hour');
const startPeriod = time.format('A');
const endPeriod = endTime.format('A');
if (startPeriod === endPeriod) {
return `${time.format('h')}-${endTime.format('h')} ${startPeriod}`;
} else {
return `${time.format('h:mm')} ${startPeriod} - ${endTime.format('h:mm')} ${endPeriod}`;
}
};
export default function AddToCartDialog() { export default function AddToCartDialog() {
const router = useRouter(); const router = useRouter();
const { addedToCartProduct, clearAddedToCartProduct } = useCartStore(); const { addedToCartProduct, clearAddedToCartProduct } = useCartStore();
@ -171,7 +158,7 @@ export default function AddToCartDialog() {
<MaterialIcons name="local-shipping" size={20} color="#3B82F6" style={tw`mt-0.5`} /> <MaterialIcons name="local-shipping" size={20} color="#3B82F6" style={tw`mt-0.5`} />
<View style={tw`ml-3 flex-1`}> <View style={tw`ml-3 flex-1`}>
<MyText style={tw`text-gray-900 font-bold text-base`}> <MyText style={tw`text-gray-900 font-bold text-base`}>
{dayjs(slot.deliveryTime).format('ddd, DD MMM • ')}{formatTimeRange(slot.deliveryTime)} {dayjs(slot.deliveryTime).format('ddd, DD MMM • h:mm A')}
</MyText> </MyText>
</View> </View>
{selectedSlotId === slot.id ? ( {selectedSlotId === slot.id ? (

View file

@ -63,7 +63,7 @@ const isDevMode = Constants.executionEnvironment !== "standalone";
// const BASE_API_URL = API_URL; // const BASE_API_URL = API_URL;
// const BASE_API_URL = 'http://10.0.2.2:4000'; // const BASE_API_URL = 'http://10.0.2.2:4000';
// const BASE_API_URL = 'http://192.168.100.101:4000'; // const BASE_API_URL = 'http://192.168.100.101:4000';
// const BASE_API_URL = 'http://192.168.100.105:4000'; // const BASE_API_URL = 'http://192.168.1.3:4000';
let BASE_API_URL = "https://mf.freshyo.in"; let BASE_API_URL = "https://mf.freshyo.in";
// let BASE_API_URL = 'http://192.168.100.104:4000'; // let BASE_API_URL = 'http://192.168.100.104:4000';
// let BASE_API_URL = 'http://192.168.29.176:4000'; // let BASE_API_URL = 'http://192.168.29.176:4000';

View file

@ -200,7 +200,6 @@ interface ConfirmationDialogProps {
message?: string; message?: string;
confirmText?: string; confirmText?: string;
cancelText?: string; cancelText?: string;
isLoading?: boolean;
} }
export const ConfirmationDialog: React.FC<ConfirmationDialogProps> = (props) => { export const ConfirmationDialog: React.FC<ConfirmationDialogProps> = (props) => {
@ -212,8 +211,7 @@ export const ConfirmationDialog: React.FC<ConfirmationDialogProps> = (props) =>
title = "Are you sure?", title = "Are you sure?",
message = "Do you really want to proceed with this action?", message = "Do you really want to proceed with this action?",
confirmText = "Confirm", confirmText = "Confirm",
cancelText = "Cancel", cancelText = "Cancel"
isLoading = false,
} = props; } = props;
const [comment, setComment] = useState(''); const [comment, setComment] = useState('');
@ -255,15 +253,8 @@ export const ConfirmationDialog: React.FC<ConfirmationDialogProps> = (props) =>
)} )}
<View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: commentNeeded ? 16 : 0 }}> <View style={{ flexDirection: 'row', justifyContent: 'space-between', marginTop: commentNeeded ? 16 : 0 }}>
<MyButton textContent={cancelText} onPress={handleCancel} fillColor="gray1" textColor="white1" disabled={isLoading} /> <MyButton textContent={cancelText} onPress={handleCancel} fillColor="gray1" textColor="white1" />
<MyButton <MyButton textContent={confirmText} style={{ flexShrink: 0 }} onPress={handleConfirm} fillColor="red1" textColor="white1" />
textContent={isLoading ? "Processing..." : confirmText}
style={{ flexShrink: 0 }}
onPress={handleConfirm}
fillColor="red1"
textColor="white1"
disabled={isLoading}
/>
</View> </View>
</View> </View>
</BottomDialog> </BottomDialog>