This commit is contained in:
shafi54 2026-02-08 16:02:57 +05:30
parent 5b19a0486c
commit ee0b71fcd3
8 changed files with 3830 additions and 12 deletions

File diff suppressed because one or more lines are too long

View file

@ -11,6 +11,7 @@ import {
AppContainer,
MyText,
tw,
Checkbox,
} from 'common-ui';
import { trpc } from '@/src/trpc-client';
import { formatDistanceToNow } from 'date-fns';
@ -103,10 +104,20 @@ export default function UserDetails() {
{ enabled: !!userId }
);
const updateSuspension = trpc.admin.user.updateUserSuspension.useMutation({
onSuccess: () => {
refetch();
},
});
const handleOrderPress = useCallback((orderId: number) => {
router.push(`/(drawer)/order-details/${orderId}`);
}, [router]);
const handleSuspensionToggle = useCallback((isSuspended: boolean) => {
updateSuspension.mutate({ userId, isSuspended });
}, [userId, updateSuspension]);
if (isLoading) {
return (
<AppContainer>
@ -163,16 +174,23 @@ export default function UserDetails() {
<MaterialIcons name="person" size={24} color="#3B82F6" />
</View>
<View style={tw`flex-1`}>
<View style={tw`flex-row items-center`}>
<MyText style={tw`text-gray-900 font-bold text-lg mb-0.5`}>
{user.mobile || 'No Mobile'}
</MyText>
{user.isSuspended && (
<View style={tw`ml-2 px-2 py-0.5 bg-red-100 rounded-full border border-red-200`}>
<MyText style={tw`text-[10px] font-bold text-red-700 uppercase`}>Suspended</MyText>
</View>
)}
</View>
<MyText style={tw`text-gray-500`}>
{displayName}
</MyText>
</View>
</View>
<View style={tw`bg-gray-50 p-3 rounded-xl`}>
<View style={tw`bg-gray-50 p-3 rounded-xl mb-4`}>
<View style={tw`flex-row items-center`}>
<MaterialIcons name="access-time" size={18} color="#6B7280" />
<MyText style={tw`ml-2 text-gray-600`}>
@ -180,6 +198,27 @@ export default function UserDetails() {
</MyText>
</View>
</View>
{/* Suspension Toggle */}
<View style={tw`flex-row items-center justify-between pt-4 border-t border-gray-100`}>
<View style={tw`flex-1`}>
<MyText style={tw`text-gray-900 font-bold text-base mb-1`}>
Suspend User
</MyText>
<MyText style={tw`text-gray-500 text-sm`}>
Prevent user from placing orders
</MyText>
</View>
{updateSuspension.isPending ? (
<ActivityIndicator size="small" color="#3b82f6" />
) : (
<Checkbox
checked={user.isSuspended}
onPress={() => handleSuspensionToggle(!user.isSuspended)}
size={28}
/>
)}
</View>
</View>
{/* Orders Section */}

View file

@ -25,6 +25,7 @@ interface User {
createdAt: string;
totalOrders: number;
lastOrderDate: string | null;
isSuspended: boolean;
}
interface UserItemProps {
@ -56,9 +57,16 @@ const UserItem: React.FC<UserItemProps> = ({ user, index, onPress }) => {
{/* Middle: User Info */}
<View style={tw`flex-1`}>
{/* Mobile number - primary identifier */}
<MyText style={tw`text-gray-900 font-bold text-base mb-0.5`}>
<View style={tw`flex-row items-center mb-0.5`}>
<MyText style={tw`text-gray-900 font-bold text-base`}>
{user.mobile || 'No Mobile'}
</MyText>
{user.isSuspended && (
<View style={tw`ml-2 px-2 py-0.5 bg-red-100 rounded-full border border-red-200`}>
<MyText style={tw`text-[10px] font-bold text-red-700 uppercase`}>Suspended</MyText>
</View>
)}
</View>
{/* Name */}
<MyText style={tw`text-gray-500 text-sm mb-1`}>

View file

@ -1,7 +1,7 @@
ENV_MODE=PROD
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=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
PHONE_PE_BASE_URL=https://api-preprod.phonepe.com/
PHONE_PE_CLIENT_ID=TEST-M23F2IGP34ZAR_25090
PHONE_PE_CLIENT_VERSION=1

View file

@ -0,0 +1,7 @@
CREATE TABLE "mf"."user_notifications" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "mf"."user_notifications_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
"image_url" varchar(500),
"created_at" timestamp DEFAULT now() NOT NULL,
"body" text NOT NULL,
"applicable_users" jsonb
);

File diff suppressed because it is too large Load diff

View file

@ -505,6 +505,13 @@
"when": 1770321591876,
"tag": "0071_moaning_shadow_king",
"breakpoints": true
},
{
"idx": 72,
"version": "7",
"when": 1770546741428,
"tag": "0072_flowery_deathbird",
"breakpoints": true
}
]
}

View file

@ -1,7 +1,7 @@
import { protectedProcedure } from '../trpc-index';
import { z } from 'zod';
import { db } from '../../db/db_index';
import { users, complaints, orders, orderItems, notifCreds, userNotifications } from '../../db/schema';
import { users, complaints, orders, orderItems, notifCreds, userNotifications, userDetails } from '../../db/schema';
import { eq, sql, desc, asc, count, max } from 'drizzle-orm';
import { ApiError } from '../../lib/api-error';
@ -103,6 +103,7 @@ export const userRouter = {
let orderCounts: { userId: number; totalOrders: number }[] = [];
let lastOrders: { userId: number; lastOrderDate: Date | null }[] = [];
let suspensionStatuses: { userId: number; isSuspended: boolean }[] = [];
if (userIds.length > 0) {
// Get total orders per user
@ -124,17 +125,28 @@ export const userRouter = {
.from(orders)
.where(sql`${orders.userId} IN (${sql.join(userIds, sql`, `)})`)
.groupBy(orders.userId);
// Get suspension status for each user
suspensionStatuses = await db
.select({
userId: userDetails.userId,
isSuspended: userDetails.isSuspended,
})
.from(userDetails)
.where(sql`${userDetails.userId} IN (${sql.join(userIds, sql`, `)})`);
}
// Create lookup maps
const orderCountMap = new Map(orderCounts.map(o => [o.userId, o.totalOrders]));
const lastOrderMap = new Map(lastOrders.map(o => [o.userId, o.lastOrderDate]));
const suspensionMap = new Map(suspensionStatuses.map(s => [s.userId, s.isSuspended]));
// Combine data
const usersWithStats = usersToReturn.map(user => ({
...user,
totalOrders: orderCountMap.get(user.id) || 0,
lastOrderDate: lastOrderMap.get(user.id) || null,
isSuspended: suspensionMap.get(user.id) ?? false,
}));
// Get next cursor
@ -170,6 +182,15 @@ export const userRouter = {
throw new ApiError('User not found', 404);
}
// Get user suspension status
const userDetail = await db
.select({
isSuspended: userDetails.isSuspended,
})
.from(userDetails)
.where(eq(userDetails.userId, userId))
.limit(1);
// Get all orders for this user with order items count
const userOrders = await db
.select({
@ -237,11 +258,62 @@ export const userRouter = {
});
return {
user: user[0],
user: {
...user[0],
isSuspended: userDetail[0]?.isSuspended ?? false,
},
orders: ordersWithDetails,
};
}),
updateUserSuspension: protectedProcedure
.input(z.object({
userId: z.number(),
isSuspended: z.boolean(),
}))
.mutation(async ({ input }) => {
const { userId, isSuspended } = input;
// Check if user exists
const user = await db
.select({ id: users.id })
.from(users)
.where(eq(users.id, userId))
.limit(1);
if (!user || user.length === 0) {
throw new ApiError('User not found', 404);
}
// Check if user_details record exists
const existingDetail = await db
.select({ id: userDetails.id })
.from(userDetails)
.where(eq(userDetails.userId, userId))
.limit(1);
if (existingDetail.length > 0) {
// Update existing record
await db
.update(userDetails)
.set({ isSuspended })
.where(eq(userDetails.userId, userId));
} else {
// Insert new record
await db
.insert(userDetails)
.values({
userId,
isSuspended,
});
}
return {
success: true,
message: `User ${isSuspended ? 'suspended' : 'unsuspended'} successfully`,
};
}),
getUsersForNotification: protectedProcedure
.input(z.object({
search: z.string().optional(),