Compare commits

..

2 commits

Author SHA1 Message Date
shafi54
1b62c6add4 enh 2026-07-31 22:59:34 +05:30
shafi54
588cbe00f3 enh 2026-07-31 22:08:00 +05:30
9 changed files with 14 additions and 1211 deletions

View file

@ -0,0 +1,4 @@
# Taste (Continuously Learned by [CommandCode][cmd])
[cmd]: https://commandcode.ai/

View file

@ -1,801 +0,0 @@
import React, { useState , useEffect } from 'react';
import { View, TouchableOpacity, Alert, TextInput, ActivityIndicator, Linking } from 'react-native';
import { AppContainer, MyText, tw, MyFlatList, BottomDialog, BottomDropdown, Checkbox, theme, MyTextInput } from 'common-ui';
import { trpc } from '@/src/trpc-client';
import { useRouter, useLocalSearchParams } from 'expo-router';
import dayjs from 'dayjs';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { Entypo } from '@expo/vector-icons';
import CancelOrderDialog from '@/components/CancelOrderDialog';
import { OrderOptionsMenu } from '@/components/OrderOptionsMenu';
import * as Location from 'expo-location';
const AdminNotesForm = ({ orderId, existingNotes, onClose, refetch }: { orderId: string; existingNotes?: string | null; onClose: () => void; refetch: () => void }) => {
const [notesText, setNotesText] = useState(existingNotes || '');
const updateNotesMutation = trpc.admin.order.updateNotes.useMutation();
return (
<View style={tw`p-4`}>
<MyText style={tw`text-lg font-bold mb-4`}>Admin Notes</MyText>
<TextInput
style={tw`border border-gray-300 rounded p-3 h-24 text-base`}
multiline
value={notesText}
onChangeText={setNotesText}
placeholder="Enter admin notes..."
/>
<View style={tw`flex-row mt-4`}>
<TouchableOpacity
style={tw`flex-1 bg-blue-500 p-3 rounded ml-2`}
onPress={() => {
updateNotesMutation.mutate(
{ orderId: parseInt(orderId), adminNotes: notesText },
{
onSuccess: () => {
onClose();
Alert.alert('Success', 'Notes updated successfully');
refetch();
},
onError: (error: any) => {
Alert.alert('Error', error.message || 'Failed to update notes');
},
}
);
}}
>
<MyText style={tw`text-center text-white font-semibold`}>Save</MyText>
</TouchableOpacity>
</View>
</View>
);
};
interface OrderType {
id: number;
orderId: string;
readableId: number;
customerName: string | null;
customerMobile?: string | null;
address: string;
addressId: number;
latitude: number | null;
longitude: number | null;
totalAmount: number;
deliveryCharge: number;
items: {
id?: number;
name: string;
quantity: number;
price: number;
amount: number;
unit: string;
isPackaged?: boolean;
isPackageVerified?: boolean;
productSize: number;
}[];
createdAt: string;
deliveryTime: string | null;
status: 'pending' | 'delivered' | 'cancelled';
isPackaged: boolean;
isDelivered: boolean;
isCod: boolean;
isFlashDelivery: boolean;
couponCode?: string;
couponDescription?: string;
discountAmount?: number;
adminNotes?: string | null;
userNotes?: string | null;
userNegativityScore?: number;
}
const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }) => {
const id = order.orderId;
const router = useRouter();
const [menuOpen, setMenuOpen] = useState(false);
const [itemsDialogOpen, setItemsDialogOpen] = useState(false);
const [notesDialogOpen, setNotesDialogOpen] = useState(false);
const [cancelDialogOpen, setCancelDialogOpen] = useState(false);
const [userNotesDialogOpen, setUserNotesDialogOpen] = useState(false);
const [adminNotesDialogOpen, setAdminNotesDialogOpen] = useState(false);
const [updatingItems, setUpdatingItems] = useState<Set<number>>(new Set());
const updatePackagedMutation = trpc.admin.order.updatePackaged.useMutation();
const updateDeliveredMutation = trpc.admin.order.updateDelivered.useMutation();
const updateItemPackagingMutation = trpc.admin.order.updateOrderItemPackaging.useMutation();
const handleOrderPress = () => {
router.push(`/manage-orders/order-details/${order.orderId}` as any);
};
const handleMenuOption = () => {
setMenuOpen(false);
router.push(`/manage-orders/order-details/${order.orderId}` as any);
};
const handleMarkPackaged = (isPackaged: boolean) => {
updatePackagedMutation.mutate(
{ orderId: order.orderId.toString(), isPackaged },
{
onSuccess: () => {
refetch();
},
}
);
};
const handleMarkDelivered = (isDelivered: boolean) => {
updateDeliveredMutation.mutate(
{ orderId: order.orderId.toString(), isDelivered },
{
onSuccess: () => {
refetch();
},
}
);
};
const handleItemPackagingToggle = (itemId: number, field: 'isPackaged' | 'isPackageVerified', value: boolean) => {
setUpdatingItems(prev => new Set(prev).add(itemId));
updateItemPackagingMutation.mutate(
{ orderItemId: itemId, [field]: value },
{
onSuccess: () => {
setUpdatingItems(prev => {
const newSet = new Set(prev);
newSet.delete(itemId);
return newSet;
});
refetch();
},
onError: (error: any) => {
setUpdatingItems(prev => {
const newSet = new Set(prev);
newSet.delete(itemId);
return newSet;
});
Alert.alert("Error", error.message || "Failed to update packaging status");
},
}
);
};
return (
<>
<TouchableOpacity
style={tw`bg-white mx-4 mb-4 rounded-xl shadow-sm border border-gray-100 overflow-hidden`}
onPress={handleOrderPress}
activeOpacity={0.9}
>
{/* Header Section */}
<View style={tw`p-4 border-b border-gray-100 bg-gray-50/50`}>
<View style={tw`flex-row justify-between items-start`}>
<View style={tw`flex-1`}>
<View style={tw`flex-row items-center mb-1`}>
<MyText style={tw`font-bold text-lg mr-2 ${order.status === 'cancelled' ? 'text-red-600' : (order.userNegativityScore && order.userNegativityScore > 0 ? 'text-yellow-600' : 'text-gray-900')}`}>
{order.customerName || order.customerMobile || 'Unknown Customer'}
</MyText>
<View style={tw`bg-gray-200 px-2 py-0.5 rounded mr-2`}>
<MyText style={tw`text-xs font-medium text-gray-600`}>#{order.readableId}</MyText>
</View>
{order.isFlashDelivery && (
<View style={tw`bg-amber-100 px-2 py-0.5 rounded-full border border-amber-200 flex-row items-center`}>
<MaterialIcons name="bolt" size={12} color="#D97706" />
<MyText style={tw`text-xs font-bold text-amber-700 ml-1`}>FLASH</MyText>
</View>
)}
</View>
<View style={tw`flex-row items-center`}>
<MaterialIcons name="access-time" size={12} color="#6B7280" />
<MyText style={tw`text-xs text-gray-500 ml-1`}>
{dayjs(order.createdAt).format('MMM D, h:mm A')}
</MyText>
{order.userNegativityScore && order.userNegativityScore > 0 && (
<View style={tw`flex-row items-center ml-2`}>
<MaterialIcons name="warning" size={14} color="#CA8A04" />
<MyText style={tw`text-xs text-yellow-600 font-semibold ml-1`}>Negative Customer</MyText>
</View>
)}
</View>
</View>
<TouchableOpacity
onPress={() => setMenuOpen(true)}
style={tw`p-2 -mr-2 -mt-2 rounded-full`}
>
<Entypo name="dots-three-vertical" size={16} color="#9CA3AF" />
</TouchableOpacity>
</View>
</View>
{/* Main Content */}
<View style={tw`p-4`}>
{/* Status Badges */}
<View style={tw`flex-row flex-wrap gap-4 mb-4`}>
{/* <View style={tw`px-2.5 py-1 rounded-full ${getStatusColor(order.status)}`}>
<MyText style={tw`text-xs font-semibold capitalize`}>{order.status}</MyText>
</View> */}
{/* {order.isCod && (
<View style={tw`px-2.5 py-1 rounded-full bg-blue-50 border border-blue-100`}>
<MyText style={tw`text-xs font-semibold text-blue-700`}>COD</MyText>
</View>
)} */}
<View style={tw`flex-row items-center gap-1`}>
<MyText style={tw`text-sm font-semibold text-gray-600`}>Packaged</MyText>
<Checkbox
checked={order.isPackaged}
// onPress={() => handleMarkPackaged(!order.isPackaged)}
onPress={() => {}}
size={18}
fillColor={theme.colors.gray500}
checkColor="#FFFFFF"
/>
</View>
<View style={tw`flex-row items-center gap-1`}>
<MyText style={tw`text-xs font-semibold text-gray-600`}>Delivered</MyText>
<Checkbox
checked={order.isDelivered}
onPress={() => handleMarkDelivered(!order.isDelivered)}
size={18}
fillColor="#10B981"
checkColor="#FFFFFF"
/>
</View>
{order.status === 'cancelled' && (
<View style={tw`border border-red-500 px-2 py-0.5 rounded-full`}>
<MyText style={tw`text-xs font-bold text-red-600`}>CANCELLED</MyText>
</View>
)}
</View>
{/* Delivery Info */}
<View style={tw`flex-row items-start mb-4 bg-blue-50/50 p-3 rounded-lg`}>
<MaterialIcons name="location-pin" size={18} color="#3B82F6" style={tw`mt-0.5`} />
<View style={tw`ml-2 flex-1`}>
<MyText style={tw`text-xs font-bold text-blue-800 mb-0.5 uppercase tracking-wide`}>Delivery Address</MyText>
<MyText style={tw`text-sm text-gray-700 leading-5`} numberOfLines={2}>
{order.address}
</MyText>
<View style={tw`flex-row items-center mt-2`}>
<MaterialIcons name="event" size={14} color="#6B7280" />
<MyText style={tw`text-xs text-gray-600 ml-1`}>
{order.isFlashDelivery ? "1 Hr Delivery:" : "Slot:"} {order.isFlashDelivery ? dayjs(order.createdAt).add(30, 'minutes').format('MMM D, h:mm A') : order.deliveryTime ? dayjs(order.deliveryTime).format("ddd, MMM D • h:mm A") : 'Not scheduled'}
</MyText>
</View>
{order.isFlashDelivery && (
<View style={tw`flex-row items-center mt-1 bg-amber-50 px-2 py-1 rounded`}>
<MaterialIcons name="bolt" size={12} color="#D97706" />
<MyText style={tw`text-xs text-amber-700 ml-1 font-medium`}>
1 Hour Delivery High Priority
</MyText>
</View>
)}
</View>
</View>
{/* Items Summary & Total */}
<View style={tw`mb-4`}>
<View style={tw`mb-2`}>
<View style={tw`flex-row justify-between items-center`}>
<TouchableOpacity
onPress={() => setItemsDialogOpen(true)}
style={tw`flex-row items-center py-2 px-3 bg-blue-50 rounded-lg flex-1 mr-3`}
>
<MaterialIcons name="shopping-cart" size={16} color="#3B82F6" />
<MyText style={tw`text-sm font-medium text-blue-700 ml-2`}>
{order.items.length} {order.items.length === 1 ? 'item' : 'items'}
</MyText>
{order.isFlashDelivery && (
<View style={tw`ml-2 bg-amber-100 px-1.5 py-0.5 rounded-full`}>
<MyText style={tw`text-xs font-bold text-amber-700`}></MyText>
</View>
)}
</TouchableOpacity>
<View style={tw`flex-row items-center`}>
<MyText style={tw`text-base font-semibold text-gray-900 mr-2`}>Total:</MyText>
<MyText style={tw`text-lg font-bold ${order.isFlashDelivery ? 'text-amber-700' : 'text-gray-900'}`}>{order.totalAmount}</MyText>
</View>
</View>
</View>
</View>
{/* Coupons */}
{order.couponCode && (
<View style={tw`mb-4`}>
<MyText style={tw`text-xs font-bold text-gray-500 mb-2 uppercase tracking-wide`}>Applied Coupons</MyText>
<View style={tw`bg-pink-50 border border-pink-200 rounded-lg p-3`}>
<MyText style={tw`text-sm text-pink-800 font-medium mb-1`}>
{order.couponCode}
</MyText>
{order.couponDescription && (
<MyText style={tw`text-xs text-pink-600 mb-2`}>
{order.couponDescription}
</MyText>
)}
{order.discountAmount && (
<MyText style={tw`text-sm font-bold text-pink-800`}>
Discount: {order.discountAmount}
</MyText>
)}
</View>
</View>
)}
{/* Notes Section */}
<View style={tw`flex-row gap-2`}>
{order.userNotes && (
<TouchableOpacity
style={tw`flex-row items-center p-3 bg-amber-50 rounded-lg flex-1`}
onPress={() => setUserNotesDialogOpen(true)}
>
<MaterialIcons name="note" size={18} color="#D97706" />
<MyText style={tw`text-amber-800 font-medium ml-2`}>
User Notes
</MyText>
</TouchableOpacity>
)}
{order.adminNotes && (
<TouchableOpacity
style={tw`flex-row items-center p-3 bg-blue-50 rounded-lg flex-1`}
onPress={() => setNotesDialogOpen(true)}
>
<MaterialIcons name="admin-panel-settings" size={18} color="#2563EB" />
<MyText style={tw`text-blue-800 font-medium ml-2`}>
Admin Notes
</MyText>
</TouchableOpacity>
)}
</View>
{/* Footer / Delivery Charge */}
{order.deliveryCharge > 0 && (
<View style={tw`pt-3 border-t border-gray-100`}>
<View style={tw`flex-row justify-between items-center`}>
<MyText style={tw`text-sm text-gray-500`}>Delivery Charge</MyText>
<MyText style={tw`text-sm text-gray-900`}>{order.deliveryCharge}</MyText>
</View>
</View>
)}
</View>
</TouchableOpacity>
<OrderOptionsMenu
open={menuOpen}
onClose={() => setMenuOpen(false)}
order={{
id: order.id,
readableId: order.readableId,
isPackaged: order.isPackaged,
isDelivered: order.isDelivered,
isFlashDelivery: order.isFlashDelivery,
address: order.address,
addressId: order.addressId,
adminNotes: order.adminNotes,
userNotes: order.userNotes,
latitude: order.latitude,
longitude: order.longitude,
status: order.status,
}}
onViewDetails={handleMenuOption}
onTogglePackaged={() => handleMarkPackaged(!order.isPackaged)}
onToggleDelivered={() => handleMarkDelivered(!order.isDelivered)}
onOpenAdminNotes={() => {
setMenuOpen(false);
setNotesDialogOpen(true);
}}
onCancelOrder={() => {
setMenuOpen(false);
setCancelDialogOpen(true);
}}
onAttachLocation={() => refetch()}
onWhatsApp={() => {}}
onDial={() => {}}
/>
<BottomDialog open={itemsDialogOpen} onClose={() => setItemsDialogOpen(false)}>
<View style={tw`py-6`}>
<View style={tw`flex-row items-center justify-between mb-4`}>
<MyText style={tw`text-lg font-bold text-gray-800`}>
Order Items
</MyText>
{order.isFlashDelivery && (
<View style={tw`bg-amber-100 px-2 py-1 rounded-full border border-amber-200 flex-row items-center`}>
<MaterialIcons name="bolt" size={14} color="#D97706" />
<MyText style={tw`text-xs font-bold text-amber-700 ml-1`}>FLASH</MyText>
</View>
)}
</View>
<MyText style={tw`text-sm text-gray-600 mb-6`}>
Total: {order.totalAmount}
</MyText>
{order.items.map((item, idx) => (
<View key={idx} style={tw`py-2 border-b border-gray-50 last:border-0`}>
<View style={tw`flex-row items-center`}>
<View style={tw`bg-gray-100 px-2 py-1 rounded items-center justify-center mr-2`}>
<MyText style={tw`text-xs font-bold text-gray-600`}>{item.quantity * item.productSize } {item.unit}</MyText>
</View>
<MyText style={tw`text-sm text-gray-800 flex-1`} numberOfLines={1} ellipsizeMode="tail">
{item.name.length > 30 ? `${item.name.substring(0, 30)}...` : item.name}
</MyText>
{item.isPackaged !== undefined && item.isPackageVerified !== undefined && (
<>
<View style={tw`flex-row items-center gap-1 mr-3`}>
<MyText style={tw`text-sm font-medium text-gray-600`}>pkg</MyText>
<Checkbox
checked={item.isPackaged}
onPress={() => handleItemPackagingToggle(item.id!, 'isPackaged', !item.isPackaged)}
size={18}
fillColor={updatingItems.has(item.id!) ? "#F59E0B" : "#10B981"}
checkColor="#FFFFFF"
/>
</View>
<View style={tw`flex-row items-center gap-1`}>
<MyText style={tw`text-sm font-medium text-gray-600`}>verf</MyText>
<Checkbox
checked={item.isPackageVerified}
onPress={() => handleItemPackagingToggle(item.id!, 'isPackageVerified', !item.isPackageVerified)}
size={18}
fillColor={updatingItems.has(item.id!) ? "#F59E0B" : "#10B981"}
checkColor="#FFFFFF"
/>
</View>
{updatingItems.has(item.id!) && (
<ActivityIndicator size="small" color="#F59E0B" style={tw`ml-1`} />
)}
</>
)}
</View>
</View>
))}
</View>
</BottomDialog>
<BottomDialog open={notesDialogOpen} onClose={() => setNotesDialogOpen(false)}>
<AdminNotesForm orderId={order.orderId} existingNotes={order.adminNotes} onClose={() => setNotesDialogOpen(false)} refetch={refetch} />
</BottomDialog>
<CancelOrderDialog
orderId={order.id}
open={cancelDialogOpen}
onClose={() => setCancelDialogOpen(false)}
onSuccess={refetch}
/>
<BottomDialog open={userNotesDialogOpen} onClose={() => setUserNotesDialogOpen(false)}>
<View style={tw`p-6`}>
<MyText style={tw`text-lg font-bold text-gray-800 mb-4`}>
User Notes
</MyText>
<View style={tw`bg-amber-50 p-4 rounded-lg border border-amber-200`}>
<MyText style={tw`text-sm text-amber-900 leading-5`}>
{order.userNotes}
</MyText>
</View>
</View>
</BottomDialog>
<BottomDialog open={adminNotesDialogOpen} onClose={() => setAdminNotesDialogOpen(false)}>
<View style={tw`p-6`}>
<MyText style={tw`text-lg font-bold text-gray-800 mb-4`}>
Admin Notes
</MyText>
<View style={tw`bg-blue-50 p-4 rounded-lg border border-blue-200`}>
<MyText style={tw`text-sm text-blue-900 leading-5`}>
{order.adminNotes}
</MyText>
</View>
</View>
</BottomDialog>
</>
);
};
export default function Orders() {
const router = useRouter();
const { filter } = useLocalSearchParams<{ filter?: string }>();
const [selectedSlot, setSelectedSlot] = useState<number | null>(null);
const [selectedSlotType, setSelectedSlotType] = useState<'slot' | 'flash' | null>(null);
const [packagedFilter, setPackagedFilter] = useState<'all' | 'packaged' | 'not_packaged'>('all');
const [packagedChecked, setPackagedChecked] = useState(false);
const [notPackagedChecked, setNotPackagedChecked] = useState(false);
const [deliveredFilter, setDeliveredFilter] = useState<'all' | 'delivered' | 'not_delivered'>('all');
const [deliveredChecked, setDeliveredChecked] = useState(false);
const [notDeliveredChecked, setNotDeliveredChecked] = useState(false);
const [cancellationFilter, setCancellationFilter] = useState<'all' | 'cancelled' | 'not_cancelled'>('all');
const [cancelledChecked, setCancelledChecked] = useState(false);
const [notCancelledChecked, setNotCancelledChecked] = useState(false);
const [flashDeliveryFilter, setFlashDeliveryFilter] = useState<'all' | 'flash' | 'regular'>('all');
const [flashChecked, setFlashChecked] = useState(false);
const [regularChecked, setRegularChecked] = useState(false);
const [filterDialogOpen, setFilterDialogOpen] = useState(false);
// Handle initial filter from URL params
useEffect(() => {
if (filter === 'flash') {
setSelectedSlotType('flash');
setFlashDeliveryFilter('flash');
setFlashChecked(true);
setRegularChecked(false);
}
}, [filter]);
const { data: slotsData } = trpc.admin.slots.getAll.useQuery();
const { data, isLoading, isFetchingNextPage, fetchNextPage, hasNextPage, refetch } = trpc.admin.order.getAll.useInfiniteQuery(
{
limit: 20,
slotId: selectedSlotType === 'slot' ? selectedSlot : null,
packagedFilter,
deliveredFilter,
cancellationFilter,
flashDeliveryFilter: selectedSlotType === 'flash' ? 'flash' : flashDeliveryFilter
},
{
getNextPageParam: (lastPage) => lastPage?.nextCursor,
}
);
const orders = data?.pages.flatMap(page => page?.orders) || [];
if (isLoading) {
return (
<View style={tw`flex-1 justify-center items-center bg-white`}>
<ActivityIndicator size="large" color="#3B82F6" />
<MyText style={tw`text-gray-600 mt-4`}>Loading orders...</MyText>
</View>
);
}
const slotOptions = [
{ label: '⚡ Flash Deliveries', value: 'flash' },
...(slotsData?.slots?.map(slot => ({
label: dayjs(slot.deliveryTime).format('ddd DD MMM, h:mm a'),
value: slot.id.toString(),
})) || [])
];
return (
<>
<MyFlatList
data={orders}
keyExtractor={(item) => item!.orderId}
renderItem={({ item }) => item ? <OrderItem order={item} refetch={refetch} /> : null}
onEndReached={() => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}}
onEndReachedThreshold={0.5}
onRefresh={() => refetch()}
ListHeaderComponent={
<>
<View style={tw`flex-row justify-between items-center p-4 bg-white`}>
<View style={tw`flex-1 mr-4`}>
<BottomDropdown
label="Select Slot"
options={slotOptions}
value={selectedSlotType === 'flash' ? 'flash' : (selectedSlot?.toString() || '')}
onValueChange={(val) => {
if (val === 'flash') {
setSelectedSlotType('flash');
setSelectedSlot(null);
setFlashDeliveryFilter('flash');
// Reset other filters when switching to flash
setPackagedFilter('all');
setPackagedChecked(false);
setNotPackagedChecked(false);
setDeliveredFilter('all');
setDeliveredChecked(false);
setNotDeliveredChecked(false);
setCancellationFilter('all');
setCancelledChecked(false);
setNotCancelledChecked(false);
} else {
setSelectedSlotType('slot');
setSelectedSlot(val ? Number(val) : null);
setFlashDeliveryFilter('all');
}
}}
placeholder="All slots"
/>
</View>
<TouchableOpacity
onPress={() => setFilterDialogOpen(true)}
style={tw`p-2`}
>
<MaterialIcons name="filter-list" size={24} color="#6b7280" />
</TouchableOpacity>
</View>
{!isLoading && selectedSlotType && (
<View style={tw`bg-gray-50 p-3 border-b border-gray-200`}>
<MyText style={tw`text-center text-gray-600`}>
{selectedSlotType === 'flash'
? `${orders.length} Flash delivery orders`
: `${orders.length} Orders in slot`
}
</MyText>
</View>
)}
</>
}
ListFooterComponent={
isFetchingNextPage ? (
<View style={tw`py-4 items-center flex-row justify-center`}>
<ActivityIndicator size="small" color="#3B82F6" />
<MyText style={tw`text-gray-600 ml-2`}>Loading more...</MyText>
</View>
) : null
}
/>
<BottomDialog open={filterDialogOpen} onClose={() => setFilterDialogOpen(false)}>
<AppContainer>
<View style={tw`mt-4`}>
<MyText style={tw`text-lg font-semibold mb-2`}>Packaged Status</MyText>
<View style={tw`flex-row items-center mb-2`}>
<Checkbox
checked={packagedChecked}
onPress={() => {
const newValue = !packagedChecked;
setPackagedChecked(newValue);
if (newValue && notPackagedChecked) {
setPackagedFilter('all');
} else if (newValue) {
setPackagedFilter('packaged');
} else if (notPackagedChecked) {
setPackagedFilter('not_packaged');
} else {
setPackagedFilter('all');
}
}}
/>
<MyText style={tw`ml-2`}>Packaged</MyText>
</View>
<View style={tw`flex-row items-center`}>
<Checkbox
checked={notPackagedChecked}
onPress={() => {
const newValue = !notPackagedChecked;
setNotPackagedChecked(newValue);
if (packagedChecked && newValue) {
setPackagedFilter('all');
} else if (newValue) {
setPackagedFilter('not_packaged');
} else if (packagedChecked) {
setPackagedFilter('packaged');
} else {
setPackagedFilter('all');
}
}}
/>
<MyText style={tw`ml-2`}>Not Packaged</MyText>
</View>
</View>
<View style={tw`mt-6`}>
<MyText style={tw`text-lg font-semibold mb-2`}>Delivered Status</MyText>
<View style={tw`flex-row items-center mb-2`}>
<Checkbox
checked={deliveredChecked}
onPress={() => {
const newValue = !deliveredChecked;
setDeliveredChecked(newValue);
if (newValue && notDeliveredChecked) {
setDeliveredFilter('all');
} else if (newValue) {
setDeliveredFilter('delivered');
} else if (notDeliveredChecked) {
setDeliveredFilter('not_delivered');
} else {
setDeliveredFilter('all');
}
}}
/>
<MyText style={tw`ml-2`}>Delivered</MyText>
</View>
<View style={tw`flex-row items-center`}>
<Checkbox
checked={notDeliveredChecked}
onPress={() => {
const newValue = !notDeliveredChecked;
setNotDeliveredChecked(newValue);
if (deliveredChecked && newValue) {
setDeliveredFilter('all');
} else if (newValue) {
setDeliveredFilter('not_delivered');
} else if (deliveredChecked) {
setDeliveredFilter('delivered');
} else {
setDeliveredFilter('all');
}
}}
/>
<MyText style={tw`ml-2`}>Not Delivered</MyText>
</View>
</View>
<View style={tw`mt-6`}>
<MyText style={tw`text-lg font-semibold mb-2`}>Cancellation Status</MyText>
<View style={tw`flex-row items-center mb-2`}>
<Checkbox
checked={cancelledChecked}
onPress={() => {
const newValue = !cancelledChecked;
setCancelledChecked(newValue);
if (newValue && notCancelledChecked) {
setCancellationFilter('all');
} else if (newValue) {
setCancellationFilter('cancelled');
} else if (notCancelledChecked) {
setCancellationFilter('not_cancelled');
} else {
setCancellationFilter('all');
}
}}
/>
<MyText style={tw`ml-2`}>Cancelled</MyText>
</View>
<View style={tw`flex-row items-center`}>
<Checkbox
checked={notCancelledChecked}
onPress={() => {
const newValue = !notCancelledChecked;
setNotCancelledChecked(newValue);
if (cancelledChecked && newValue) {
setCancellationFilter('all');
} else if (newValue) {
setCancellationFilter('not_cancelled');
} else if (cancelledChecked) {
setCancellationFilter('cancelled');
} else {
setCancellationFilter('all');
}
}}
/>
<MyText style={tw`ml-2`}>Not Cancelled</MyText>
</View>
</View>
<View style={tw`mt-6`}>
<MyText style={tw`text-lg font-semibold mb-2`}>Delivery Type</MyText>
<View style={tw`flex-row items-center mb-2`}>
<Checkbox
checked={flashChecked}
onPress={() => {
const newValue = !flashChecked;
setFlashChecked(newValue);
if (newValue && regularChecked) {
setFlashDeliveryFilter('all');
} else if (newValue) {
setFlashDeliveryFilter('flash');
} else if (regularChecked) {
setFlashDeliveryFilter('regular');
} else {
setFlashDeliveryFilter('all');
}
}}
/>
<MyText style={tw`ml-2`}> 1 Hr Delivery</MyText>
</View>
<View style={tw`flex-row items-center`}>
<Checkbox
checked={regularChecked}
onPress={() => {
const newValue = !regularChecked;
setRegularChecked(newValue);
if (flashChecked && newValue) {
setFlashDeliveryFilter('all');
} else if (newValue) {
setFlashDeliveryFilter('regular');
} else if (flashChecked) {
setFlashDeliveryFilter('flash');
} else {
setFlashDeliveryFilter('all');
}
}}
/>
<MyText style={tw`ml-2`}>Regular Delivery</MyText>
</View>
</View>
</AppContainer>
</BottomDialog>
</>
);
}

View file

@ -1,82 +0,0 @@
import React from 'react';
import { Alert } from 'react-native';
import { AppContainer, ImageUploaderNeoPayload } from 'common-ui';
import ProductForm from '@/src/components/ProductForm';
import { trpc } from '@/src/trpc-client';
import { useUploadToObjectStorage } from '@/hooks/useUploadToObjectStore';
export default function AddProduct() {
const createProduct = trpc.admin.product.createProduct.useMutation();
const { refetch: refetchProducts } = trpc.admin.product.getProducts.useQuery(undefined, {
enabled: false,
});
const { upload, isUploading } = useUploadToObjectStorage();
const handleSubmit = async (values: any, images: ImageUploaderNeoPayload[]) => {
try {
let uploadUrls: string[] = [];
if (images.length > 0) {
const blobs = await Promise.all(
images.map(async (img) => {
const response = await fetch(img.url);
const blob = await response.blob();
return { blob, mimeType: img.mimeType || 'image/jpeg' };
})
);
const result = await upload({ images: blobs, contextString: 'product_info' });
uploadUrls = result.presignedUrls;
}
await createProduct.mutateAsync({
name: values.name,
shortDescription: values.shortDescription,
longDescription: values.longDescription,
unitId: parseInt(values.unitId),
storeId: parseInt(values.storeId),
price: parseFloat(values.price),
marketPrice: values.marketPrice ? parseFloat(values.marketPrice) : undefined,
incrementStep: 1,
productQuantity: values.productQuantity || 1,
isSuspended: values.isSuspended || false,
isFlashAvailable: values.isFlashAvailable || false,
flashPrice: values.flashPrice ? parseFloat(values.flashPrice) : undefined,
uploadUrls,
tagIds: values.tagIds || [],
});
await refetchProducts();
Alert.alert('Success', 'Product created successfully!');
} catch (error: any) {
Alert.alert('Error', error.message || 'Failed to create product');
}
};
const initialValues = {
name: '',
shortDescription: '',
longDescription: '',
unitId: 0,
price: '',
storeId: 1,
marketPrice: '',
deals: [{ quantity: '', price: '', validTill: new Date() }],
tagIds: [],
isSuspended: false,
isFlashAvailable: false,
flashPrice: '',
productQuantity: 1,
};
return (
<AppContainer>
<ProductForm
mode="create"
initialValues={initialValues}
onSubmit={handleSubmit}
isLoading={createProduct.isPending || isUploading}
/>
</AppContainer>
);
}

View file

@ -1,262 +0,0 @@
import React, { useState, useEffect, useCallback, forwardRef, useImperativeHandle, useMemo } from 'react';
import { View, TouchableOpacity } from 'react-native';
import { Formik, FieldArray } from 'formik';
import * as Yup from 'yup';
import { MyTextInput, BottomDropdown, MyText, useTheme, DatePicker, tw, useFocusCallback, Checkbox, ImageUploaderNeo, ImageUploaderNeoItem, ImageUploaderNeoPayload } from 'common-ui';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { trpc } from '../trpc-client';
interface ProductFormData {
name: string;
shortDescription: string;
longDescription: string;
unitId: number;
storeId: number;
price: string;
marketPrice: string;
isSuspended: boolean;
isFlashAvailable: boolean;
flashPrice: string;
deals: Deal[];
tagIds: number[];
productQuantity: number;
}
interface Deal {
quantity: string;
price: string;
validTill: Date | null;
}
export interface ProductFormRef {
clearImages: () => void;
}
interface ProductFormProps {
mode: 'create' | 'edit';
initialValues: ProductFormData;
onSubmit: (values: ProductFormData, images: ImageUploaderNeoPayload[], imagesToDelete: string[]) => void;
isLoading: boolean;
existingImages?: ImageUploaderNeoItem[];
existingImageKeys?: string[];
}
const unitOptions = [
{ label: 'Kg', value: 1 },
{ label: 'Litre', value: 2 },
{ label: 'Dozen', value: 3 },
{ label: 'Unit Piece', value: 4 },
];
const ProductForm = forwardRef<ProductFormRef, ProductFormProps>(({
mode,
initialValues,
onSubmit,
isLoading,
existingImages:existingImagesRaw,
existingImageKeys = [],
}, ref) => {
const { theme } = useTheme();
const [images, setImages] = useState<ImageUploaderNeoItem[]>([]);
const existingImages = existingImagesRaw || []
// Sync images state when existingImages prop changes (e.g., when async query data arrives)
useEffect(() => {
setImages(existingImages);
}, [existingImagesRaw]);
const { data: storesData } = trpc.common.getStoresSummary.useQuery();
const storeOptions = storesData?.stores.map(store => ({
label: store.name,
value: store.id,
})) || [];
const { data: tagsData } = trpc.admin.product.getProductTags.useQuery();
const tagOptions = tagsData?.tags.map(tag => ({
label: tag.tagName,
value: tag.id.toString(),
})) || [];
// Build signed URL -> S3 key mapping for existing images
const signedUrlToKey = useMemo(() => {
const map: Record<string, string> = {};
existingImages.forEach((img, i) => {
if (existingImageKeys[i]) {
map[img.imgUrl] = existingImageKeys[i];
}
});
return map;
}, [existingImages, existingImageKeys]);
return (
<Formik
initialValues={initialValues}
onSubmit={(values) => {
// New images have mimeType set, existing images have mimeType === null
const newImages = images.filter(img => img.mimeType !== null);
const deletedImageKeys = existingImages
.filter(existing => !images.some(current => current.imgUrl === existing.imgUrl))
.map(deleted => signedUrlToKey[deleted.imgUrl])
.filter(Boolean);
onSubmit(
values,
newImages.map(img => ({ url: img.imgUrl, mimeType: img.mimeType })),
deletedImageKeys,
);
}}
enableReinitialize
>
{({ handleChange, handleSubmit, values, setFieldValue, resetForm }) => {
const clearForm = useCallback(() => {
setImages([]);
resetForm();
}, [resetForm]);
useFocusCallback(clearForm);
useImperativeHandle(ref, () => ({
clearImages: clearForm,
}), [clearForm]);
const submit = () => handleSubmit();
return (
<View>
<MyTextInput
topLabel="Product Name"
placeholder="Enter product name"
value={values.name}
onChangeText={handleChange('name')}
style={{ marginBottom: 16 }}
/>
<MyTextInput
topLabel="Short Description"
placeholder="Enter short description"
multiline
numberOfLines={2}
value={values.shortDescription}
onChangeText={handleChange('shortDescription')}
style={{ marginBottom: 16 }}
/>
<MyTextInput
topLabel="Long Description"
placeholder="Enter detailed description"
multiline
numberOfLines={4}
value={values.longDescription}
onChangeText={handleChange('longDescription')}
style={{ marginBottom: 16 }}
/>
<ImageUploaderNeo
images={images}
onImageAdd={(payloads) => setImages(prev => [...prev, ...payloads.map(p => ({ imgUrl: p.url, mimeType: p.mimeType }))])}
onImageRemove={(payload) => setImages(prev => prev.filter(img => img.imgUrl !== payload.url))}
allowMultiple={true}
/>
<BottomDropdown
topLabel='Unit'
label="Unit"
value={values.unitId}
options={unitOptions}
onValueChange={(value) => setFieldValue('unitId', value)}
placeholder="Select unit"
style={{ marginBottom: 16 }}
/>
<MyTextInput
topLabel="Product Quantity"
placeholder="Enter product quantity"
keyboardType="numeric"
value={values.productQuantity.toString()}
onChangeText={(text) => setFieldValue('productQuantity', text)}
style={{ marginBottom: 16 }}
/>
<BottomDropdown
topLabel="Store"
label="Store"
value={values.storeId}
options={storeOptions}
onValueChange={(value) => setFieldValue('storeId', value)}
placeholder="Select store"
style={{ marginBottom: 16 }}
/>
<BottomDropdown
topLabel="Tags"
label="Tags"
value={values.tagIds.map(id => id.toString())}
options={tagOptions}
onValueChange={(value) => setFieldValue('tagIds', (value as string[]).map(id => parseInt(id)))}
multiple={true}
placeholder="Select tags"
style={{ marginBottom: 16 }}
/>
<MyTextInput
topLabel="Unit Price"
placeholder="Enter unit price"
keyboardType="numeric"
value={values.price}
onChangeText={handleChange('price')}
style={{ marginBottom: 16 }}
/>
<MyTextInput
topLabel="Market Price (Optional)"
placeholder="Enter market price"
keyboardType="numeric"
value={values.marketPrice}
onChangeText={handleChange('marketPrice')}
style={{ marginBottom: 16 }}
/>
<View style={tw`flex-row items-center mb-4`}>
<Checkbox
checked={values.isSuspended}
onPress={() => setFieldValue('isSuspended', !values.isSuspended)}
style={tw`mr-3`}
/>
<MyText style={tw`text-gray-700 font-medium`}>Suspend Product</MyText>
</View>
<View style={tw`flex-row items-center mb-4`}>
<Checkbox
checked={values.isFlashAvailable}
onPress={() => {
setFieldValue('isFlashAvailable', !values.isFlashAvailable);
if (values.isFlashAvailable) setFieldValue('flashPrice', '');
}}
style={tw`mr-3`}
/>
<MyText style={tw`text-gray-700 font-medium`}>Flash Available</MyText>
</View>
{values.isFlashAvailable && (
<MyTextInput
topLabel="Flash Price"
placeholder="Enter flash price"
keyboardType="numeric"
value={values.flashPrice}
onChangeText={handleChange('flashPrice')}
style={{ marginBottom: 16 }}
/>
)}
<TouchableOpacity
onPress={submit}
disabled={isLoading}
style={tw`px-4 py-2 rounded-lg shadow-lg items-center mt-2 ${isLoading ? 'bg-gray-400' : 'bg-blue-500'}`}
>
<MyText style={tw`text-white text-lg font-bold`}>
{isLoading ? (mode === 'create' ? 'Creating...' : 'Updating...') : (mode === 'create' ? 'Create Product' : 'Update Product')}
</MyText>
</TouchableOpacity>
</View>
);
}}
</Formik>
);
});
ProductForm.displayName = 'ProductForm';
export default ProductForm;

View file

@ -73,7 +73,6 @@ export {
updateSkuDeals, updateSkuDeals,
replaceProductTags, replaceProductTags,
mergeSkus, mergeSkus,
toggleSkuOutOfStock,
updateSlotProducts, updateSlotProducts,
getSlotProductIds, getSlotProductIds,
getSlotsProductIds, getSlotsProductIds,

View file

@ -7,7 +7,6 @@ import {
getAllProducts as getAllProductsInDb, getAllProducts as getAllProductsInDb,
getProductById as getProductByIdInDb, getProductById as getProductByIdInDb,
deleteProduct as deleteProductInDb, deleteProduct as deleteProductInDb,
toggleProductOutOfStock as toggleProductOutOfStockInDb,
updateSlotProducts as updateSlotProductsInDb, updateSlotProducts as updateSlotProductsInDb,
getSlotProductIds as getSlotProductIdsInDb, getSlotProductIds as getSlotProductIdsInDb,
getSlotsProductIds as getSlotsProductIdsInDb, getSlotsProductIds as getSlotsProductIdsInDb,
@ -21,11 +20,9 @@ import {
checkProductExistsByName, checkProductExistsByName,
checkUnitExists, checkUnitExists,
createProduct as createProductInDb, createProduct as createProductInDb,
createSpecialDealsForProduct,
replaceProductTags, replaceProductTags,
getProductImagesById, getProductImagesById,
updateProduct as updateProductInDb, updateProduct as updateProductInDb,
updateProductDeals,
checkProductTagExistsByName, checkProductTagExistsByName,
createProductTag as createProductTagInDb, createProductTag as createProductTagInDb,
updateProductTag as updateProductTagInDb, updateProductTag as updateProductTagInDb,
@ -44,7 +41,6 @@ import type {
AdminProductListResponse, AdminProductListResponse,
AdminProductResponse, AdminProductResponse,
AdminDeleteProductResult, AdminDeleteProductResult,
AdminToggleOutOfStockResult,
AdminUpdateSlotProductsResult, AdminUpdateSlotProductsResult,
AdminSlotProductIdsResult, AdminSlotProductIdsResult,
AdminSlotsProductIdsResult, AdminSlotsProductIdsResult,
@ -187,46 +183,6 @@ export const productRouter = router({
} }
}), }),
toggleOutOfStock: protectedProcedure
.input(z.object({
id: z.number(),
}))
.mutation(async ({ input }): Promise<AdminToggleOutOfStockResult> => {
const { id } = input;
const updatedProduct = await toggleProductOutOfStockInDb(id)
/*
// Old implementation - direct DB queries:
const product = await db.query.productInfo.findFirst({
where: eq(productInfo.id, id),
});
if (!product) {
throw new ApiError("Product not found", 404);
}
const [updatedProduct] = await db
.update(productInfo)
.set({
isOutOfStock: !product.isOutOfStock,
})
.where(eq(productInfo.id, id))
.returning();
*/
if (!updatedProduct) {
throw new ApiError('Product not found', 404)
}
await scheduleStoreInitialization()
return {
product: updatedProduct,
message: `Product marked as ${updatedProduct.isOutOfStock ? 'out of stock' : 'in stock'}`,
}
}),
createProduct: protectedProcedure createProduct: protectedProcedure
.input(z.object({ .input(z.object({
name: z.string().min(1, 'Name is required'), name: z.string().min(1, 'Name is required'),
@ -386,16 +342,16 @@ export const productRouter = router({
updateSlotProducts: protectedProcedure updateSlotProducts: protectedProcedure
.input(z.object({ .input(z.object({
slotId: z.string(), slotId: z.string(),
productIds: z.array(z.string()), skuIds: z.array(z.string()),
})) }))
.mutation(async ({ input }): Promise<AdminUpdateSlotProductsResult> => { .mutation(async ({ input }): Promise<AdminUpdateSlotProductsResult> => {
const { slotId, productIds } = input; const { slotId, skuIds } = input;
if (!Array.isArray(productIds)) { if (!Array.isArray(skuIds)) {
throw new ApiError("productIds must be an array", 400); throw new ApiError("skuIds must be an array", 400);
} }
const result = await updateSlotProductsInDb(slotId, productIds) const result = await updateSlotProductsInDb(slotId, skuIds)
/* /*
// Old implementation - direct DB queries: // Old implementation - direct DB queries:
@ -460,7 +416,7 @@ export const productRouter = router({
.query(async ({ input }): Promise<AdminSlotProductIdsResult> => { .query(async ({ input }): Promise<AdminSlotProductIdsResult> => {
const { slotId } = input; const { slotId } = input;
const productIds = await getSlotProductIdsInDb(slotId) const skuIds = await getSlotProductIdsInDb(slotId)
/* /*
// Old implementation - direct DB queries: // Old implementation - direct DB queries:
@ -474,12 +430,7 @@ export const productRouter = router({
const productIds = associations.map(assoc => assoc.productId); const productIds = associations.map(assoc => assoc.productId);
return { return {
productIds, skuIds,
};
*/
return {
productIds,
} }
}), }),

View file

@ -83,7 +83,6 @@ export {
updateSkuDeals, updateSkuDeals,
replaceProductTags, replaceProductTags,
mergeSkus, mergeSkus,
toggleSkuOutOfStock,
updateSlotProducts, updateSlotProducts,
getSlotProductIds, getSlotProductIds,
getSlotsProductIds, getSlotsProductIds,

View file

@ -370,7 +370,6 @@ export async function updateProduct(id: number, input: any): Promise<AdminProduc
} }
} }
export async function toggleSkuOutOfStock(id: number): Promise<AdminSku | null> {
const product = await db.query.productSkus.findFirst({ const product = await db.query.productSkus.findFirst({
where: eq(productSkus.id, id), where: eq(productSkus.id, id),
}) })

View file

@ -466,7 +466,8 @@ export interface AdminSpecialDeal {
} }
export interface AdminProductWithDetails extends AdminProduct { export interface AdminProductWithDetails extends AdminProduct {
unit: AdminUnit; store: Store | null;
skus: AdminSku[];
deals: AdminSpecialDeal[]; deals: AdminSpecialDeal[];
tags: AdminProductTagInfo[]; tags: AdminProductTagInfo[];
} }
@ -484,11 +485,6 @@ export interface AdminDeleteProductResult {
message: string; message: string;
} }
export interface AdminToggleOutOfStockResult {
product: AdminProduct;
message: string;
}
export interface AdminUpdateSlotProductsResult { export interface AdminUpdateSlotProductsResult {
message: string; message: string;
added: number; added: number;
@ -496,7 +492,7 @@ export interface AdminUpdateSlotProductsResult {
} }
export interface AdminSlotProductIdsResult { export interface AdminSlotProductIdsResult {
productIds: number[]; skuIds: number[];
} }
export type AdminSlotsProductIdsResult = Record<number, number[]>; export type AdminSlotsProductIdsResult = Record<number, number[]>;