From 72475f7f714d57721ece2ce6ec24596f378f7214 Mon Sep 17 00:00:00 2001
From: shafi54 <108669266+shafi-aviz@users.noreply.github.com>
Date: Wed, 4 Mar 2026 23:36:11 +0530
Subject: [PATCH] enh
---
.../manage-orders/order-details/[id].tsx | 189 +---------------
.../(drawer)/manage-orders/orders/index.tsx | 12 +-
.../app/(drawer)/user-management/[id].tsx | 4 +
.../admin-ui/components/UserIncidentsView.tsx | 206 ++++++++++++++++++
apps/backend/src/lib/init.ts | 3 +
.../src/stores/user-negativity-store.ts | 118 ++++++++++
apps/backend/src/trpc/admin-apis/order.ts | 64 +++---
apps/backend/src/trpc/admin-apis/user.ts | 20 +-
8 files changed, 385 insertions(+), 231 deletions(-)
create mode 100644 apps/admin-ui/components/UserIncidentsView.tsx
create mode 100644 apps/backend/src/stores/user-negativity-store.ts
diff --git a/apps/admin-ui/app/(drawer)/manage-orders/order-details/[id].tsx b/apps/admin-ui/app/(drawer)/manage-orders/order-details/[id].tsx
index cf279af..cbd9009 100644
--- a/apps/admin-ui/app/(drawer)/manage-orders/order-details/[id].tsx
+++ b/apps/admin-ui/app/(drawer)/manage-orders/order-details/[id].tsx
@@ -13,194 +13,7 @@ import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import FontAwesome5 from "@expo/vector-icons/FontAwesome5";
import dayjs from "dayjs";
import CancelOrderDialog from "@/components/CancelOrderDialog";
-
-function UserIncidentDialog({ userId, orderId, open, onClose, onSuccess }: { userId: number; orderId: number; open: boolean; onClose: () => void; onSuccess?: () => void }) {
- const [adminComment, setAdminComment] = useState('');
- const [negativityScore, setNegativityScore] = useState('');
-
- const addIncidentMutation = trpc.admin.user.addUserIncident.useMutation({
- onSuccess: () => {
- Alert.alert('Success', 'Incident added successfully');
- setAdminComment('');
- setNegativityScore('');
- onClose();
- onSuccess?.();
- },
- onError: (error: any) => {
- Alert.alert('Error', error.message || 'Failed to add incident');
- },
- });
-
- const handleAddIncident = () => {
- const score = negativityScore ? parseInt(negativityScore) : undefined;
-
- if (!adminComment.trim() && !negativityScore) {
- Alert.alert('Error', 'Please enter a comment or negativity score');
- return;
- }
-
- addIncidentMutation.mutate({
- userId,
- orderId,
- adminComment: adminComment || undefined,
- negativityScore: score,
- });
- };
-
- return (
-
-
-
-
-
-
-
- Add User Incident
-
-
- Record an incident for this user. This will be visible in their profile.
-
-
-
-
-
-
-
-
-
-
- Higher negativity scores indicate more serious incidents (e.g., repeated cancellations, abusive behavior).
-
-
-
-
-
- Cancel
-
-
-
- {addIncidentMutation.isPending ? 'Adding...' : 'Add Incident'}
-
-
-
-
-
- );
-}
-
-function UserIncidentsView({ userId, orderId }: { userId: number; orderId: number }) {
- const [incidentDialogOpen, setIncidentDialogOpen] = useState(false);
-
- const { data: incidentsData, refetch: refetchIncidents } = trpc.admin.user.getUserIncidents.useQuery(
- { userId },
- { enabled: !!userId }
- );
-
- return (
- <>
-
-
-
- User Incidents
-
- setIncidentDialogOpen(true)}
- style={tw`flex-row items-center bg-amber-200 px-3 py-1.5 rounded-lg`}
- >
-
- Add Incident
-
-
-
- {incidentsData?.incidents && incidentsData.incidents.length > 0 ? (
-
- {incidentsData.incidents.map((incident: any, index: number) => (
-
-
-
-
-
- {dayjs(incident.dateAdded).format('MMM DD, YYYY • h:mm A')}
-
-
- {incident.negativityScore && (
-
-
- Score: {incident.negativityScore}
-
-
- )}
-
-
- {incident.adminComment && (
-
-
- {incident.adminComment}
-
-
- )}
-
-
-
-
- Added by {incident.addedBy}
-
- {incident.orderId && (
- <>
-
-
- Order #{incident.orderId}
-
- >
- )}
-
-
- ))}
-
- ) : (
-
-
-
- No incidents recorded for this user
-
-
- )}
-
-
- setIncidentDialogOpen(false)}
- onSuccess={refetchIncidents}
- />
- >
- );
-}
+import { UserIncidentsView } from "@/components/UserIncidentsView";
export default function OrderDetails() {
const { id } = useLocalSearchParams<{ id: string }>();
diff --git a/apps/admin-ui/app/(drawer)/manage-orders/orders/index.tsx b/apps/admin-ui/app/(drawer)/manage-orders/orders/index.tsx
index af6c94a..fd17855 100644
--- a/apps/admin-ui/app/(drawer)/manage-orders/orders/index.tsx
+++ b/apps/admin-ui/app/(drawer)/manage-orders/orders/index.tsx
@@ -56,6 +56,7 @@ interface OrderType {
orderId: string;
readableId: number;
customerName: string | null;
+ customerMobile?: string | null;
address: string;
addressId: number;
latitude: number | null;
@@ -85,6 +86,7 @@ interface OrderType {
discountAmount?: number;
adminNotes?: string | null;
userNotes?: string | null;
+ userNegativityScore?: number;
}
const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }) => {
@@ -171,8 +173,8 @@ const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }
-
- {order.customerName || 'Unknown Customer'}
+ 0 ? 'text-yellow-600' : 'text-gray-900')}`}>
+ {order.customerName || order.customerMobile || 'Unknown Customer'}
#{order.readableId}
@@ -189,6 +191,12 @@ const OrderItem = ({ order, refetch }: { order: OrderType; refetch: () => void }
{dayjs(order.createdAt).format('MMM D, h:mm A')}
+ {order.userNegativityScore && order.userNegativityScore > 0 && (
+
+
+ Negative Customer
+
+ )}
diff --git a/apps/admin-ui/app/(drawer)/user-management/[id].tsx b/apps/admin-ui/app/(drawer)/user-management/[id].tsx
index 03a7eef..d4a57ef 100644
--- a/apps/admin-ui/app/(drawer)/user-management/[id].tsx
+++ b/apps/admin-ui/app/(drawer)/user-management/[id].tsx
@@ -16,6 +16,7 @@ import {
import { trpc } from '@/src/trpc-client';
import { formatDistanceToNow } from 'date-fns';
import dayjs from 'dayjs';
+import { UserIncidentsView } from '@/components/UserIncidentsView';
interface Order {
id: number;
@@ -221,6 +222,9 @@ export default function UserDetails() {
+ {/* User Incidents Section */}
+
+
{/* Orders Section */}
diff --git a/apps/admin-ui/components/UserIncidentsView.tsx b/apps/admin-ui/components/UserIncidentsView.tsx
new file mode 100644
index 0000000..55c4e84
--- /dev/null
+++ b/apps/admin-ui/components/UserIncidentsView.tsx
@@ -0,0 +1,206 @@
+import React, { useState } from 'react';
+import { View, TouchableOpacity, Alert } from 'react-native';
+import { MyText, tw, BottomDialog, MyTextInput } from 'common-ui';
+import { trpc } from '@/src/trpc-client';
+import MaterialIcons from '@expo/vector-icons/MaterialIcons';
+import dayjs from 'dayjs';
+
+function UserIncidentDialog({
+ userId,
+ orderId,
+ open,
+ onClose,
+ onSuccess
+}: {
+ userId: number;
+ orderId: number | null;
+ open: boolean;
+ onClose: () => void;
+ onSuccess?: () => void
+}) {
+ const [adminComment, setAdminComment] = useState('');
+ const [negativityScore, setNegativityScore] = useState('');
+
+ const addIncidentMutation = trpc.admin.user.addUserIncident.useMutation({
+ onSuccess: () => {
+ Alert.alert('Success', 'Incident added successfully');
+ setAdminComment('');
+ setNegativityScore('');
+ onClose();
+ onSuccess?.();
+ },
+ onError: (error: any) => {
+ Alert.alert('Error', error.message || 'Failed to add incident');
+ },
+ });
+
+ const handleAddIncident = () => {
+ const score = negativityScore ? parseInt(negativityScore) : undefined;
+
+ if (!adminComment.trim() && !negativityScore) {
+ Alert.alert('Error', 'Please enter a comment or negativity score');
+ return;
+ }
+
+ addIncidentMutation.mutate({
+ userId,
+ orderId: orderId || undefined,
+ adminComment: adminComment || undefined,
+ negativityScore: score,
+ });
+ };
+
+ return (
+
+
+
+
+
+
+
+ Add User Incident
+
+
+ Record an incident for this user. This will be visible in their profile.
+
+
+
+
+
+
+
+
+
+
+ Higher negativity scores indicate more serious incidents (e.g., repeated cancellations, abusive behavior).
+
+
+
+
+
+ Cancel
+
+
+
+ {addIncidentMutation.isPending ? 'Adding...' : 'Add Incident'}
+
+
+
+
+
+ );
+}
+
+export function UserIncidentsView({ userId, orderId }: { userId: number; orderId: number | null }) {
+ const [incidentDialogOpen, setIncidentDialogOpen] = useState(false);
+
+ const { data: incidentsData, refetch: refetchIncidents } = trpc.admin.user.getUserIncidents.useQuery(
+ { userId },
+ { enabled: !!userId }
+ );
+
+ return (
+ <>
+
+
+
+ User Incidents
+
+ setIncidentDialogOpen(true)}
+ style={tw`flex-row items-center bg-amber-200 px-3 py-1.5 rounded-lg`}
+ >
+
+ Add Incident
+
+
+
+ {incidentsData?.incidents && incidentsData.incidents.length > 0 ? (
+
+ {incidentsData.incidents.map((incident: any, index: number) => (
+
+
+
+
+
+ {dayjs(incident.dateAdded).format('MMM DD, YYYY • h:mm A')}
+
+
+ {incident.negativityScore && (
+
+
+ Score: {incident.negativityScore}
+
+
+ )}
+
+
+ {incident.adminComment && (
+
+
+ {incident.adminComment}
+
+
+ )}
+
+
+
+
+ Added by {incident.addedBy}
+
+ {incident.orderId && (
+ <>
+
+
+ Order #{incident.orderId}
+
+ >
+ )}
+
+
+ ))}
+
+ ) : (
+
+
+
+ No incidents recorded for this user
+
+
+ )}
+
+
+ setIncidentDialogOpen(false)}
+ onSuccess={refetchIncidents}
+ />
+ >
+ );
+}
diff --git a/apps/backend/src/lib/init.ts b/apps/backend/src/lib/init.ts
index af2a5db..e5f9455 100755
--- a/apps/backend/src/lib/init.ts
+++ b/apps/backend/src/lib/init.ts
@@ -1,5 +1,6 @@
import './notif-job';
import { initializeAllStores } from '../stores/store-initializer';
+import { initializeUserNegativityStore } from '../stores/user-negativity-store';
import { startOrderHandler, startCancellationHandler, publishOrder } from './post-order-handler';
import { deleteOrders } from './delete-orders';
@@ -10,6 +11,7 @@ import { deleteOrders } from './delete-orders';
* - Const Store (syncs constants from DB to Redis)
* - Post Order Handler (Redis Pub/Sub subscriber)
* - Cancellation Handler (Redis Pub/Sub subscriber for order cancellations)
+ * - User Negativity Store (caches user negativity scores in Redis)
* - Other services can be added here in the future
*/
export const initFunc = async (): Promise => {
@@ -18,6 +20,7 @@ export const initFunc = async (): Promise => {
await Promise.all([
initializeAllStores(),
+ initializeUserNegativityStore(),
startOrderHandler(),
startCancellationHandler(),
]);
diff --git a/apps/backend/src/stores/user-negativity-store.ts b/apps/backend/src/stores/user-negativity-store.ts
new file mode 100644
index 0000000..d1285b7
--- /dev/null
+++ b/apps/backend/src/stores/user-negativity-store.ts
@@ -0,0 +1,118 @@
+import redisClient from 'src/lib/redis-client';
+import { db } from '../db/db_index';
+import { userIncidents } from '../db/schema';
+import { eq, sum } from 'drizzle-orm';
+
+export async function initializeUserNegativityStore(): Promise {
+ try {
+ console.log('Initializing user negativity store in Redis...');
+
+ const results = await db
+ .select({
+ userId: userIncidents.userId,
+ totalNegativityScore: sum(userIncidents.negativityScore).mapWith(Number),
+ })
+ .from(userIncidents)
+ .groupBy(userIncidents.userId);
+
+ for (const { userId, totalNegativityScore } of results) {
+ await redisClient.set(
+ `user:negativity:${userId}`,
+ totalNegativityScore.toString(),
+ );
+ }
+
+ console.log(`User negativity store initialized for ${results.length} users`);
+ } catch (error) {
+ console.error('Error initializing user negativity store:', error);
+ throw error;
+ }
+}
+
+export async function getUserNegativity(userId: number): Promise {
+ try {
+ const key = `user:negativity:${userId}`;
+ const data = await redisClient.get(key);
+
+ if (!data) {
+ return 0;
+ }
+
+ return parseInt(data, 10);
+ } catch (error) {
+ console.error(`Error getting negativity score for user ${userId}:`, error);
+ return 0;
+ }
+}
+
+export async function getAllUserNegativityScores(): Promise> {
+ try {
+ const keys = await redisClient.KEYS('user:negativity:*');
+
+ if (keys.length === 0) return {};
+
+ const values = await redisClient.MGET(keys);
+
+ const result: Record = {};
+ for (let i = 0; i < keys.length; i++) {
+ const key = keys[i];
+ const value = values[i];
+
+ const match = key.match(/user:negativity:(\d+)/);
+ if (match && value) {
+ const userId = parseInt(match[1], 10);
+ result[userId] = parseInt(value, 10);
+ }
+ }
+
+ return result;
+ } catch (error) {
+ console.error('Error getting all user negativity scores:', error);
+ return {};
+ }
+}
+
+export async function getMultipleUserNegativityScores(userIds: number[]): Promise> {
+ try {
+ if (userIds.length === 0) return {};
+
+ const keys = userIds.map(id => `user:negativity:${id}`);
+
+ const values = await redisClient.MGET(keys);
+
+ const result: Record = {};
+ for (let i = 0; i < userIds.length; i++) {
+ const value = values[i];
+ if (value) {
+ result[userIds[i]] = parseInt(value, 10);
+ } else {
+ result[userIds[i]] = 0;
+ }
+ }
+
+ return result;
+ } catch (error) {
+ console.error('Error getting multiple user negativity scores:', error);
+ return {};
+ }
+}
+
+export async function recomputeUserNegativityScore(userId: number): Promise {
+ try {
+ const [result] = await db
+ .select({
+ totalNegativityScore: sum(userIncidents.negativityScore).mapWith(Number),
+ })
+ .from(userIncidents)
+ .where(eq(userIncidents.userId, userId))
+ .limit(1);
+
+ const totalScore = result?.totalNegativityScore || 0;
+
+ const key = `user:negativity:${userId}`;
+ await redisClient.set(key, totalScore.toString());
+ } catch (error) {
+ console.error(`Error recomputing negativity score for user ${userId}:`, error);
+ throw error;
+ }
+}
diff --git a/apps/backend/src/trpc/admin-apis/order.ts b/apps/backend/src/trpc/admin-apis/order.ts
index dcb5c2b..ba7e1f1 100644
--- a/apps/backend/src/trpc/admin-apis/order.ts
+++ b/apps/backend/src/trpc/admin-apis/order.ts
@@ -20,6 +20,7 @@ import {
sendOrderDeliveredNotification,
} from "../../lib/notif-job";
import { publishCancellation } from "../../lib/post-order-handler";
+import { getMultipleUserNegativityScores } from "../../stores/user-negativity-store";
const updateOrderNotesSchema = z.object({
orderId: z.number(),
@@ -780,6 +781,9 @@ export const orderRouter = router({
const hasMore = allOrders.length > limit;
const ordersToReturn = hasMore ? allOrders.slice(0, limit) : allOrders;
+ const userIds = [...new Set(ordersToReturn.map(o => o.userId))];
+ const negativityScores = await getMultipleUserNegativityScores(userIds);
+
const filteredOrders = ordersToReturn.filter((order) => {
const statusRecord = order.orderStatus[0];
return (
@@ -813,35 +817,37 @@ export const orderRouter = router({
.sort((first, second) => first.id - second.id);
dayjs.extend(utc);
return {
- id: order.id,
- orderId: order.id.toString(),
- readableId: order.id,
- customerName: order.user.name,
- address: `${order.address.addressLine1}${
- order.address.addressLine2
- ? `, ${order.address.addressLine2}`
- : ""
- }, ${order.address.city}, ${order.address.state} - ${
- order.address.pincode
- }, Phone: ${order.address.phone}`,
- addressId: order.addressId,
- latitude: order.address.adminLatitude ?? order.address.latitude,
- longitude: order.address.adminLongitude ?? order.address.longitude,
- totalAmount: parseFloat(order.totalAmount),
- deliveryCharge: parseFloat(order.deliveryCharge || "0"),
- items,
- createdAt: order.createdAt,
- // deliveryTime: order.slot ? dayjs.utc(order.slot.deliveryTime).format('ddd, MMM D • h:mm A') : 'Not scheduled',
- deliveryTime: order.slot?.deliveryTime.toISOString() || null,
- status,
- isPackaged:
- order.orderItems.every((item) => item.is_packaged) || false,
- isDelivered: statusRecord?.isDelivered || false,
- isCod: order.isCod,
- isFlashDelivery: order.isFlashDelivery,
- userNotes: order.userNotes,
- adminNotes: order.adminNotes,
- };
+ id: order.id,
+ orderId: order.id.toString(),
+ readableId: order.id,
+ customerName: order.user.name,
+ customerMobile: order.user.mobile,
+ address: `${order.address.addressLine1}${
+ order.address.addressLine2
+ ? `, ${order.address.addressLine2}`
+ : ""
+ }, ${order.address.city}, ${order.address.state} - ${
+ order.address.pincode
+ }, Phone: ${order.address.phone}`,
+ addressId: order.addressId,
+ latitude: order.address.adminLatitude ?? order.address.latitude,
+ longitude: order.address.adminLongitude ?? order.address.longitude,
+ totalAmount: parseFloat(order.totalAmount),
+ deliveryCharge: parseFloat(order.deliveryCharge || "0"),
+ items,
+ createdAt: order.createdAt,
+ // deliveryTime: order.slot ? dayjs.utc(order.slot.deliveryTime).format('ddd, MMM D • h:mm A') : 'Not scheduled',
+ deliveryTime: order.slot?.deliveryTime.toISOString() || null,
+ status,
+ isPackaged:
+ order.orderItems.every((item) => item.is_packaged) || false,
+ isDelivered: statusRecord?.isDelivered || false,
+ isCod: order.isCod,
+ isFlashDelivery: order.isFlashDelivery,
+ userNotes: order.userNotes,
+ adminNotes: order.adminNotes,
+ userNegativityScore: negativityScores[order.userId] || 0,
+ };
});
return {
diff --git a/apps/backend/src/trpc/admin-apis/user.ts b/apps/backend/src/trpc/admin-apis/user.ts
index f67b828..77141ce 100644
--- a/apps/backend/src/trpc/admin-apis/user.ts
+++ b/apps/backend/src/trpc/admin-apis/user.ts
@@ -4,7 +4,8 @@ import { db } from '../../db/db_index';
import { users, complaints, orders, orderItems, notifCreds, unloggedUserTokens, userDetails, userIncidents } from '../../db/schema';
import { eq, sql, desc, asc, count, max, inArray } from 'drizzle-orm';
import { ApiError } from '../../lib/api-error';
-import { notificationQueue } from '../../lib/notif-job';
+import { notificationQueue } from '../../lib/notif-job';
+import { recomputeUserNegativityScore } from '../../stores/user-negativity-store';
async function createUserByMobile(mobile: string): Promise {
// Clean mobile number (remove non-digits)
@@ -470,21 +471,16 @@ export const userRouter = {
}
- const incidentObj = {userId, orderId, adminComment, addedBy: adminUserId, negativityScore}
+ const incidentObj = { userId, orderId, adminComment, addedBy: adminUserId, negativityScore };
const [incident] = await db.insert(userIncidents)
- .values(
- // {
- // userId,
- // orderId,
- // adminComment,
- // addedBy: adminUserId,
- // negativityScore,
- // }
- {...incidentObj}
- )
+ .values({
+ ...incidentObj,
+ })
.returning();
+ recomputeUserNegativityScore(userId);
+
return {
success: true,
data: incident,