enh
This commit is contained in:
parent
1122159552
commit
501667a4d2
12 changed files with 107 additions and 475 deletions
6
apps/admin-ui/.expo/types/router.d.ts
vendored
6
apps/admin-ui/.expo/types/router.d.ts
vendored
File diff suppressed because one or more lines are too long
|
|
@ -75,7 +75,6 @@ app.use('/api/trpc', createExpressMiddleware({
|
||||||
let staffUser = null;
|
let staffUser = null;
|
||||||
const authHeader = req.headers.authorization;
|
const authHeader = req.headers.authorization;
|
||||||
|
|
||||||
console.log({authHeader})
|
|
||||||
if (authHeader?.startsWith('Bearer ')) {
|
if (authHeader?.startsWith('Bearer ')) {
|
||||||
const token = authHeader.substring(7);
|
const token = authHeader.substring(7);
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ import { ApiError } from "@/src/lib/api-error"
|
||||||
import v1Router from "@/src/v1-router"
|
import v1Router from "@/src/v1-router"
|
||||||
import testController from "@/src/test-controller"
|
import testController from "@/src/test-controller"
|
||||||
import { authenticateUser } from "@/src/middleware/auth.middleware"
|
import { authenticateUser } from "@/src/middleware/auth.middleware"
|
||||||
import { raiseComplaint } from "@/src/uv-apis/user-rest.controller"
|
|
||||||
import uploadHandler from "@/src/lib/upload-handler"
|
|
||||||
|
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ export const commonApiRouter = router({
|
||||||
|
|
||||||
generateUploadUrls: protectedProcedure
|
generateUploadUrls: protectedProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
contextString: z.enum(['review', 'product_info', 'store', 'notification', 'profile']),
|
contextString: z.enum(['review', 'product_info', 'store', 'notification', 'profile', 'complaint']),
|
||||||
mimeTypes: z.array(z.string()),
|
mimeTypes: z.array(z.string()),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input }): Promise<{ uploadUrls: string[] }> => {
|
.mutation(async ({ input }): Promise<{ uploadUrls: string[] }> => {
|
||||||
|
|
@ -89,6 +89,8 @@ export const commonApiRouter = router({
|
||||||
folder = 'store-images';
|
folder = 'store-images';
|
||||||
} else if (contextString === 'profile') {
|
} else if (contextString === 'profile') {
|
||||||
folder = 'profile-images';
|
folder = 'profile-images';
|
||||||
|
} else if (contextString === 'complaint') {
|
||||||
|
folder = 'complaint-images';
|
||||||
}
|
}
|
||||||
// else if (contextString === 'review_response') {
|
// else if (contextString === 'review_response') {
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { z } from 'zod';
|
||||||
import { db } from '@/src/db/db_index';
|
import { db } from '@/src/db/db_index';
|
||||||
import { complaints } from '@/src/db/schema';
|
import { complaints } from '@/src/db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { scaffoldAssetUrl, claimUploadUrl } from '@/src/lib/s3-client';
|
||||||
|
|
||||||
export const complaintRouter = router({
|
export const complaintRouter = router({
|
||||||
getAll: protectedProcedure
|
getAll: protectedProcedure
|
||||||
|
|
@ -17,6 +18,7 @@ export const complaintRouter = router({
|
||||||
isResolved: complaints.isResolved,
|
isResolved: complaints.isResolved,
|
||||||
createdAt: complaints.createdAt,
|
createdAt: complaints.createdAt,
|
||||||
orderId: complaints.orderId,
|
orderId: complaints.orderId,
|
||||||
|
images: complaints.images,
|
||||||
})
|
})
|
||||||
.from(complaints)
|
.from(complaints)
|
||||||
.where(eq(complaints.userId, userId))
|
.where(eq(complaints.userId, userId))
|
||||||
|
|
@ -30,34 +32,39 @@ export const complaintRouter = router({
|
||||||
isResolved: c.isResolved,
|
isResolved: c.isResolved,
|
||||||
createdAt: c.createdAt,
|
createdAt: c.createdAt,
|
||||||
orderId: c.orderId,
|
orderId: c.orderId,
|
||||||
|
images: c.images ? scaffoldAssetUrl(c.images as string[]) : [],
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
raise: protectedProcedure
|
raise: protectedProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
orderId: z.string().optional(),
|
orderId: z.number().optional(),
|
||||||
complaintBody: z.string().min(1, 'Complaint body is required'),
|
complaintBody: z.string().min(1, 'Complaint body is required'),
|
||||||
|
imageKeys: z.array(z.string()).optional(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const userId = ctx.user.userId;
|
const userId = ctx.user.userId;
|
||||||
const { orderId, complaintBody } = input;
|
const { orderId, complaintBody, imageKeys } = input;
|
||||||
|
|
||||||
let orderIdNum: number | null = null;
|
|
||||||
|
|
||||||
if (orderId) {
|
|
||||||
const readableIdMatch = orderId.match(/^ORD(\d+)$/);
|
|
||||||
if (readableIdMatch) {
|
|
||||||
orderIdNum = parseInt(readableIdMatch[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.insert(complaints).values({
|
await db.insert(complaints).values({
|
||||||
userId,
|
userId,
|
||||||
orderId: orderIdNum,
|
orderId: orderId || null,
|
||||||
complaintBody: complaintBody.trim(),
|
complaintBody: complaintBody.trim(),
|
||||||
|
images: imageKeys || [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Claim upload URLs for images
|
||||||
|
if (imageKeys && imageKeys.length > 0) {
|
||||||
|
for (const key of imageKeys) {
|
||||||
|
try {
|
||||||
|
await claimUploadUrl(key);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to claim upload URL for key: ${key}`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true, message: 'Complaint raised successfully' };
|
return { success: true, message: 'Complaint raised successfully' };
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,321 +0,0 @@
|
||||||
import { Request, Response, NextFunction } from 'express';
|
|
||||||
import bcrypt from 'bcryptjs';
|
|
||||||
import jwt from 'jsonwebtoken';
|
|
||||||
import { eq } from 'drizzle-orm';
|
|
||||||
import { db } from '@/src/db/db_index'
|
|
||||||
import { users, userCreds, userDetails } from '@/src/db/schema'
|
|
||||||
import { ApiError } from '@/src/lib/api-error'
|
|
||||||
import catchAsync from '@/src/lib/catch-async'
|
|
||||||
import { jwtSecret } from '@/src/lib/env-exporter';
|
|
||||||
import uploadHandler from '@/src/lib/upload-handler'
|
|
||||||
import { imageUploadS3, generateSignedUrlFromS3Url } from '@/src/lib/s3-client'
|
|
||||||
|
|
||||||
interface RegisterRequest {
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
mobile: string;
|
|
||||||
password: string;
|
|
||||||
profileImage?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateProfileRequest {
|
|
||||||
name?: string;
|
|
||||||
email?: string;
|
|
||||||
mobile?: string;
|
|
||||||
password?: string;
|
|
||||||
bio?: string;
|
|
||||||
dateOfBirth?: string;
|
|
||||||
gender?: string;
|
|
||||||
occupation?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuthResponse {
|
|
||||||
token: string;
|
|
||||||
user: {
|
|
||||||
id: number;
|
|
||||||
name: string | null;
|
|
||||||
email: string | null;
|
|
||||||
mobile: string | null;
|
|
||||||
profileImage?: string | null;
|
|
||||||
bio?: string | null;
|
|
||||||
dateOfBirth?: string | null;
|
|
||||||
gender?: string | null;
|
|
||||||
occupation?: string | null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateToken = (userId: number): string => {
|
|
||||||
const secret = jwtSecret;
|
|
||||||
if (!secret) {
|
|
||||||
throw new ApiError('JWT secret not configured', 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
return jwt.sign({ userId }, secret, { expiresIn: '7d' });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const register = catchAsync(async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const { name, email, mobile, password }: RegisterRequest = req.body;
|
|
||||||
|
|
||||||
// Handle profile image upload
|
|
||||||
let profileImageUrl: string | undefined;
|
|
||||||
if (req.file) {
|
|
||||||
const key = `profile-images/${Date.now()}-${req.file.originalname}`;
|
|
||||||
profileImageUrl = await imageUploadS3(req.file.buffer, req.file.mimetype, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!name || !email || !mobile || !password) {
|
|
||||||
throw new ApiError('All fields are required', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate email format
|
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
||||||
if (!emailRegex.test(email)) {
|
|
||||||
throw new ApiError('Invalid email format', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate mobile format (Indian mobile numbers)
|
|
||||||
const cleanMobile = mobile.replace(/\D/g, '');
|
|
||||||
if (cleanMobile.length !== 10 || !/^[6-9]/.test(cleanMobile)) {
|
|
||||||
throw new ApiError('Invalid mobile number', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if email already exists
|
|
||||||
const [existingEmail] = await db
|
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.email, email.toLowerCase()))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingEmail) {
|
|
||||||
throw new ApiError('Email already registered', 409);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if mobile already exists
|
|
||||||
const [existingMobile] = await db
|
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.mobile, cleanMobile))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingMobile) {
|
|
||||||
throw new ApiError('Mobile number already registered', 409);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash password
|
|
||||||
const hashedPassword = await bcrypt.hash(password, 12);
|
|
||||||
|
|
||||||
// Create user and credentials in a transaction
|
|
||||||
const newUser = await db.transaction(async (tx) => {
|
|
||||||
// Create user
|
|
||||||
const [user] = await tx
|
|
||||||
.insert(users)
|
|
||||||
.values({
|
|
||||||
name: name.trim(),
|
|
||||||
email: email.toLowerCase().trim(),
|
|
||||||
mobile: cleanMobile,
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
// Create user credentials
|
|
||||||
await tx
|
|
||||||
.insert(userCreds)
|
|
||||||
.values({
|
|
||||||
userId: user.id,
|
|
||||||
userPassword: hashedPassword,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create user details with profile image
|
|
||||||
await tx
|
|
||||||
.insert(userDetails)
|
|
||||||
.values({
|
|
||||||
userId: user.id,
|
|
||||||
profileImage: profileImageUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
return user;
|
|
||||||
});
|
|
||||||
|
|
||||||
const token = generateToken(newUser.id);
|
|
||||||
|
|
||||||
// Generate signed URL for profile image if it was uploaded
|
|
||||||
const profileImageSignedUrl = profileImageUrl
|
|
||||||
? await generateSignedUrlFromS3Url(profileImageUrl)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const response: AuthResponse = {
|
|
||||||
token,
|
|
||||||
user: {
|
|
||||||
id: newUser.id,
|
|
||||||
name: newUser.name,
|
|
||||||
email: newUser.email,
|
|
||||||
mobile: newUser.mobile,
|
|
||||||
profileImage: profileImageSignedUrl,
|
|
||||||
bio: null,
|
|
||||||
dateOfBirth: null,
|
|
||||||
gender: null,
|
|
||||||
occupation: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
res.status(201).json({
|
|
||||||
success: true,
|
|
||||||
data: response,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateProfile = catchAsync(async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const userId = req.user?.userId;
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
throw new ApiError('User not authenticated', 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { name, email, mobile, password, bio, dateOfBirth, gender, occupation }: UpdateProfileRequest = req.body;
|
|
||||||
|
|
||||||
// Handle profile image upload
|
|
||||||
let profileImageUrl: string | undefined;
|
|
||||||
if (req.file) {
|
|
||||||
const key = `profile-images/${Date.now()}-${req.file.originalname}`;
|
|
||||||
profileImageUrl = await imageUploadS3(req.file.buffer, req.file.mimetype, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate email format if provided
|
|
||||||
if (email) {
|
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
||||||
if (!emailRegex.test(email)) {
|
|
||||||
throw new ApiError('Invalid email format', 400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate mobile format if provided
|
|
||||||
if (mobile) {
|
|
||||||
const cleanMobile = mobile.replace(/\D/g, '');
|
|
||||||
if (cleanMobile.length !== 10 || !/^[6-9]/.test(cleanMobile)) {
|
|
||||||
throw new ApiError('Invalid mobile number', 400);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if email already exists (if changing email)
|
|
||||||
if (email) {
|
|
||||||
const [existingEmail] = await db
|
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.email, email.toLowerCase()))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingEmail && existingEmail.id !== userId) {
|
|
||||||
throw new ApiError('Email already registered', 409);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if mobile already exists (if changing mobile)
|
|
||||||
if (mobile) {
|
|
||||||
const cleanMobile = mobile.replace(/\D/g, '');
|
|
||||||
const [existingMobile] = await db
|
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.mobile, cleanMobile))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingMobile && existingMobile.id !== userId) {
|
|
||||||
throw new ApiError('Mobile number already registered', 409);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update user and user details in a transaction
|
|
||||||
const updatedUser = await db.transaction(async (tx) => {
|
|
||||||
// Update user table
|
|
||||||
const updateData: any = {};
|
|
||||||
if (name) updateData.name = name.trim();
|
|
||||||
if (email) updateData.email = email.toLowerCase().trim();
|
|
||||||
if (mobile) updateData.mobile = mobile.replace(/\D/g, '');
|
|
||||||
|
|
||||||
if (Object.keys(updateData).length > 0) {
|
|
||||||
await tx
|
|
||||||
.update(users)
|
|
||||||
.set(updateData)
|
|
||||||
.where(eq(users.id, userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update password if provided
|
|
||||||
if (password) {
|
|
||||||
const hashedPassword = await bcrypt.hash(password, 12);
|
|
||||||
await tx
|
|
||||||
.update(userCreds)
|
|
||||||
.set({ userPassword: hashedPassword })
|
|
||||||
.where(eq(userCreds.userId, userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update or insert user details
|
|
||||||
const userDetailsUpdate: any = {};
|
|
||||||
if (bio !== undefined) userDetailsUpdate.bio = bio;
|
|
||||||
if (dateOfBirth !== undefined) userDetailsUpdate.dateOfBirth = dateOfBirth ? new Date(dateOfBirth) : null;
|
|
||||||
if (gender !== undefined) userDetailsUpdate.gender = gender;
|
|
||||||
if (occupation !== undefined) userDetailsUpdate.occupation = occupation;
|
|
||||||
if (profileImageUrl) userDetailsUpdate.profileImage = profileImageUrl;
|
|
||||||
userDetailsUpdate.updatedAt = new Date();
|
|
||||||
|
|
||||||
// Check if user details record exists
|
|
||||||
const [existingDetails] = await tx
|
|
||||||
.select()
|
|
||||||
.from(userDetails)
|
|
||||||
.where(eq(userDetails.userId, userId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingDetails) {
|
|
||||||
// Update existing record
|
|
||||||
await tx
|
|
||||||
.update(userDetails)
|
|
||||||
.set(userDetailsUpdate)
|
|
||||||
.where(eq(userDetails.userId, userId));
|
|
||||||
} else {
|
|
||||||
// Create new record
|
|
||||||
userDetailsUpdate.userId = userId;
|
|
||||||
userDetailsUpdate.createdAt = new Date();
|
|
||||||
await tx
|
|
||||||
.insert(userDetails)
|
|
||||||
.values(userDetailsUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return updated user data
|
|
||||||
const [user] = await tx
|
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.id, userId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
return user;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get updated user details for response
|
|
||||||
const [userDetail] = await db
|
|
||||||
.select()
|
|
||||||
.from(userDetails)
|
|
||||||
.where(eq(userDetails.userId, userId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
// Generate signed URL for profile image if it exists
|
|
||||||
const profileImageSignedUrl = userDetail?.profileImage
|
|
||||||
? await generateSignedUrlFromS3Url(userDetail.profileImage)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const response: AuthResponse = {
|
|
||||||
token: req.headers.authorization?.replace('Bearer ', '') || '', // Keep existing token
|
|
||||||
user: {
|
|
||||||
id: updatedUser.id,
|
|
||||||
name: updatedUser.name,
|
|
||||||
email: updatedUser.email,
|
|
||||||
mobile: updatedUser.mobile,
|
|
||||||
profileImage: profileImageSignedUrl,
|
|
||||||
bio: userDetail?.bio || null,
|
|
||||||
dateOfBirth: userDetail?.dateOfBirth || null,
|
|
||||||
gender: userDetail?.gender || null,
|
|
||||||
occupation: userDetail?.occupation || null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
data: response,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { Router } from 'express';
|
|
||||||
import { register, updateProfile } from '@/src/uv-apis/auth.controller'
|
|
||||||
import { verifyToken } from '@/src/middleware/auth'
|
|
||||||
import uploadHandler from '@/src/lib/upload-handler'
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.post('/register', uploadHandler.single('profileImage'), register);
|
|
||||||
router.put('/profile', verifyToken, uploadHandler.single('profileImage'), updateProfile);
|
|
||||||
|
|
||||||
const authRouter = router;
|
|
||||||
export default authRouter;
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
import { Request, Response, NextFunction } from 'express';
|
|
||||||
import { db } from '@/src/db/db_index'
|
|
||||||
import { complaints } from '@/src/db/schema'
|
|
||||||
import { ApiError } from '@/src/lib/api-error'
|
|
||||||
import catchAsync from '@/src/lib/catch-async'
|
|
||||||
import { imageUploadS3 } from '@/src/lib/s3-client'
|
|
||||||
|
|
||||||
interface RaiseComplaintRequest {
|
|
||||||
orderId?: string;
|
|
||||||
complaintBody: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const raiseComplaint = catchAsync(async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
console.log('raising complaint')
|
|
||||||
|
|
||||||
const userId = req.user?.userId;
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
throw new ApiError('User not authenticated', 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { orderId, complaintBody }: RaiseComplaintRequest = req.body;
|
|
||||||
|
|
||||||
let orderIdNum: number | null = null;
|
|
||||||
|
|
||||||
if (orderId) {
|
|
||||||
const readableIdMatch = orderId.match(/^ORD(\d+)$/);
|
|
||||||
if (readableIdMatch) {
|
|
||||||
orderIdNum = parseInt(readableIdMatch[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle image uploads
|
|
||||||
const images = (req.files as Express.Multer.File[])?.filter(item => item.fieldname === 'images');
|
|
||||||
let uploadedImageUrls: string[] = [];
|
|
||||||
|
|
||||||
if (images && Array.isArray(images)) {
|
|
||||||
const imageUploadPromises = images.map((file, index) => {
|
|
||||||
const key = `complaint-images/${Date.now()}-${index}`;
|
|
||||||
return imageUploadS3(file.buffer, file.mimetype, key);
|
|
||||||
});
|
|
||||||
|
|
||||||
uploadedImageUrls = await Promise.all(imageUploadPromises);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.insert(complaints).values({
|
|
||||||
userId,
|
|
||||||
orderId: orderIdNum,
|
|
||||||
complaintBody: complaintBody.trim(),
|
|
||||||
images: uploadedImageUrls.length > 0 ? uploadedImageUrls : null,
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
message: 'Complaint raised successfully'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,12 +1,8 @@
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import authRouter from "@/src/uv-apis/auth.router"
|
|
||||||
import { raiseComplaint } from "@/src/uv-apis/user-rest.controller"
|
|
||||||
import uploadHandler from "@/src/lib/upload-handler";
|
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.use("/auth", authRouter);
|
// All user APIs migrated to tRPC
|
||||||
router.use("/complaints/raise", uploadHandler.array('images'),raiseComplaint)
|
|
||||||
|
|
||||||
const uvRouter = router;
|
const uvRouter = router;
|
||||||
export default uvRouter;
|
export default uvRouter;
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,23 @@ const ComplaintItem: React.FC<ComplaintItemProps> = ({ item }) => (
|
||||||
{item.complaintBody}
|
{item.complaintBody}
|
||||||
</MyText>
|
</MyText>
|
||||||
|
|
||||||
|
{/* Complaint Images */}
|
||||||
|
{item.images && item.images.length > 0 && (
|
||||||
|
<View style={tw`mb-4`}>
|
||||||
|
<View style={tw`flex-row flex-wrap -m-1`}>
|
||||||
|
{item.images.map((imageUrl: string, index: number) => (
|
||||||
|
<View key={index} style={tw`w-1/3 p-1`}>
|
||||||
|
<Image
|
||||||
|
source={{ uri: imageUrl }}
|
||||||
|
style={tw`w-full aspect-square rounded-lg`}
|
||||||
|
contentFit="cover"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Admin Response */}
|
{/* Admin Response */}
|
||||||
{item.response && (
|
{item.response && (
|
||||||
<View style={tw`bg-blue-50 p-4 rounded-xl border border-blue-100`}>
|
<View style={tw`bg-blue-50 p-4 rounded-xl border border-blue-100`}>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, TextInput, ActivityIndicator, KeyboardAvoidingView, Platform, Alert } from 'react-native';
|
import { View, TextInput, ActivityIndicator, KeyboardAvoidingView, Platform, Alert } from 'react-native';
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
import { MaterialIcons } from '@expo/vector-icons';
|
||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
import { MyText, ImageUploader, tw, MyTouchableOpacity } from 'common-ui';
|
import { MyText, ImageUploader, tw, MyTouchableOpacity } from 'common-ui';
|
||||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
import usePickImage from 'common-ui/src/components/use-pick-image';
|
||||||
import axios from '../services/axios-user-ui';
|
import { trpc } from '@/src/trpc-client';
|
||||||
// import axios from 'common-ui/src/services/axios';
|
import { useUploadToObjectStorage } from '@/src/hooks/useUploadToObjectStorage';
|
||||||
|
|
||||||
interface ComplaintFormProps {
|
interface ComplaintFormProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|
@ -15,70 +14,73 @@ interface ComplaintFormProps {
|
||||||
|
|
||||||
export default function ComplaintForm({ open, onClose, orderId }: ComplaintFormProps) {
|
export default function ComplaintForm({ open, onClose, orderId }: ComplaintFormProps) {
|
||||||
const [complaintBody, setComplaintBody] = useState('');
|
const [complaintBody, setComplaintBody] = useState('');
|
||||||
const [complaintImages, setComplaintImages] = useState<{ uri?: string }[]>([]);
|
const [complaintImages, setComplaintImages] = useState<{ uri: string; mimeType: string }[]>([]);
|
||||||
|
|
||||||
// API function
|
const raiseComplaintMutation = trpc.user.complaint.raise.useMutation();
|
||||||
const raiseComplaintApi = async (payload: { complaintBody: string; images: { uri?: string }[] }) => {
|
const { upload, isUploading } = useUploadToObjectStorage();
|
||||||
const formData = new FormData();
|
|
||||||
|
|
||||||
formData.append('orderId', orderId.toString());
|
|
||||||
formData.append('complaintBody', payload.complaintBody);
|
|
||||||
|
|
||||||
// Add images if provided
|
|
||||||
if (payload.images && payload.images.length > 0) {
|
|
||||||
payload.images.forEach((image, index) => {
|
|
||||||
if (image.uri) {
|
|
||||||
const fileName = `complaint-image-${index}.jpg`;
|
|
||||||
formData.append('images', {
|
|
||||||
uri: image.uri,
|
|
||||||
name: fileName,
|
|
||||||
type: 'image/jpeg',
|
|
||||||
} as any);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.post('/uv/complaints/raise', formData, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Hook
|
|
||||||
const raiseComplaintMutation = useMutation({
|
|
||||||
mutationFn: raiseComplaintApi,
|
|
||||||
});
|
|
||||||
|
|
||||||
const pickComplaintImage = usePickImage({
|
const pickComplaintImage = usePickImage({
|
||||||
setFile: (files) => setComplaintImages(prev => [...prev, ...files]),
|
setFile: async (assets: any) => {
|
||||||
|
if (!assets || (Array.isArray(assets) && assets.length === 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = Array.isArray(assets) ? assets : [assets];
|
||||||
|
const newImages = files.map((asset: any) => ({
|
||||||
|
uri: asset.uri,
|
||||||
|
mimeType: asset.mimeType || 'image/jpeg',
|
||||||
|
}));
|
||||||
|
|
||||||
|
setComplaintImages(prev => [...prev, ...newImages]);
|
||||||
|
},
|
||||||
multiple: true,
|
multiple: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleRemoveImage = (image: { uri: string; mimeType: string }) => {
|
||||||
|
setComplaintImages(prev => prev.filter(img => img.uri !== image.uri));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
if (!complaintBody.trim()) {
|
if (!complaintBody.trim()) {
|
||||||
Alert.alert('Error', 'Please enter complaint details');
|
Alert.alert('Error', 'Please enter complaint details');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
raiseComplaintMutation.mutate(
|
try {
|
||||||
{
|
let imageKeys: string[] = [];
|
||||||
complaintBody: complaintBody.trim(),
|
|
||||||
images: complaintImages,
|
// Upload images if provided
|
||||||
},
|
if (complaintImages.length > 0) {
|
||||||
{
|
// Fetch blobs from URIs
|
||||||
onSuccess: () => {
|
const imagesWithBlobs = await Promise.all(
|
||||||
Alert.alert('Success', 'Complaint raised successfully');
|
complaintImages.map(async (img) => {
|
||||||
setComplaintBody('');
|
const response = await fetch(img.uri);
|
||||||
setComplaintImages([]);
|
const blob = await response.blob();
|
||||||
onClose();
|
return { blob, mimeType: img.mimeType };
|
||||||
},
|
})
|
||||||
onError: (error: any) => {
|
);
|
||||||
Alert.alert('Error', error.message || 'Failed to raise complaint');
|
|
||||||
},
|
const result = await upload({
|
||||||
|
images: imagesWithBlobs,
|
||||||
|
contextString: 'complaint',
|
||||||
|
});
|
||||||
|
imageKeys = result.keys;
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
// Submit complaint
|
||||||
|
await raiseComplaintMutation.mutateAsync({
|
||||||
|
orderId,
|
||||||
|
complaintBody: complaintBody.trim(),
|
||||||
|
imageKeys,
|
||||||
|
});
|
||||||
|
|
||||||
|
Alert.alert('Success', 'Complaint raised successfully');
|
||||||
|
setComplaintBody('');
|
||||||
|
setComplaintImages([]);
|
||||||
|
onClose();
|
||||||
|
} catch (error: any) {
|
||||||
|
Alert.alert('Error', error.message || 'Failed to raise complaint');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
@ -109,14 +111,15 @@ export default function ComplaintForm({ open, onClose, orderId }: ComplaintFormP
|
||||||
images={complaintImages}
|
images={complaintImages}
|
||||||
onAddImage={pickComplaintImage}
|
onAddImage={pickComplaintImage}
|
||||||
onRemoveImage={(uri) => setComplaintImages(prev => prev.filter(img => img.uri !== uri))}
|
onRemoveImage={(uri) => setComplaintImages(prev => prev.filter(img => img.uri !== uri))}
|
||||||
|
allowMultiple={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MyTouchableOpacity
|
<MyTouchableOpacity
|
||||||
style={tw`bg-yellow-500 py-4 rounded-xl shadow-sm items-center mt-4 ${raiseComplaintMutation.isPending ? 'opacity-70' : ''}`}
|
style={tw`bg-yellow-500 py-4 rounded-xl shadow-sm items-center mt-4 ${isUploading || raiseComplaintMutation.isPending ? 'opacity-70' : ''}`}
|
||||||
onPress={handleSubmit}
|
onPress={handleSubmit}
|
||||||
disabled={raiseComplaintMutation.isPending}
|
disabled={isUploading || raiseComplaintMutation.isPending}
|
||||||
>
|
>
|
||||||
{raiseComplaintMutation.isPending ? (
|
{isUploading || raiseComplaintMutation.isPending ? (
|
||||||
<ActivityIndicator color="white" />
|
<ActivityIndicator color="white" />
|
||||||
) : (
|
) : (
|
||||||
<MyText style={tw`text-white font-bold text-lg`}>Submit Complaint</MyText>
|
<MyText style={tw`text-white font-bold text-lg`}>Submit Complaint</MyText>
|
||||||
|
|
@ -125,4 +128,4 @@ export default function ComplaintForm({ open, onClose, orderId }: ComplaintFormP
|
||||||
</View>
|
</View>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { useState } from 'react';
|
||||||
import { trpc } from '../trpc-client';
|
import { trpc } from '../trpc-client';
|
||||||
// import { trpc } from '../src/trpc-client';
|
// import { trpc } from '../src/trpc-client';
|
||||||
|
|
||||||
type ContextString = 'review' | 'product_info' | 'notification' | 'store' | 'profile';
|
type ContextString = 'review' | 'product_info' | 'notification' | 'store' | 'profile' | 'complaint';
|
||||||
|
|
||||||
interface UploadInput {
|
interface UploadInput {
|
||||||
blob: Blob;
|
blob: Blob;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue