enh
This commit is contained in:
parent
5b19a0486c
commit
ee0b71fcd3
8 changed files with 3830 additions and 12 deletions
File diff suppressed because one or more lines are too long
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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`}>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
7
apps/backend/drizzle/0072_flowery_deathbird.sql
Normal file
7
apps/backend/drizzle/0072_flowery_deathbird.sql
Normal 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
|
||||
);
|
||||
3685
apps/backend/drizzle/meta/0072_snapshot.json
Normal file
3685
apps/backend/drizzle/meta/0072_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue