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;
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
console.log({authHeader})
|
||||
if (authHeader?.startsWith('Bearer ')) {
|
||||
const token = authHeader.substring(7);
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import { ApiError } from "@/src/lib/api-error"
|
|||
import v1Router from "@/src/v1-router"
|
||||
import testController from "@/src/test-controller"
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ export const commonApiRouter = router({
|
|||
|
||||
generateUploadUrls: protectedProcedure
|
||||
.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()),
|
||||
}))
|
||||
.mutation(async ({ input }): Promise<{ uploadUrls: string[] }> => {
|
||||
|
|
@ -89,6 +89,8 @@ export const commonApiRouter = router({
|
|||
folder = 'store-images';
|
||||
} else if (contextString === 'profile') {
|
||||
folder = 'profile-images';
|
||||
} else if (contextString === 'complaint') {
|
||||
folder = 'complaint-images';
|
||||
}
|
||||
// else if (contextString === 'review_response') {
|
||||
//
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { z } from 'zod';
|
|||
import { db } from '@/src/db/db_index';
|
||||
import { complaints } from '@/src/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { scaffoldAssetUrl, claimUploadUrl } from '@/src/lib/s3-client';
|
||||
|
||||
export const complaintRouter = router({
|
||||
getAll: protectedProcedure
|
||||
|
|
@ -17,6 +18,7 @@ export const complaintRouter = router({
|
|||
isResolved: complaints.isResolved,
|
||||
createdAt: complaints.createdAt,
|
||||
orderId: complaints.orderId,
|
||||
images: complaints.images,
|
||||
})
|
||||
.from(complaints)
|
||||
.where(eq(complaints.userId, userId))
|
||||
|
|
@ -30,34 +32,39 @@ export const complaintRouter = router({
|
|||
isResolved: c.isResolved,
|
||||
createdAt: c.createdAt,
|
||||
orderId: c.orderId,
|
||||
images: c.images ? scaffoldAssetUrl(c.images as string[]) : [],
|
||||
})),
|
||||
};
|
||||
}),
|
||||
|
||||
raise: protectedProcedure
|
||||
.input(z.object({
|
||||
orderId: z.string().optional(),
|
||||
orderId: z.number().optional(),
|
||||
complaintBody: z.string().min(1, 'Complaint body is required'),
|
||||
imageKeys: z.array(z.string()).optional(),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const userId = ctx.user.userId;
|
||||
const { orderId, complaintBody } = input;
|
||||
|
||||
let orderIdNum: number | null = null;
|
||||
|
||||
if (orderId) {
|
||||
const readableIdMatch = orderId.match(/^ORD(\d+)$/);
|
||||
if (readableIdMatch) {
|
||||
orderIdNum = parseInt(readableIdMatch[1]);
|
||||
}
|
||||
}
|
||||
const { orderId, complaintBody, imageKeys } = input;
|
||||
|
||||
await db.insert(complaints).values({
|
||||
userId,
|
||||
orderId: orderIdNum,
|
||||
orderId: orderId || null,
|
||||
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' };
|
||||
}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 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();
|
||||
|
||||
router.use("/auth", authRouter);
|
||||
router.use("/complaints/raise", uploadHandler.array('images'),raiseComplaint)
|
||||
// All user APIs migrated to tRPC
|
||||
|
||||
const uvRouter = router;
|
||||
export default uvRouter;
|
||||
|
|
|
|||
|
|
@ -48,6 +48,23 @@ const ComplaintItem: React.FC<ComplaintItemProps> = ({ item }) => (
|
|||
{item.complaintBody}
|
||||
</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 */}
|
||||
{item.response && (
|
||||
<View style={tw`bg-blue-50 p-4 rounded-xl border border-blue-100`}>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import React, { useState } from 'react';
|
||||
import { View, TextInput, ActivityIndicator, KeyboardAvoidingView, Platform, Alert } from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { MyText, ImageUploader, tw, MyTouchableOpacity } from 'common-ui';
|
||||
import usePickImage from 'common-ui/src/components/use-pick-image';
|
||||
import axios from '../services/axios-user-ui';
|
||||
// import axios from 'common-ui/src/services/axios';
|
||||
import { trpc } from '@/src/trpc-client';
|
||||
import { useUploadToObjectStorage } from '@/src/hooks/useUploadToObjectStorage';
|
||||
|
||||
interface ComplaintFormProps {
|
||||
open: boolean;
|
||||
|
|
@ -15,70 +14,73 @@ interface ComplaintFormProps {
|
|||
|
||||
export default function ComplaintForm({ open, onClose, orderId }: ComplaintFormProps) {
|
||||
const [complaintBody, setComplaintBody] = useState('');
|
||||
const [complaintImages, setComplaintImages] = useState<{ uri?: string }[]>([]);
|
||||
const [complaintImages, setComplaintImages] = useState<{ uri: string; mimeType: string }[]>([]);
|
||||
|
||||
// API function
|
||||
const raiseComplaintApi = async (payload: { complaintBody: string; images: { uri?: string }[] }) => {
|
||||
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 raiseComplaintMutation = trpc.user.complaint.raise.useMutation();
|
||||
const { upload, isUploading } = useUploadToObjectStorage();
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
const handleRemoveImage = (image: { uri: string; mimeType: string }) => {
|
||||
setComplaintImages(prev => prev.filter(img => img.uri !== image.uri));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!complaintBody.trim()) {
|
||||
Alert.alert('Error', 'Please enter complaint details');
|
||||
return;
|
||||
}
|
||||
|
||||
raiseComplaintMutation.mutate(
|
||||
{
|
||||
try {
|
||||
let imageKeys: string[] = [];
|
||||
|
||||
// Upload images if provided
|
||||
if (complaintImages.length > 0) {
|
||||
// Fetch blobs from URIs
|
||||
const imagesWithBlobs = await Promise.all(
|
||||
complaintImages.map(async (img) => {
|
||||
const response = await fetch(img.uri);
|
||||
const blob = await response.blob();
|
||||
return { blob, mimeType: img.mimeType };
|
||||
})
|
||||
);
|
||||
|
||||
const result = await upload({
|
||||
images: imagesWithBlobs,
|
||||
contextString: 'complaint',
|
||||
});
|
||||
imageKeys = result.keys;
|
||||
}
|
||||
|
||||
// Submit complaint
|
||||
await raiseComplaintMutation.mutateAsync({
|
||||
orderId,
|
||||
complaintBody: complaintBody.trim(),
|
||||
images: complaintImages,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
imageKeys,
|
||||
});
|
||||
|
||||
Alert.alert('Success', 'Complaint raised successfully');
|
||||
setComplaintBody('');
|
||||
setComplaintImages([]);
|
||||
onClose();
|
||||
},
|
||||
onError: (error: any) => {
|
||||
} catch (error: any) {
|
||||
Alert.alert('Error', error.message || 'Failed to raise complaint');
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
|
@ -109,14 +111,15 @@ export default function ComplaintForm({ open, onClose, orderId }: ComplaintFormP
|
|||
images={complaintImages}
|
||||
onAddImage={pickComplaintImage}
|
||||
onRemoveImage={(uri) => setComplaintImages(prev => prev.filter(img => img.uri !== uri))}
|
||||
allowMultiple={true}
|
||||
/>
|
||||
|
||||
<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}
|
||||
disabled={raiseComplaintMutation.isPending}
|
||||
disabled={isUploading || raiseComplaintMutation.isPending}
|
||||
>
|
||||
{raiseComplaintMutation.isPending ? (
|
||||
{isUploading || raiseComplaintMutation.isPending ? (
|
||||
<ActivityIndicator color="white" />
|
||||
) : (
|
||||
<MyText style={tw`text-white font-bold text-lg`}>Submit Complaint</MyText>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useState } from 'react';
|
|||
import { trpc } from '../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 {
|
||||
blob: Blob;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue