Compare commits

..

No commits in common. "3d7e02396509d957b516a3c145e2d263d8f12272" and "31f011ba8c4645a093fbcad158a1951096746cc0" have entirely different histories.

25 changed files with 72 additions and 190 deletions

File diff suppressed because one or more lines are too long

View file

@ -201,7 +201,9 @@ export default function Layout() {
<Drawer.Screen name="dashboard" options={{ title: "Dashboard" }} />
<Drawer.Screen name="products" options={{ title: "Products" }} />
<Drawer.Screen name="prices-overview" options={{ title: "Prices Overview" }} />
<Drawer.Screen name="product-groupings" options={{ title: "Product Groupings" }} />
<Drawer.Screen name="product-groupings" options={{ title: "Product Groupings" }} />
<Drawer.Screen name="create-product-group" options={{ title: "Create Product Group" }} />
<Drawer.Screen name="edit-product-group/[id]" options={{ title: "Edit Product Group" }} />
<Drawer.Screen
@ -211,12 +213,14 @@ export default function Layout() {
<Drawer.Screen name="complaints" options={{ title: "Complaints" }} />
<Drawer.Screen name="coupons" options={{ title: "Coupons" }} />
<Drawer.Screen name="slots" options={{ title: "Slots" }} />
<Drawer.Screen name="vendor-snippets" options={{ title: "Vendor Snippets" }} />
<Drawer.Screen name="stores" options={{ title: "Stores" }} />
<Drawer.Screen name="vendor-snippets" options={{ title: "Vendor Snippets" }} />
<Drawer.Screen name="delivery-sequences" options={{ title: "Delivery Sequences", headerShown: false }} />
<Drawer.Screen name="stores" options={{ title: "Stores" }} />
<Drawer.Screen name="address-management" options={{ title: "Address Management" }} />
<Drawer.Screen name="product-tags" options={{ title: "Product Tags" }} />
<Drawer.Screen name="order-details/[id]" options={{ title: "Order Details" }} />
<Drawer.Screen name="rebalance-orders" options={{ title: "Rebalance Orders" }} />
<Drawer.Screen name="product-tags" options={{ title: "Product Tags" }} />
<Drawer.Screen name="order-details/[id]" options={{ title: "Order Details" }} />
<Drawer.Screen name="orders" options={{ title: "Orders" }} />
<Drawer.Screen name="rebalance-orders" options={{ title: "Rebalance Orders" }} />
</Drawer>
);
}

View file

@ -1,7 +1,7 @@
import React from 'react';
import { View } from 'react-native';
import { AppContainer } from 'common-ui';
import ProductGroupForm from '../../../components/ProductGroupForm';
import ProductGroupForm from '../../components/ProductGroupForm';
import { useRouter } from 'expo-router';
export default function CreateProductGroup() {

View file

@ -4,8 +4,8 @@ export default function Layout() {
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" options={{ title: 'Dashboard Banners' }} />
<Stack.Screen name="create" options={{ title: 'Create Banner' }} />
<Stack.Screen name="edit/[id]" options={{ title: 'Edit Banner' }} />
<Stack.Screen name="create-banner" options={{ title: 'Create Banner' }} />
<Stack.Screen name="edit-banner" options={{ title: 'Edit Banner' }} />
</Stack>
);
}

View file

@ -0,0 +1,9 @@
import { Stack } from 'expo-router';
export default function Layout() {
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" options={{ title: 'Create Banner' }} />
</Stack>
);
}

View file

@ -5,7 +5,7 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { useRouter } from 'expo-router';
import { FormikHelpers } from 'formik';
import BannerForm, { BannerFormData } from '@/components/BannerForm';
import { trpc } from '@/src/trpc-client';
import { trpc } from '../../../../src/trpc-client';
export default function CreateBanner() {
const router = useRouter();

View file

@ -5,7 +5,7 @@ import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { useRouter, useLocalSearchParams } from 'expo-router';
import { FormikHelpers } from 'formik';
import BannerForm, { BannerFormData } from '@/components/BannerForm';
import { trpc } from '@/src/trpc-client';
import { trpc } from '../../../../src/trpc-client';
interface Banner {
id: number;

View file

@ -0,0 +1,9 @@
import { Stack } from 'expo-router';
export default function Layout() {
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="[id]" options={{ title: 'Edit Banner' }} />
</Stack>
);
}

View file

@ -180,7 +180,7 @@ export default function DashboardBanners() {
};
const handleEdit = (banner: Banner) => {
router.push(`/dashboard-banners/edit/${banner.id}` as any);
router.push(`/dashboard-banners/edit-banner/${banner.id}` as any);
};
const handleDelete = (id: number) => {
@ -207,7 +207,7 @@ export default function DashboardBanners() {
};
const handleCreate = () => {
router.push('/dashboard-banners/create' as any);
router.push('/(drawer)/dashboard-banners/create-banner' as any);
};
if (isLoading) {

View file

@ -6,7 +6,6 @@ import { MyText, tw } from 'common-ui';
import { LinearGradient } from 'expo-linear-gradient';
import { theme } from 'common-ui/src/theme';
import { trpc } from '@/src/trpc-client';
import { useNavigationTarget } from 'common-ui/hooks/useNavigationTarget';
interface MenuItem {
title: string;
@ -17,7 +16,6 @@ interface MenuItem {
iconColor?: string;
iconBg?: string;
badgeCount?: number;
onPress?: () => void;
}
interface MenuItemComponentProps {
@ -29,7 +27,8 @@ const MenuItemComponent: React.FC<MenuItemComponentProps> = ({ item, router }) =
return (
<Pressable
onPress={() => item.onPress ? item.onPress() : router.push(item.route as any)}
key={item.route}
onPress={() => router.push(item.route as any)}
style={({ pressed }) => [
tw`flex-row items-center p-4 bg-white border border-gray-100 rounded-xl mb-3 shadow-sm`,
pressed && tw`bg-gray-50`,
@ -56,41 +55,28 @@ const MenuItemComponent: React.FC<MenuItemComponentProps> = ({ item, router }) =
export default function Dashboard() {
const router = useRouter();
const { setNavigationTarget } = useNavigationTarget();
const { data: essentialsData } = trpc.admin.user.getEssentials.useQuery();
const handleManageOrdersPress = () => {
setNavigationTarget('/manage-orders/orders');
router.push('/(drawer)/manage-orders');
};
const handleDeliverySequencesPress = () => {
setNavigationTarget('/manage-orders/delivery-sequences');
router.push('/(drawer)/manage-orders');
};
const menuItems: MenuItem[] = [
{
title: 'Manage Orders',
icon: 'shopping-bag',
description: 'View and manage customer orders',
route: '/(drawer)/manage-orders',
route: '/(drawer)/orders',
category: 'orders',
iconColor: '#10B981',
iconBg: '#D1FAE5',
onPress: handleManageOrdersPress,
},
{
title: 'Delivery Sequences',
icon: 'alt-route',
description: 'Plan and optimize delivery routes',
route: '/manage-orders/delivery-sequences',
route: '/(drawer)/delivery-sequences',
category: 'orders',
iconColor: '#8B5CF6',
iconBg: '#EDE9FE',
onPress: handleDeliverySequencesPress,
},
{
title: 'Delivery Slots',
@ -225,8 +211,8 @@ export default function Dashboard() {
<View style={tw`flex-row flex-wrap gap-3`}>
{quickActions.map((item) => (
<Pressable
key={`quick-${item.route}`}
onPress={() => item.onPress ? item.onPress() : router.push(item.route as any)}
key={item.route}
onPress={() => router.push(item.route as any)}
style={({ pressed }) => [
tw`bg-white rounded-xl p-3 shadow-sm border border-gray-100 items-center`,
{ width: 'calc(25% - 9px)' },
@ -263,7 +249,7 @@ export default function Dashboard() {
</View>
<MyText style={tw`text-gray-700 font-bold text-base`}>{category.title}</MyText>
</View>
{categoryItems.map(item => <MenuItemComponent key={`menu-${item.route}`} item={item} router={router} />)}
{categoryItems.map(item => <MenuItemComponent key={item.route} item={item} router={router} />)}
</View>
);
})}

View file

@ -2,8 +2,8 @@ import { Stack } from 'expo-router';
export default function Layout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: 'Delivery Sequences', headerShown: false }} />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" options={{ title: 'Delivery Sequences' }} />
</Stack>
);
}

View file

@ -302,7 +302,7 @@ export default function DeliverySequences() {
// Auto-select first slot if no slotId provided
useEffect(() => {
if (!slotId && slotsData?.slots && slotsData.slots.length > 0) {
router.replace(`/manage-orders/delivery-sequences?slotId=${slotsData.slots[0].id}`);
router.replace(`/delivery-sequences?slotId=${slotsData.slots[0].id}`);
}
}, [slotId, slotsData, router]);
@ -505,7 +505,7 @@ export default function DeliverySequences() {
value={selectedSlotId || ""}
onValueChange={(val) => {
if (val) {
router.replace(`/manage-orders/delivery-sequences?slotId=${val}`);
router.replace(`/delivery-sequences?slotId=${val}`);
}
}}
placeholder="Select slot"

View file

@ -1,9 +1,9 @@
import React from 'react';
import { View } from 'react-native';
import { AppContainer, MyText } from 'common-ui';
import ProductGroupForm from '../../../../components/ProductGroupForm';
import ProductGroupForm from '../../../components/ProductGroupForm';
import { useRouter, useLocalSearchParams } from 'expo-router';
import { trpc } from '@/src/trpc-client';
import { trpc } from '../../../src/trpc-client';
export default function EditProductGroup() {
const router = useRouter();

View file

@ -4,8 +4,6 @@ export default function Layout() {
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" options={{ title: 'Manage Orders' }} />
<Stack.Screen name="delivery-sequences" options={{ title: 'Delivery Sequences' }} />
<Stack.Screen name="orders" options={{ title: 'Orders' }} />
</Stack>
);
}

View file

@ -1,28 +1,16 @@
import { View, TouchableOpacity, Alert } from 'react-native';
import { MyText, BottomDropdown, tw, MyFlatList, useMarkDataFetchers } from 'common-ui';
import { useRouter } from 'expo-router';
import { useFocusEffect } from '@react-navigation/native';
import dayjs from 'dayjs';
import { useState, useCallback } from 'react';
import { useState } from 'react';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { trpc } from '@/src/trpc-client';
import { useNavigationTarget } from 'common-ui/hooks/useNavigationTarget';
export default function ManageOrders() {
const router = useRouter();
const { getNavigationTarget } = useNavigationTarget();
const [selectedSlotId, setSelectedSlotId] = useState<string | null>(null);
const { data: slotsData, refetch } = trpc.admin.slots.getAll.useQuery();
useFocusEffect(
useCallback(() => {
const target = getNavigationTarget();
if (target) {
router.replace(target as any);
}
}, [router, getNavigationTarget])
);
useMarkDataFetchers(() => {
refetch();
});
@ -46,7 +34,7 @@ export default function ManageOrders() {
Alert.alert('Flash Deliveries', 'Flash deliveries do not have delivery sequences. Use the Orders menu to manage flash deliveries.');
return;
}
router.push(`/manage-orders/delivery-sequences?slotId=${selectedSlotId}`);
router.push(`/(drawer)/delivery-sequences?slotId=${selectedSlotId}`);
},
},
{
@ -55,9 +43,9 @@ export default function ManageOrders() {
color: 'bg-cyan-500',
onPress: () => {
if (selectedSlotId === 'flash') {
router.push('/manage-orders/orders?filter=flash');
router.push('/(drawer)/orders?filter=flash');
} else {
router.push('/manage-orders/orders');
router.push('/(drawer)/orders');
}
},
},

View file

@ -1,9 +0,0 @@
import { Stack } from 'expo-router';
export default function Layout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: 'Orders', headerShown: false }} />
</Stack>
);
}

View file

@ -0,0 +1,9 @@
import { Stack } from 'expo-router';
export default function Layout() {
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" options={{ title: 'Orders' }} />
</Stack>
);
}

View file

@ -1,7 +1,7 @@
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 { trpc } from '../../../src/trpc-client';
import { useRouter, useLocalSearchParams } from 'expo-router';
import dayjs from 'dayjs';
import MaterialIcons from '@expo/vector-icons/MaterialIcons';

View file

@ -4,8 +4,6 @@ export default function Layout() {
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="index" options={{ title: 'Product Groupings' }} />
<Stack.Screen name="create" options={{ title: 'Create Product Group' }} />
<Stack.Screen name="edit/[id]" options={{ title: 'Edit Product Group' }} />
</Stack>
);
}

View file

@ -143,11 +143,11 @@ export default function ProductGroupings() {
});
const handleCreate = () => {
router.push("/product-groupings/create");
router.push("/(drawer)/create-product-group");
};
const handleEdit = (group: ProductGroup) => {
router.push(`/product-groupings/edit/${group.id}`);
router.push(`/(drawer)/edit-product-group/${group.id}`);
};
const handleDelete = (id: number) => {

View file

@ -198,7 +198,7 @@ export default function SlotDetails() {
{/* FAB for Edit Slot */}
<MyTouchableOpacity
onPress={() => router.push(`/slots/edit/${slot.id}` as any)}
onPress={() => router.push(`/edit-slot/${slot.id}` as any)}
activeOpacity={0.95}
style={{ position: 'absolute', bottom: 32, right: 24, zIndex: 100 }}
>

View file

@ -1,6 +1,6 @@
import './notif-job';
import { initializeAllStores } from '../stores/store-initializer';
import { startOrderHandler, startCancellationHandler, publishOrder } from './post-order-handler';
import { startOrderHandler, publishOrder } from './post-order-handler';
import { deleteOrders } from './delete-orders';
/**
@ -9,7 +9,6 @@ import { deleteOrders } from './delete-orders';
* - Role Manager (fetches and caches all roles)
* - Const Store (syncs constants from DB to Redis)
* - Post Order Handler (Redis Pub/Sub subscriber)
* - Cancellation Handler (Redis Pub/Sub subscriber for order cancellations)
* - Other services can be added here in the future
*/
export const initFunc = async (): Promise<void> => {
@ -19,7 +18,6 @@ export const initFunc = async (): Promise<void> => {
await Promise.all([
initializeAllStores(),
startOrderHandler(),
startCancellationHandler(),
]);
console.log('Application initialization completed successfully');

View file

@ -1,23 +1,15 @@
import { db } from '../db/db_index';
import { orders, orderStatus } from '../db/schema';
import { orders } from '../db/schema';
import redisClient from './redis-client';
import { sendTelegramMessage } from './telegram-service';
import { inArray, eq } from 'drizzle-orm';
import { inArray } from 'drizzle-orm';
const ORDER_CHANNEL = 'orders:placed';
const CANCELLED_CHANNEL = 'orders:cancelled';
interface OrderIdMessage {
orderIds: number[];
}
interface CancellationMessage {
orderId: number;
cancelledBy: 'user' | 'admin';
reason: string;
cancelledAt: string;
}
const formatDateTime = (dateStr: string | null | undefined): string => {
if (!dateStr) return 'N/A';
return new Date(dateStr).toLocaleString('en-IN', {
@ -63,28 +55,6 @@ const formatOrderMessageWithFullData = (ordersData: any[]): string => {
return message;
};
const formatCancellationMessage = (orderData: any, cancellationData: CancellationMessage): string => {
const message = `❌ <b>Order Cancelled</b>
<b>Order #${orderData.id}</b>
👤 <b>Name:</b> ${orderData.address?.name || 'N/A'}
📞 <b>Phone:</b> ${orderData.address?.phone || 'N/A'}
📦 <b>Items:</b>
${orderData.orderItems?.map((item: any) => `${item.product?.name || 'Unknown'} x${item.quantity}`).join('\n') || ' N/A'}
💰 <b>Total:</b> ${orderData.totalAmount}
💳 <b>Refund:</b> ${orderData.refundStatus === 'na' ? 'N/A (COD)' : orderData.refundStatus || 'Pending'}
<b>Reason:</b> ${cancellationData.reason}
👤 <b>Cancelled by:</b> ${cancellationData.cancelledBy === 'admin' ? 'Admin' : 'User'}
<b>Time:</b> ${formatDateTime(cancellationData.cancelledAt)}
`;
return message;
};
/**
* Start the post order handler
* Subscribes to the orders:placed channel and sends to Telegram
@ -134,56 +104,6 @@ export const stopOrderHandler = async (): Promise<void> => {
}
};
export const startCancellationHandler = async (): Promise<void> => {
try {
console.log('Starting cancellation handler...');
await redisClient.subscribe(CANCELLED_CHANNEL, async (message: string) => {
try {
const cancellationData: CancellationMessage = JSON.parse(message);
console.log('Order cancellation received, sending to Telegram...');
const orderData = await db.query.orders.findFirst({
where: eq(orders.id, cancellationData.orderId),
with: {
address: true,
orderItems: { with: { product: true } },
refunds: true,
},
});
if (!orderData) {
console.error('Order not found for cancellation:', cancellationData.orderId);
await sendTelegramMessage(`⚠️ Order ${cancellationData.orderId} was cancelled but could not be found in database`);
return;
}
const refundStatus = orderData.refunds?.[0]?.refundStatus || 'pending';
const telegramMessage = formatCancellationMessage({ ...orderData, refundStatus }, cancellationData);
await sendTelegramMessage(telegramMessage);
} catch (error) {
console.error('Failed to process cancellation message:', error);
await sendTelegramMessage(`⚠️ Error processing cancellation: ${message}`);
}
});
console.log('Cancellation handler started successfully');
} catch (error) {
console.error('Failed to start cancellation handler:', error);
throw error;
}
};
export const stopCancellationHandler = async (): Promise<void> => {
try {
await redisClient.unsubscribe(CANCELLED_CHANNEL);
console.log('Cancellation handler stopped');
} catch (error) {
console.error('Error stopping cancellation handler:', error);
}
};
export const publishOrder = async (orderDetails: OrderIdMessage): Promise<boolean> => {
try {
const message = JSON.stringify(orderDetails);
@ -207,24 +127,3 @@ export const publishFormattedOrder = async (
return false;
}
};
export const publishCancellation = async (
orderId: number,
cancelledBy: 'user' | 'admin',
reason: string
): Promise<boolean> => {
try {
const message: CancellationMessage = {
orderId,
cancelledBy,
reason,
cancelledAt: new Date().toISOString(),
};
await redisClient.publish(CANCELLED_CHANNEL, JSON.stringify(message));
console.log('Cancellation published to Redis:', orderId);
return true;
} catch (error) {
console.error('Failed to publish cancellation:', error);
return false;
}
};

View file

@ -19,7 +19,6 @@ import {
sendOrderPackagedNotification,
sendOrderDeliveredNotification,
} from "../../lib/notif-job";
import { publishCancellation } from "../../lib/post-order-handler";
const updateOrderNotesSchema = z.object({
orderId: z.number(),
@ -956,9 +955,6 @@ export const orderRouter = router({
return { orderId: order.id, userId: order.userId };
});
// Publish to Redis for Telegram notification
await publishCancellation(result.orderId, 'admin', reason);
return { success: true, message: "Order cancelled successfully" };
}),
});

View file

@ -25,7 +25,7 @@ import {
import { RazorpayPaymentService } from "../../lib/payments-utils";
import { getNextDeliveryDate } from "../common-apis/common";
import { CONST_KEYS, getConstant, getConstants } from "../../lib/const-store";
import { publishFormattedOrder, publishCancellation } from "../../lib/post-order-handler";
import { publishFormattedOrder } from "../../lib/post-order-handler";
const validateAndGetCoupon = async (
@ -785,9 +785,6 @@ export const orderRouter = router({
result.orderId.toString()
);
// Publish to Redis for Telegram notification
await publishCancellation(result.orderId, 'user', reason);
return { success: true, message: "Order cancelled successfully" };
} catch (e) {
console.log(e);